diff options
-rw-r--r-- | exitter.el | 257 |
1 files changed, 243 insertions, 14 deletions
@@ -27,20 +27,66 @@ exitter-url-task (format "%s/onboarding/task.json" exitter-url-endpoint) exitter-url-token - (format "https://api.twitter.com/oauth2/token")) + (format "https://api.twitter.com/oauth2/token") + exitter-url-tweet-detail + "https://api.twitter.com/graphql/3XDB26fBve-MmjHaWTUZxA/TweetDetail") (setq exitter-tor-param "-x socks5://127.0.0.1:9050/") -(setq 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-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" . :json-true) + ("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" . :json-true) + ("longform_notetweets_inline_media_enabled" . :json-false) + ("longform_notetweets_richtext_consumption_enabled" . :json-true) + ("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" . :json-true) + ("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" . :json-true) + ("rweb_lists_timeline_redesign_enabled" . :json-true) + ("spaces_2022_h2_clipping" . :json-true) + ("spaces_2022_h2_spaces_communities" . :json-true) + ("standardized_nudges_misinfo" . :json-false) + ("subscriptions_verification_info_enabled" . :json-true) + ("subscriptions_verification_info_reason_enabled" . :json-true) + ("subscriptions_verification_info_verified_since_enabled" . :json-true) + ("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) @@ -209,7 +255,7 @@ ((exitter-find-subtask data "LoginSuccessSubtask") (message "LoginSuccessSubtask") (let* ((subtask - (print (exitter-find-subtask data "LoginSuccessSubtask"))) + (exitter-find-subtask data "LoginSuccessSubtask")) (open-account (alist-get 'open_account subtask))) (setq exitter-oauth-token (alist-get 'oauth_token open-account) @@ -253,4 +299,187 @@ (message "Got error: %S" error-thrown))) )) +(defun exitter-get-sign-oauth (uri method) + (let* ((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"))) + (method-up (upcase method)) + (link (url-hexify-string uri)) + (param-to-sign-unencoded + (mapconcat + (lambda (pair) + (format "%s=%s" (car pair) (url-hexify-string (cdr pair)))) + 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 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))) + params + ", ")))) + +(defun exitter-do-fetch (uri headers) + (when exitter-debug (message "entering exitter-do-fetch")) + (let ((authorization (exitter-get-sign-oauth uri "GET"))) + (request uri + :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)") + ) + :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" . :json-true) + ("withQuickPromoteEligibilityTweetFields" . :json-false) + ("includeHasBirdwatchNotes" . :json-false) + ("withBirdwatchNotes" . :json-false) + ("withVoice" . :json-false) + ("withV2Timeline" . :json-true) + )))) + (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) |