瀏覽代碼

initial implementation of TweetDetails

Yuchen Pei 1 年之前
父節點
當前提交
2a528e8643
共有 1 個文件被更改,包括 243 次插入14 次删除
  1. 243 14
      exitter.el

+ 243 - 14
exitter.el

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