Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 43 additions & 72 deletions gptel-gh.el
Original file line number Diff line number Diff line change
Expand Up @@ -180,26 +180,28 @@
(cl-defstruct (gptel--gh (:include gptel-openai)
(:copier nil)
(:constructor gptel--make-gh))
token github-token sessionid machineid)
sessionid machineid)

(defcustom gptel-gh-github-token-file (expand-file-name ".cache/copilot-chat/github-token"
user-emacs-directory)
"File where the GitHub token is stored."
:type 'string
:group 'gptel)

(defcustom gptel-gh-token-file (expand-file-name ".cache/copilot-chat/token"
user-emacs-directory)
"File where the chat token is cached."
:type 'string
:group 'gptel)
(defvar gptel-gh-github-token-load 'gptel--gh-restore-from-file
"Function to load the current github token.")

(defvar gptel-gh-github-token-save 'gptel--gh-save-to-file
"Function to save the new github token.")

(defconst gptel--gh-auth-common-headers
`(("editor-plugin-version" . "gptel/*")
("editor-version" . ,(concat "emacs/" emacs-version))))

(defconst gptel--gh-client-id "Iv1.b507a08c87ecfe98")

(defvar gptel--gh-token nil "Variable that holds the chat token in memory")

;; https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)
(defun gptel--gh-uuid ()
"Generate a UUID v4-1."
Expand All @@ -219,25 +221,25 @@
(setq hex (nconc hex (list (aref hex-chars (random 16))))))
(apply #'string hex)))

(defun gptel--gh-restore (file)
"Restore saved object from FILE."
(when (file-exists-p file)
(defun gptel--gh-restore-from-file ()
"Restore saved object from gptel-gh-github-token-file."
(when (file-exists-p gptel-gh-github-token-file)
;; We set the coding system to `utf-8-auto-dos' when reading so that
;; files with CR EOL can still be read properly
(let ((coding-system-for-read 'utf-8-auto-dos))
(with-temp-buffer
(set-buffer-multibyte nil)
(insert-file-contents-literally file)
(insert-file-contents-literally gptel-gh-github-token-file)
(goto-char (point-min))
(read (current-buffer))))))

(defun gptel--gh-save (file obj)
"Save OBJ to FILE."
(defun gptel--gh-save-to-file (obj)
"Save OBJ to gptel--gh-save-to-file."
(let ((print-length nil)
(print-level nil)
(coding-system-for-write 'utf-8-unix))
(make-directory (file-name-directory file) t)
(write-region (prin1-to-string obj) nil file nil :silent)
(make-directory (file-name-directory gptel-gh-github-token-file) t)
(write-region (prin1-to-string obj) nil gptel-gh-github-token-file nil :silent)
obj))

(defun gptel-gh-login ()
Expand All @@ -249,20 +251,7 @@ In SSH sessions, the URL and code will be displayed for manual entry
instead of attempting to open a browser automatically."
(interactive)
;; Determine which GitHub backend to use
(let ((gh-backend
(cond
;; If current backend is GitHub, use it
((and (boundp 'gptel-backend)
gptel-backend
(gptel--gh-p gptel-backend))
gptel-backend)
;; Otherwise, find any GitHub backend
((cl-find-if (lambda (b) (gptel--gh-p b))
(mapcar #'cdr gptel--known-backends)))
;; No GitHub backend found
(t (user-error "No GitHub Copilot backend found. \
Please set one up with `gptel-make-gh-copilot' first"))))
;; Detect SSH sessions
(let (;; Detect SSH sessions
(in-ssh-session (or (getenv "SSH_CLIENT")
(getenv "SSH_CONNECTION")
(getenv "SSH_TTY"))))
Expand All @@ -287,28 +276,24 @@ in your local browser, enter the code, and authorize. Press ENTER after authori
(format "Your one-time code %s is copied. \
Press ENTER to open GitHub in your browser. \
If your browser does not open automatically, browse to %s."
user_code verification_uri))
(browse-url verification_uri)
(read-from-minibuffer "Press ENTER after authorizing. "))
;; Use gh-backend for token storage
(thread-last
(plist-get
(gptel--url-retrieve
"https://github.com/login/oauth/access_token"
:method 'post
:headers gptel--gh-auth-common-headers
:data `( :client_id ,gptel--gh-client-id
:device_code ,device_code
:grant_type "urn:ietf:params:oauth:grant-type:device_code"))
:access_token)
(gptel--gh-save gptel-gh-github-token-file)
(setf (gptel--gh-github-token gh-backend))))
;; Check gh-backend for success
(if (and (gptel--gh-github-token gh-backend)
(not (string-empty-p
(gptel--gh-github-token gh-backend))))
(message "Successfully logged in to GitHub Copilot.")
(user-error "Error: You might not have access to GitHub Copilot Chat!"))))
user_code verification_uri))
(browse-url verification_uri)
(read-from-minibuffer "Press ENTER after authorizing."))
(thread-last
(plist-get
(gptel--url-retrieve
"https://github.com/login/oauth/access_token"
:method 'post
:headers gptel--gh-auth-common-headers
:data `( :client_id ,gptel--gh-client-id
:device_code ,device_code
:grant_type "urn:ietf:params:oauth:grant-type:device_code"))
:access_token)
(funcall gptel-gh-github-token-save))
(let ((github-token (funcall gptel-gh-github-token-load)))
(if (and github-token (not (string-empty-p github-token)))
(message "Successfully logged in to GitHub Copilot")
(user-error "Error: You might not have access to GitHub Copilot Chat!"))))))

(defun gptel--gh-renew-token ()
"Renew session token."
Expand All @@ -317,38 +302,25 @@ If your browser does not open automatically, browse to %s."
"https://api.github.com/copilot_internal/v2/token"
:method 'get
:headers `(("authorization"
. ,(format "token %s" (gptel--gh-github-token gptel-backend)))
. ,(format "token %s" (funcall gptel-gh-github-token-load)))
,@gptel--gh-auth-common-headers))))
(if (not (plist-get token :token))
(progn
(setf (gptel--gh-github-token gptel-backend) nil)
(user-error "Error: You might not have access to GitHub Copilot Chat!"))
(thread-last
(gptel--gh-save gptel-gh-token-file token)
(setf (gptel--gh-token gptel-backend))))))
(user-error "Error: You might not have access to GitHub Copilot Chat!")
(setq gptel--gh-token token))))

(defun gptel--gh-auth ()
"Authenticate with GitHub Copilot API.

We first need github authorization (github token).
Then we need a session token."
(unless (gptel--gh-github-token gptel-backend)
(let ((token (gptel--gh-restore gptel-gh-github-token-file)))
(if token
(setf (gptel--gh-github-token gptel-backend) token)
(gptel-gh-login))))

(when (null (gptel--gh-token gptel-backend))
;; try to load token from `gptel-gh-token-file'
(setf (gptel--gh-token gptel-backend)
(gptel--gh-restore gptel-gh-token-file)))
(unless (funcall gptel-gh-github-token-load)
(gptel-gh-login))

(pcase-let (((map :token :expires_at)
(gptel--gh-token gptel-backend)))
gptel--gh-token))
(when (or (null token)
(and expires_at
(> (round (float-time (current-time)))
expires_at)))
(> (round (float-time)) expires_at)))
(gptel--gh-renew-token))))

;;;###autoload
Expand All @@ -357,8 +329,7 @@ Then we need a session token."
(header (lambda ()
(gptel--gh-auth)
`(("openai-intent" . "conversation-panel")
("authorization" . ,(concat "Bearer "
(plist-get (gptel--gh-token gptel-backend) :token)))
("authorization" . ,(concat "Bearer " (plist-get gptel--gh-token :token)))
("x-request-id" . ,(gptel--gh-uuid))
("vscode-sessionid" . ,(or (gptel--gh-sessionid gptel-backend) ""))
("vscode-machineid" . ,(or (gptel--gh-machineid gptel-backend) ""))
Expand Down