Skip to content

Commit

Permalink
Merge pull request #2 from jcs-elpa/premium
Browse files Browse the repository at this point in the history
Premium version implementation
  • Loading branch information
jcs090218 authored Oct 28, 2020
2 parents 50c8aae + d682edd commit df61893
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 38 deletions.
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
## Examples

Below is an simple example that how you can use this library for calling
Below is an simple example that how you can use this library for calling
Grammarly API interface.

```el
Expand All @@ -27,15 +27,25 @@ Grammarly API interface.
(grammarly-check-text "Hello World")
```

## Using a Paid Grammarly Account

You will need the set the following variable in order to use paid version
of Grammarly!

```el
(setq grammarly-username "") ; Your Grammarly Username
(setq grammarly-password "") ; Your Grammarly Password
```

## References

* [grammarly-api](https://github.com/dexterleng/grammarly-api)
* [reverse-engineered-grammarly-api](https://github.com/c0nn3r/reverse-engineered-grammarly-api)
* [grammarly (vscode)](https://github.com/znck/grammarly)

## Todo List

- [ ] Support multiple requests at the same time.
- [ ] Support login and premium account.

## Contribution

Expand Down
197 changes: 161 additions & 36 deletions grammarly.el
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,32 @@
:group 'text
:link '(url-link :tag "Github" "https://github.com/jcs-elpa/grammarly"))

(defcustom grammarly-username ""
"Grammarly login username."
:type 'string
:group 'grammarly)

(defcustom grammarly-password ""
"Grammarly login password."
:type 'string
:group 'grammarly)

(defconst grammarly--user-agent
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:68.0) Gecko/20100101 Firefox/68.0"
"User agent.")

(defconst grammarly--browser-header
`(("User-Agent" . ())
("Accept" . "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3")
("Accept-Language" . "en-GB,en-US;q=0.9,en;q=0.8")
("Cache-Control" . "no-cache")
("Pragma" . "no-cache"))
"Header for simulate using a browser.")

(defconst grammarly--authorize-msg
'(("origin" . "chrome-extension://kbfnbcaeplbcioakkpcpgfkobkghlhen")
`(("origin" . "chrome-extension://kbfnbcaeplbcioakkpcpgfkobkghlhen")
("Cookie" . "$COOKIES$")
("User-Agent" . "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:68.0) Gecko/20100101 Firefox/68.0"))
("User-Agent" . ,grammarly--user-agent))
"Authorize message for Grammarly API.")

(defconst grammarly--init-msg
Expand Down Expand Up @@ -100,45 +122,155 @@
(defvar grammarly--timer nil
"Universal timer for each await use.")

(defvar grammarly--start-checking-p nil
"Flag to after we are done preparing; basically after authentication process.")

(defvar grammarly--show-debug-message nil
"Flag to see if we show debug messages.")

;;
;; (@* "Util" )
;;

(defun grammarly--debug-message (fmt &rest args)
"Debug message like function `message' with same argument FMT and ARGS."
(when grammarly--show-debug-message
(apply 'message fmt args)))

(defun grammarly--kill-websocket ()
"Kill the websocket."
(when grammarly--client
(websocket-close grammarly--client)
(setq grammarly--client nil)))

(defun grammarly--kill-timer ()
"Kill the timer."
(when (timerp grammarly--timer)
(cancel-timer grammarly--timer)
(setq grammarly--timer nil)))

(defun grammarly--execute-function-list (lst &rest args)
"Execute all function LST with ARGS."
(cond
((functionp lst) (apply lst args))
((listp lst) (dolist (fnc lst) (apply fnc args)))
(t (user-error "[ERROR] Function does not exists: %s" lst))))

;;
;; (@* "Cookie" )
;;

(defvar grammarly--auth-cookie '()
"Authorization cookie container.")

(defun grammarly--last-cookie (cookie cookies)
"Check if current COOKIE the last cookie from COOKIES."
(equal (nth (1- (length cookies)) cookies) cookie))

(defun grammarly--get-cookie-by-name (name)
"Return cookie value by cookie NAME."
(let ((len (length grammarly--auth-cookie)) (index 0) break cookie-val)
(while (and (not break) (< index len))
(let* ((cookie (nth index grammarly--auth-cookie))
(cookie-name (car cookie)))
(when (string= cookie-name name)
(setq cookie-val (cdr cookie))
(setq break t)))
(setq index (1+ index)))
cookie-val))

(defun grammarly--form-cookie ()
"Form all cookies into one string."
(setq grammarly--auth-cookie '())
(let ((sec-cookies (request-cookie-alist ".grammarly.com" "/" t))
(cookie-str ""))
(cookie-str "") cookie-name cookie-val)
(dolist (cookie sec-cookies)
(setq cookie-str
(format "%s %s=%s%s" cookie-str (car cookie) (cdr cookie)
(if (grammarly--last-cookie cookie sec-cookies) "" ";"))))
(setq cookie-name (car cookie) cookie-val (cdr cookie)
cookie-str
(format "%s %s=%s%s" cookie-str cookie-name cookie-val
(if (grammarly--last-cookie cookie sec-cookies) "" ";")))
(push (cons cookie-name cookie-val) grammarly--auth-cookie))
(setq grammarly--auth-cookie (reverse grammarly--auth-cookie))
(string-trim cookie-str)))

(defun grammarly--update-cookie ()
"Refresh the cookie once."
(setq grammarly--cookies (grammarly--form-cookie)))

(defun grammarly--get-cookie ()
"Get cookie."
(setq grammarly--cookies "") ; Reset to clean string.
(setq grammarly--start-checking-p nil
grammarly--cookies "") ; Reset to clean string.
(request
"https://grammarly.com/signin"
:type "GET"
:headers
(append grammarly--browser-header
'(("Sec-Fetch-Mode" . "navigate")
("Sec-Fetch-Sit" . "same-origin")
("Sec-Fetch-User" . "?1")
("Upgrade-Insecure-Requests" . "1")
("Referer" . "https://www.grammarly.com/")))
:success
(cl-function
(lambda (&key _response &allow-other-keys)
(grammarly--update-cookie)
(if (grammarly-premium-p) ; Try login to use paid version.
(grammarly--authenticate)
(setq grammarly--start-checking-p t))))
:error
;; NOTE: Accept, error.
(cl-function
(lambda (&rest args &key _error-thrown &allow-other-keys)
(grammarly--debug-message "[ERROR] Error while getting cookie: %s" args)))))

;;
;; (@* "Login" )
;;

(defun grammarly-premium-p ()
"Return non-nil means we are using premium version."
(and (not (string-empty-p grammarly-username))
(not (string-empty-p grammarly-password))))

(defun grammarly--authenticate ()
"Login to Grammarly for premium version."
(message "connecting as %s" grammarly-username)
(request
"https://grammarly.com/"
:type "GET"
:headers
'(("User-Agent" . ())
("Accept" . "application/json, text/plain, */*"))
:success
(cl-function
(lambda (&key _response &allow-other-keys)
(setq grammarly--cookies (grammarly--form-cookie))))
:error
;; NOTE: Accept, error.
(cl-function
(lambda (&rest args &key _error-thrown &allow-other-keys)
(user-error "[ERROR] Error while getting cookie")))))
"https://auth.grammarly.com/v3/api/login"
:type "POST"
:headers
`(("accept" . "application/json")
("accept-language" . "en-GB,en-US;q=0.9,en;q=0.8")
("content-type" . "application/json")
("user-agent" . ,grammarly--user-agent)
("x-client-type" . "funnel")
("x-client-version" . "1.2.2026")
("x-container-id" . ,(grammarly--get-cookie-by-name "gnar_containerId"))
("x-csrf-token" . ,(grammarly--get-cookie-by-name "csrf-token"))
("sec-fetch-site" . "same-site")
("sec-fetch-mode" . "cors")
("cookie" . ,grammarly--cookies))
:data
(json-encode
`(("email_login" . (("email" . ,grammarly-username)
("password" . ,grammarly-password)
("secureLogin" . "false")))))
:success
(cl-function
(lambda (&key _response &allow-other-keys)
(setq grammarly--start-checking-p t)))
:error
;; NOTE: Accept, error.
(cl-function
(lambda (&rest args &key _error-thrown &allow-other-keys)
(setq grammarly--start-checking-p t) ; Go back and use anonymous version
(grammarly--debug-message
"[ERROR] Error while authenticating login: %s" args)))))

;;
;; (@* "WebSocket" )
;;

(defun grammarly--form-authorize-list ()
"Form the authorize list."
Expand All @@ -161,8 +293,7 @@
grammarly--client
(websocket-open
"wss://capi.grammarly.com/freews"
:custom-header-alist
(grammarly--form-authorize-list)
:custom-header-alist (grammarly--form-authorize-list)
:on-open
(lambda (_ws)
(grammarly--execute-function-list grammarly-on-open-function-list)
Expand All @@ -175,22 +306,15 @@
(grammarly--default-callback (websocket-frame-payload frame)))
:on-error
(lambda (_ws _type err)
(user-error "[ERROR] Connection error while opening websocket: %s" err))
(grammarly--debug-message
"[ERROR] Connection error while opening websocket: %s" err))
:on-close
(lambda (_ws)
(grammarly--execute-function-list grammarly-on-close-function-list)))))

(defun grammarly--kill-websocket ()
"Kill the websocket."
(when grammarly--client
(websocket-close grammarly--client)
(setq grammarly--client nil)))

(defun grammarly--kill-timer ()
"Kill the timer."
(when (timerp grammarly--timer)
(cancel-timer grammarly--timer)
(setq grammarly--timer nil)))
;;
;; (@* "Core" )
;;

(defun grammarly--reset-timer (fnc pred)
"Reset the timer for the next run with FNC and PRED."
Expand All @@ -214,8 +338,9 @@
(user-error "[ERROR] Text can't be 'nil' or 'empty'")
(setq grammarly--text text)
(grammarly--get-cookie)
;; Delay, until we get the initial cookie.
(grammarly--reset-timer #'grammarly--after-got-cookie
'(lambda () (string-empty-p grammarly--cookies)))))
'(lambda () (null grammarly--start-checking-p)))))

(provide 'grammarly)
;;; grammarly.el ends here

0 comments on commit df61893

Please sign in to comment.