|
|
@@ -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)
|