| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489 |
- ;; -*- lexical-binding: t; -*-
- ;; Copyright (C) 2024 Free Software Foundation, Inc.
- ;; Author: Yuchen Pei <id@ypei.org>
- ;; Package-Requires: ((emacs "29.4") (request "0.3.3"))
- ;; This file is part of exitter.
- ;; exitter is free software: you can redistribute it and/or modify it under
- ;; the terms of the GNU Affero General Public License as published by
- ;; the Free Software Foundation, either version 3 of the License, or
- ;; (at your option) any later version.
- ;; exitter is distributed in the hope that it will be useful, but WITHOUT
- ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
- ;; Public License for more details.
- ;; You should have received a copy of the GNU Affero General Public
- ;; License along with exitter. If not, see <https://www.gnu.org/licenses/>.
- (require 'request)
- (defvar exitter-url-endpoint "https://api.twitter.com/1.1")
- (defvar exitter-url-activate
- (format "%s/guest/activate.json" exitter-url-endpoint))
- (defvar exitter-url-task
- (format "%s/onboarding/task.json" exitter-url-endpoint))
- (defvar exitter-url-token
- (format "https://api.twitter.com/oauth2/token"))
- (defvar exitter-url-tweet-detail
- "https://api.twitter.com/graphql/3XDB26fBve-MmjHaWTUZxA/TweetDetail")
- (defvar exitter-tor-param "-x socks5://127.0.0.1:9050/")
- (defvar exitter-init-headers
- `(
- ("Content-Type" . "application/json")
- ("User-Agent" . "TwitterAndroid/10.10.0 (29950000-r-0) ONEPLUS+A3010/9 (OnePlus;ONEPLUS+A3010;OnePlus;OnePlus3;0;;1;2016)")
- ;; ("User-Agent" . "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36")
- ("X-Twitter-API-Version" . "5")
- ("X-Twitter-Client" . "TwitterAndroid")
- ("X-Twitter-Client-Version" . "10.10.0")
- ("OS-Version" . "28")
- ("System-User-Agent" . "Dalvik/2.1.0 (Linux; U; Android 9; ONEPLUS A3010 Build/PKQ1.181203.001)")
- ("X-Twitter-Active-User" . "yes")
- ))
- (defvar exitter-default-features
- (json-encode
- '(
- ("android_graphql_skip_api_media_color_palette" . :json-false)
- ("blue_business_profile_image_shape_enabled" . :json-false)
- ("creator_subscriptions_subscription_count_enabled" . :json-false)
- ("creator_subscriptions_tweet_preview_api_enabled" . t)
- ("freedom_of_speech_not_reach_fetch_enabled" . :json-false)
- ("graphql_is_translatable_rweb_tweet_is_translatable_enabled" . :json-false)
- ("hidden_profile_likes_enabled" . :json-false)
- ("highlights_tweets_tab_ui_enabled" . :json-false)
- ("interactive_text_enabled" . :json-false)
- ("longform_notetweets_consumption_enabled" . t)
- ("longform_notetweets_inline_media_enabled" . :json-false)
- ("longform_notetweets_richtext_consumption_enabled" . t)
- ("longform_notetweets_rich_text_read_enabled" . :json-false)
- ("responsive_web_edit_tweet_api_enabled" . :json-false)
- ("responsive_web_enhance_cards_enabled" . :json-false)
- ("responsive_web_graphql_exclude_directive_enabled" . t)
- ("responsive_web_graphql_skip_user_profile_image_extensions_enabled" . :json-false)
- ("responsive_web_graphql_timeline_navigation_enabled" . :json-false)
- ("responsive_web_media_download_video_enabled" . :json-false)
- ("responsive_web_text_conversations_enabled" . :json-false)
- ("responsive_web_twitter_article_tweet_consumption_enabled" . :json-false)
- ("responsive_web_twitter_blue_verified_badge_is_enabled" . t)
- ("rweb_lists_timeline_redesign_enabled" . t)
- ("spaces_2022_h2_clipping" . t)
- ("spaces_2022_h2_spaces_communities" . t)
- ("standardized_nudges_misinfo" . :json-false)
- ("subscriptions_verification_info_enabled" . t)
- ("subscriptions_verification_info_reason_enabled" . t)
- ("subscriptions_verification_info_verified_since_enabled" . t)
- ("super_follow_badge_privacy_enabled" . :json-false)
- ("super_follow_exclusive_tweet_notifications_enabled" . :json-false)
- ("super_follow_tweet_api_enabled" . :json-false)
- ("super_follow_user_api_enabled" . :json-false)
- ("tweet_awards_web_tipping_enabled" . :json-false)
- ("tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled" . :json-false)
- ("tweetypie_unmention_optimization_enabled" . :json-false)
- ("unified_cards_ad_metadata_container_dynamic_card_content_query_enabled" . :json-false)
- ("verified_phone_label_enabled" . :json-false)
- ("vibe_api_enabled" . :json-false)
- ("view_counts_everywhere_api_enabled" . :json-false)
- )))
- (defvar exitter-oauth-consumer-key nil)
- (defvar exitter-oauth-consumer-secret nil)
- (defvar exitter-access-token nil)
- (defvar exitter-username nil)
- (defvar exitter-password nil)
- (defvar exitter-email nil)
- (defvar exitter-oauth-token nil)
- (defvar exitter-oauth-token-secret nil)
- (defvar exitter-oauth-token-ctime nil)
- (defvar exitter-debug nil)
- ;;; for debugging
- (if exitter-debug
- (setq request-message-level 'blather)
- (setq request-message-level -1))
- (defun exitter-get-access-token ()
- (let ((oauth-consumer-key-secret
- (base64-encode-string
- (format "%s:%s" exitter-oauth-consumer-key
- exitter-oauth-consumer-secret)
- t)))
- (request exitter-url-token
- :headers `(("Authorization" .
- ,(format "Basic %s" oauth-consumer-key-secret))
- ("Content-Type" . "application/x-www-form-urlencoded"))
- :data "grant_type=client_credentials"
- :parser 'json-read
- :type "POST"
- :success (cl-function
- (lambda (&key data &allow-other-keys)
- ;; will generate exactly the same token as
- ;; `exitter-access-token'
- (print (alist-get 'access_token data))))
- :error
- (cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
- (message "Got error: %S" error-thrown)))
- )))
- (defun exitter-get-guest-token ()
- (when exitter-debug (message "entering exitter-get-guest-token"))
- (request exitter-url-activate
- :headers `(("Authorization" . ,(format "Bearer %s" exitter-access-token)))
- :parser 'json-read
- :type "POST"
- :success (cl-function
- (lambda (&key data &allow-other-keys)
- (exitter-login-flow-token (alist-get 'guest_token data))))
- :error
- (cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
- (message "Got error: %S" error-thrown)))
- ))
- (defun exitter-login-flow-token (guest-token)
- (when exitter-debug (message "entering exitter-login-flow-token"))
- (let ((headers `(,@exitter-init-headers
- ("Authorization" . ,(format "Bearer %s" exitter-access-token))
- ("X-Guest-Token" . ,guest-token)
- )))
- (request exitter-url-task
- :params '(("flow_name" . "login")
- ("lang" . "en"))
- :headers headers
- :data (json-encode
- '(("flow_token" . nil)
- ("input_flow_data" .
- (("country_code" . nil)
- ("flow_context" .
- (("referral_context" .
- (("referral_details" .
- "utm_source=google-play&utm_medium=organic")
- ("referrer_url" . "")))
- ("start_location" . (("location" . "deeplink")))
- ("request_variant" . nil)
- ("target_user_id" . 0)))))))
- :parser 'json-read
- :type "POST"
- :complete (cl-function
- (lambda (&key response &allow-other-keys)
- (let* ((att
- (request-response-header response "att"))
- (data
- (request-response-data response))
- (flow-token (alist-get 'flow_token data)))
- (unless (exitter-find-subtask data "LoginEnterUserIdentifier")
- (error "Subtask LoginEnterUserIdentifier not found"))
- ;; (pp data)
- ;; (message "flow-token: %s\natt: %s"
- ;; (alist-get 'flow_token data)
- ;; att)
- (when att
- (setq headers
- `(,@headers
- ("att" . ,att)
- ("cookie" . ,(format "att=%s" att)))))
- (exitter-enter-username flow-token headers))))
- :error
- (cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
- (message "Got error: %S" error-thrown)))
- )))
- (defun exitter-find-subtask (data subtask-id)
- (when exitter-debug
- (message "entering exitter-find-subtask")
- (message "subtask-id: %s" subtask-id))
- (seq-find
- (lambda (subtask)
- (equal (alist-get 'subtask_id subtask) subtask-id))
- (alist-get 'subtasks data)))
- (defun exitter-report-error (&rest args &key error-thrown &allow-other-keys)
- (message "Got error: %S" error-thrown))
- (defun exitter-enter-username (flow-token headers)
- (when exitter-debug (message "entering exitter-enter-username"))
- (request exitter-url-task
- :params '(("lang" . "en"))
- :headers headers
- :data (json-encode
- `(("flow_token" . ,flow-token)
- ("subtask_inputs" .
- [(("enter_text" .
- (("suggestion_id" . nil)
- ("text" . ,exitter-username)
- ("link" . "next_link")))
- ("subtask_id" . "LoginEnterUserIdentifier"))])))
- :type "POST"
- :parser 'json-read
- :success (cl-function
- (lambda (&key data &allow-other-keys)
- (let ((new-flow-token
- (alist-get 'flow_token data)))
- (cond
- ((exitter-find-subtask data "LoginEnterPassword")
- (message "LoginEnterPassword")
- (exitter-enter-password new-flow-token headers))
- ((exitter-find-subtask data "LoginEnterAlternateIdentifierSubtask")
- (message "LoginEnterAlternateIdentifierSubtask")
- (exitter-enter-email new-flow-token headers))
- (t (message "Cannot find any matching subtasks"))))
- ))
- :error
- (cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
- (message "Got error: %S" error-thrown)))))
- (defun exitter-enter-password (flow-token headers)
- (when exitter-debug (message "entering exitter-enter-password"))
- (request exitter-url-task
- :params '(("lang" . "en"))
- :headers headers
- :data (json-encode
- `(("flow_token" . ,flow-token)
- ("subtask_inputs" .
- [(("enter_password" .
- (("password" . ,exitter-password)
- ("link" . "next_link")))
- ("subtask_id" . "LoginEnterPassword"))])))
- :type "POST"
- :parser 'json-read
- :success (cl-function
- (lambda (&key data &allow-other-keys)
- (cond
- ((exitter-find-subtask data "LoginSuccessSubtask")
- (message "LoginSuccessSubtask")
- (let* ((subtask
- (exitter-find-subtask data "LoginSuccessSubtask"))
- (open-account (alist-get 'open_account subtask)))
- (setq exitter-oauth-token
- (alist-get 'oauth_token open-account)
- exitter-oauth-token-secret
- (alist-get 'oauth_token_secret open-account)
- exitter-oauth-token-ctime
- (format-time-string "%Y-%m-%d %a %H:%M:%S"
- (current-time)))))
- (t (message "Cannot find any matching subtasks"))) ))
- :error
- (cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
- (message "Got error: %S" error-thrown)))
- ))
- (defun exitter-enter-email (flow-token headers)
- (when exitter-debug (message "entering exitter-enter-email"))
- (request exitter-url-task
- :params '(("lang" . "en"))
- :headers headers
- :data (json-encode
- `(("flow_token" . ,flow-token)
- ("subtask_inputs" .
- [(("enter_text" .
- (("text" . ,exitter-email)
- ("link" . "next_link")))
- ("subtask_id" . "LoginEnterAlternateIdentifierSubtask"))])))
- :type "POST"
- :parser 'json-read
- :success (cl-function
- (lambda (&key data &allow-other-keys)
- (let ((new-flow-token
- (alist-get 'flow_token data)))
- (cond
- ((exitter-find-subtask data "LoginEnterPassword")
- (message "LoginEnterPassword")
- (exitter-enter-password new-flow-token headers))
- (t (message "Cannot find any matching subtasks"))))
- ))
- :error
- (cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
- (message "Got error: %S" error-thrown)))
- ))
- (defun exitter-get-sign-oauth (link url-params method)
- (let* ((oauth-params `(("oauth_consumer_key" . ,exitter-oauth-consumer-key)
- ("oauth_nonce" . ,(exitter-nonce))
- ("oauth_signature_method" . "HMAC-SHA1")
- ("oauth_timestamp" . ,(format-time-string "%s" (current-time)))
- ("oauth_token" . ,exitter-oauth-token)
- ("oauth_version" . "1.0")))
- (all-params (sort (seq-concatenate 'list url-params oauth-params)
- (lambda (a b) (string< (car a) (car b)))))
- (method-up (upcase method))
- (hexed-link (url-hexify-string link))
- (param-to-sign-unencoded
- (mapconcat
- (lambda (pair)
- (format "%s=%s" (car pair) (url-hexify-string (cdr pair))))
- all-params
- "&"))
- (param-to-sign
- (replace-regexp-in-string
- "&" "%26"
- (replace-regexp-in-string
- "=" "%3d"
- (replace-regexp-in-string
- "%" "%25"
- (replace-regexp-in-string
- "\\+" "%20"
- param-to-sign-unencoded)))))
- (to-sign (format "%s&%s&%s" method-up hexed-link param-to-sign))
- (signature (url-hexify-string
- (base64-encode-string
- (hmac-sha1 (encode-coding-string
- (format "%s&%s"
- exitter-oauth-consumer-secret
- exitter-oauth-token-secret)
- 'utf-8)
- (encode-coding-string to-sign 'utf-8)))))
- )
- (message "PARAM-TO-SIGN-UNENC: %s" (prin1-to-string param-to-sign-unencoded))
- (message "PARAM-TO-SIGN: %s" (prin1-to-string param-to-sign))
- (message "TO-SIGN: %s" (prin1-to-string to-sign))
- (format "OAuth realm=\"http://api.twitter.com/\", oauth_signature=\"%s\", %s"
- signature
- (mapconcat
- (lambda (pair)
- (format "%s=\"%s\"" (car pair) (cdr pair)))
- oauth-params
- ", "))))
- (defun exitter-do-fetch (link params &optional headers)
- (when exitter-debug (message "entering exitter-do-fetch"))
- (let ((authorization (exitter-get-sign-oauth link params "GET")))
- (request link
- :headers `(,@headers
- ("Connection" . "Keep-Alive")
- ("Authorization" . ,authorization)
- ("Content-Type" . "application/json")
- ("X-Twitter-Active-User" . "yes")
- ("Authority" . "api.twitter.com")
- ("Accept-Encoding" . "gzip")
- ("Accept-Language" . "en-US,en;q=0.9")
- ("Accept" . "*/*")
- ("DNT" . "1")
- ("User-Agent" .
- "TwitterAndroid/10.10.0 (29950000-r-0) ONEPLUS+A3010/9 \
- (OnePlus;ONEPLUS+A3010;OnePlus;OnePlus3;0;;1;2016)")
- ("X-Twitter-API-Version" . "5")
- ("X-Twitter-Client" . "TwitterAndroid")
- ("X-Twitter-Client-Version" . "10.10.0")
- ("OS-Version" . "28")
- ("System-User-Agent" .
- "Dalvik/2.1.0 (Linux; U; Android 9; \
- ONEPLUS A3010 Build/PKQ1.181203.001)")
- )
- :params params
- :type "GET"
- :parser 'json-read
- :success (cl-function
- (lambda (&key data &allow-other-keys)
- (pp data)))
- :error
- (cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
- (message "Got error: %S" error-thrown)))
- )))
- (defun exitter-get-tweet (id)
- (let ((variables (json-encode
- `(
- ("focalTweetId" . ,id)
- ;; ("referrer" . "tweet")
- ;; ("with_rux_injections" . :json-false)
- ("includePromotedContent" . :json-false)
- ;; ("withCommunity" . t)
- ("withQuickPromoteEligibilityTweetFields" . :json-false)
- ("includeHasBirdwatchNotes" . :json-false)
- ("withBirdwatchNotes" . :json-false)
- ("withVoice" . :json-false)
- ("withV2Timeline" . t)
- ))))
- (message "VARIABLES: ")
- (exitter-do-fetch
- exitter-url-tweet-detail
- `(("variables" . ,variables)
- ("features" . ,exitter-default-features)))))
- (require 'bindat)
- (defun exitter-nonce ()
- (let ((xs))
- (dotimes (_ 32 xs)
- (setq xs (cons (random 256) xs)))
- (replace-regexp-in-string
- "[=/+]" ""
- (base64-encode-string
- (alist-get 'bs (bindat-unpack '((bs str 32)) (vconcat xs)))))))
- (require 'sha1)
- (defun hmac-sha1 (key message)
- "Return an HMAC-SHA1 authentication code for KEY and MESSAGE.
- KEY and MESSAGE must be unibyte strings. The result is a unibyte
- string. Use the function `encode-hex-string' or the function
- `base64-encode-string' to produce human-readable output.
- See URL:<http://en.wikipedia.org/wiki/HMAC> for more information
- on the HMAC-SHA1 algorithm.
- The Emacs multibyte representation actually uses a series of
- 8-bit values under the hood, so we could have allowed multibyte
- strings as arguments. However, internal 8-bit values don't
- correspond to any external representation \(at least for major
- version 22). This makes multibyte strings useless for generating
- hashes.
- Instead, callers must explicitly pick and use an encoding for
- their multibyte data. Most callers will want to use UTF-8
- encoding, which we can generate as follows:
- (let ((unibyte-key (encode-coding-string key 'utf-8 t))
- (unibyte-value (encode-coding-string value 'utf-8 t)))
- (hmac-sha1 unibyte-key unibyte-value))
- For keys and values that are already unibyte, the
- `encode-coding-string' calls just return the same string."
- (when (multibyte-string-p key)
- (error "key %s must be unibyte" key))
- (when (multibyte-string-p message)
- (error "message %s must be unibyte" message))
- ;; The key block is always exactly the block size of the hash
- ;; algorithm. If the key is too small, we pad it with zeroes (or
- ;; instead, we initialize the key block with zeroes and copy the
- ;; key onto the nulls). If the key is too large, we run it
- ;; through the hash algorithm and use the hashed value (strange
- ;; but true).
- (let ((+hmac-sha1-block-size-bytes+ 64)) ; SHA-1 uses 512-bit blocks
- (when (< +hmac-sha1-block-size-bytes+ (length key))
- (setq key (sha1 key nil nil t)))
- (let ((key-block (make-vector +hmac-sha1-block-size-bytes+ 0)))
- (dotimes (i (length key))
- (aset key-block i (aref key i)))
- (let ((opad (make-vector +hmac-sha1-block-size-bytes+ #x5c))
- (ipad (make-vector +hmac-sha1-block-size-bytes+ #x36)))
- (dotimes (i +hmac-sha1-block-size-bytes+)
- (aset ipad i (logxor (aref ipad i) (aref key-block i)))
- (aset opad i (logxor (aref opad i) (aref key-block i))))
- (when (fboundp 'unibyte-string)
- ;; `concat' of Emacs23 (and later?) generates a multi-byte
- ;; string from a vector of characters with eight bit.
- ;; Since `opad' and `ipad' must be unibyte, we have to
- ;; convert them by using `unibyte-string'.
- ;; We cannot use `string-as-unibyte' here because it encodes
- ;; bytes with the manner of UTF-8.
- (setq opad (apply 'unibyte-string (mapcar 'identity opad)))
- (setq ipad (apply 'unibyte-string (mapcar 'identity ipad))))
- (sha1 (concat opad
- (sha1 (concat ipad message)
- nil nil t))
- nil nil t)))))
- (provide 'exitter)
|