exitter.el 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. ;; -*- lexical-binding: t; -*-
  2. ;; Copyright (C) 2024 Free Software Foundation, Inc.
  3. ;; Author: Yuchen Pei <id@ypei.org>
  4. ;; Package-Requires: ((emacs "29.4") (request "0.3.3"))
  5. ;; This file is part of exitter.
  6. ;; exitter is free software: you can redistribute it and/or modify it under
  7. ;; the terms of the GNU Affero General Public License as published by
  8. ;; the Free Software Foundation, either version 3 of the License, or
  9. ;; (at your option) any later version.
  10. ;; exitter is distributed in the hope that it will be useful, but WITHOUT
  11. ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  12. ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
  13. ;; Public License for more details.
  14. ;; You should have received a copy of the GNU Affero General Public
  15. ;; License along with exitter. If not, see <https://www.gnu.org/licenses/>.
  16. (require 'request)
  17. (defvar exitter-url-endpoint "https://api.twitter.com/1.1")
  18. (defvar exitter-url-activate
  19. (format "%s/guest/activate.json" exitter-url-endpoint))
  20. (defvar exitter-url-task
  21. (format "%s/onboarding/task.json" exitter-url-endpoint))
  22. (defvar exitter-url-token
  23. (format "https://api.twitter.com/oauth2/token"))
  24. (defvar exitter-url-tweet-detail
  25. "https://api.twitter.com/graphql/3XDB26fBve-MmjHaWTUZxA/TweetDetail")
  26. (defvar exitter-tor-param "-x socks5://127.0.0.1:9050/")
  27. (defvar exitter-init-headers
  28. `(
  29. ("Content-Type" . "application/json")
  30. ("User-Agent" . "TwitterAndroid/10.10.0 (29950000-r-0) ONEPLUS+A3010/9 (OnePlus;ONEPLUS+A3010;OnePlus;OnePlus3;0;;1;2016)")
  31. ;; ("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")
  32. ("X-Twitter-API-Version" . "5")
  33. ("X-Twitter-Client" . "TwitterAndroid")
  34. ("X-Twitter-Client-Version" . "10.10.0")
  35. ("OS-Version" . "28")
  36. ("System-User-Agent" . "Dalvik/2.1.0 (Linux; U; Android 9; ONEPLUS A3010 Build/PKQ1.181203.001)")
  37. ("X-Twitter-Active-User" . "yes")
  38. ))
  39. (defvar exitter-default-features
  40. (json-encode
  41. '(
  42. ("android_graphql_skip_api_media_color_palette" . :json-false)
  43. ("blue_business_profile_image_shape_enabled" . :json-false)
  44. ("creator_subscriptions_subscription_count_enabled" . :json-false)
  45. ("creator_subscriptions_tweet_preview_api_enabled" . t)
  46. ("freedom_of_speech_not_reach_fetch_enabled" . :json-false)
  47. ("graphql_is_translatable_rweb_tweet_is_translatable_enabled" . :json-false)
  48. ("hidden_profile_likes_enabled" . :json-false)
  49. ("highlights_tweets_tab_ui_enabled" . :json-false)
  50. ("interactive_text_enabled" . :json-false)
  51. ("longform_notetweets_consumption_enabled" . t)
  52. ("longform_notetweets_inline_media_enabled" . :json-false)
  53. ("longform_notetweets_richtext_consumption_enabled" . t)
  54. ("longform_notetweets_rich_text_read_enabled" . :json-false)
  55. ("responsive_web_edit_tweet_api_enabled" . :json-false)
  56. ("responsive_web_enhance_cards_enabled" . :json-false)
  57. ("responsive_web_graphql_exclude_directive_enabled" . t)
  58. ("responsive_web_graphql_skip_user_profile_image_extensions_enabled" . :json-false)
  59. ("responsive_web_graphql_timeline_navigation_enabled" . :json-false)
  60. ("responsive_web_media_download_video_enabled" . :json-false)
  61. ("responsive_web_text_conversations_enabled" . :json-false)
  62. ("responsive_web_twitter_article_tweet_consumption_enabled" . :json-false)
  63. ("responsive_web_twitter_blue_verified_badge_is_enabled" . t)
  64. ("rweb_lists_timeline_redesign_enabled" . t)
  65. ("spaces_2022_h2_clipping" . t)
  66. ("spaces_2022_h2_spaces_communities" . t)
  67. ("standardized_nudges_misinfo" . :json-false)
  68. ("subscriptions_verification_info_enabled" . t)
  69. ("subscriptions_verification_info_reason_enabled" . t)
  70. ("subscriptions_verification_info_verified_since_enabled" . t)
  71. ("super_follow_badge_privacy_enabled" . :json-false)
  72. ("super_follow_exclusive_tweet_notifications_enabled" . :json-false)
  73. ("super_follow_tweet_api_enabled" . :json-false)
  74. ("super_follow_user_api_enabled" . :json-false)
  75. ("tweet_awards_web_tipping_enabled" . :json-false)
  76. ("tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled" . :json-false)
  77. ("tweetypie_unmention_optimization_enabled" . :json-false)
  78. ("unified_cards_ad_metadata_container_dynamic_card_content_query_enabled" . :json-false)
  79. ("verified_phone_label_enabled" . :json-false)
  80. ("vibe_api_enabled" . :json-false)
  81. ("view_counts_everywhere_api_enabled" . :json-false)
  82. )))
  83. (defvar exitter-oauth-consumer-key nil)
  84. (defvar exitter-oauth-consumer-secret nil)
  85. (defvar exitter-access-token nil)
  86. (defvar exitter-username nil)
  87. (defvar exitter-password nil)
  88. (defvar exitter-email nil)
  89. (defvar exitter-oauth-token nil)
  90. (defvar exitter-oauth-token-secret nil)
  91. (defvar exitter-oauth-token-ctime nil)
  92. (defvar exitter-debug nil)
  93. ;;; for debugging
  94. (if exitter-debug
  95. (setq request-message-level 'blather)
  96. (setq request-message-level -1))
  97. (defun exitter-get-access-token ()
  98. (let ((oauth-consumer-key-secret
  99. (base64-encode-string
  100. (format "%s:%s" exitter-oauth-consumer-key
  101. exitter-oauth-consumer-secret)
  102. t)))
  103. (request exitter-url-token
  104. :headers `(("Authorization" .
  105. ,(format "Basic %s" oauth-consumer-key-secret))
  106. ("Content-Type" . "application/x-www-form-urlencoded"))
  107. :data "grant_type=client_credentials"
  108. :parser 'json-read
  109. :type "POST"
  110. :success (cl-function
  111. (lambda (&key data &allow-other-keys)
  112. ;; will generate exactly the same token as
  113. ;; `exitter-access-token'
  114. (print (alist-get 'access_token data))))
  115. :error
  116. (cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
  117. (message "Got error: %S" error-thrown)))
  118. )))
  119. (defun exitter-get-guest-token ()
  120. (when exitter-debug (message "entering exitter-get-guest-token"))
  121. (request exitter-url-activate
  122. :headers `(("Authorization" . ,(format "Bearer %s" exitter-access-token)))
  123. :parser 'json-read
  124. :type "POST"
  125. :success (cl-function
  126. (lambda (&key data &allow-other-keys)
  127. (exitter-login-flow-token (alist-get 'guest_token data))))
  128. :error
  129. (cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
  130. (message "Got error: %S" error-thrown)))
  131. ))
  132. (defun exitter-login-flow-token (guest-token)
  133. (when exitter-debug (message "entering exitter-login-flow-token"))
  134. (let ((headers `(,@exitter-init-headers
  135. ("Authorization" . ,(format "Bearer %s" exitter-access-token))
  136. ("X-Guest-Token" . ,guest-token)
  137. )))
  138. (request exitter-url-task
  139. :params '(("flow_name" . "login")
  140. ("lang" . "en"))
  141. :headers headers
  142. :data (json-encode
  143. '(("flow_token" . nil)
  144. ("input_flow_data" .
  145. (("country_code" . nil)
  146. ("flow_context" .
  147. (("referral_context" .
  148. (("referral_details" .
  149. "utm_source=google-play&utm_medium=organic")
  150. ("referrer_url" . "")))
  151. ("start_location" . (("location" . "deeplink")))
  152. ("request_variant" . nil)
  153. ("target_user_id" . 0)))))))
  154. :parser 'json-read
  155. :type "POST"
  156. :complete (cl-function
  157. (lambda (&key response &allow-other-keys)
  158. (let* ((att
  159. (request-response-header response "att"))
  160. (data
  161. (request-response-data response))
  162. (flow-token (alist-get 'flow_token data)))
  163. (unless (exitter-find-subtask data "LoginEnterUserIdentifier")
  164. (error "Subtask LoginEnterUserIdentifier not found"))
  165. ;; (pp data)
  166. ;; (message "flow-token: %s\natt: %s"
  167. ;; (alist-get 'flow_token data)
  168. ;; att)
  169. (when att
  170. (setq headers
  171. `(,@headers
  172. ("att" . ,att)
  173. ("cookie" . ,(format "att=%s" att)))))
  174. (exitter-enter-username flow-token headers))))
  175. :error
  176. (cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
  177. (message "Got error: %S" error-thrown)))
  178. )))
  179. (defun exitter-find-subtask (data subtask-id)
  180. (when exitter-debug
  181. (message "entering exitter-find-subtask")
  182. (message "subtask-id: %s" subtask-id))
  183. (seq-find
  184. (lambda (subtask)
  185. (equal (alist-get 'subtask_id subtask) subtask-id))
  186. (alist-get 'subtasks data)))
  187. (defun exitter-report-error (&rest args &key error-thrown &allow-other-keys)
  188. (message "Got error: %S" error-thrown))
  189. (defun exitter-enter-username (flow-token headers)
  190. (when exitter-debug (message "entering exitter-enter-username"))
  191. (request exitter-url-task
  192. :params '(("lang" . "en"))
  193. :headers headers
  194. :data (json-encode
  195. `(("flow_token" . ,flow-token)
  196. ("subtask_inputs" .
  197. [(("enter_text" .
  198. (("suggestion_id" . nil)
  199. ("text" . ,exitter-username)
  200. ("link" . "next_link")))
  201. ("subtask_id" . "LoginEnterUserIdentifier"))])))
  202. :type "POST"
  203. :parser 'json-read
  204. :success (cl-function
  205. (lambda (&key data &allow-other-keys)
  206. (let ((new-flow-token
  207. (alist-get 'flow_token data)))
  208. (cond
  209. ((exitter-find-subtask data "LoginEnterPassword")
  210. (message "LoginEnterPassword")
  211. (exitter-enter-password new-flow-token headers))
  212. ((exitter-find-subtask data "LoginEnterAlternateIdentifierSubtask")
  213. (message "LoginEnterAlternateIdentifierSubtask")
  214. (exitter-enter-email new-flow-token headers))
  215. (t (message "Cannot find any matching subtasks"))))
  216. ))
  217. :error
  218. (cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
  219. (message "Got error: %S" error-thrown)))))
  220. (defun exitter-enter-password (flow-token headers)
  221. (when exitter-debug (message "entering exitter-enter-password"))
  222. (request exitter-url-task
  223. :params '(("lang" . "en"))
  224. :headers headers
  225. :data (json-encode
  226. `(("flow_token" . ,flow-token)
  227. ("subtask_inputs" .
  228. [(("enter_password" .
  229. (("password" . ,exitter-password)
  230. ("link" . "next_link")))
  231. ("subtask_id" . "LoginEnterPassword"))])))
  232. :type "POST"
  233. :parser 'json-read
  234. :success (cl-function
  235. (lambda (&key data &allow-other-keys)
  236. (cond
  237. ((exitter-find-subtask data "LoginSuccessSubtask")
  238. (message "LoginSuccessSubtask")
  239. (let* ((subtask
  240. (exitter-find-subtask data "LoginSuccessSubtask"))
  241. (open-account (alist-get 'open_account subtask)))
  242. (setq exitter-oauth-token
  243. (alist-get 'oauth_token open-account)
  244. exitter-oauth-token-secret
  245. (alist-get 'oauth_token_secret open-account)
  246. exitter-oauth-token-ctime
  247. (format-time-string "%Y-%m-%d %a %H:%M:%S"
  248. (current-time)))))
  249. (t (message "Cannot find any matching subtasks"))) ))
  250. :error
  251. (cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
  252. (message "Got error: %S" error-thrown)))
  253. ))
  254. (defun exitter-enter-email (flow-token headers)
  255. (when exitter-debug (message "entering exitter-enter-email"))
  256. (request exitter-url-task
  257. :params '(("lang" . "en"))
  258. :headers headers
  259. :data (json-encode
  260. `(("flow_token" . ,flow-token)
  261. ("subtask_inputs" .
  262. [(("enter_text" .
  263. (("text" . ,exitter-email)
  264. ("link" . "next_link")))
  265. ("subtask_id" . "LoginEnterAlternateIdentifierSubtask"))])))
  266. :type "POST"
  267. :parser 'json-read
  268. :success (cl-function
  269. (lambda (&key data &allow-other-keys)
  270. (let ((new-flow-token
  271. (alist-get 'flow_token data)))
  272. (cond
  273. ((exitter-find-subtask data "LoginEnterPassword")
  274. (message "LoginEnterPassword")
  275. (exitter-enter-password new-flow-token headers))
  276. (t (message "Cannot find any matching subtasks"))))
  277. ))
  278. :error
  279. (cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
  280. (message "Got error: %S" error-thrown)))
  281. ))
  282. (defun exitter-get-sign-oauth (link url-params method)
  283. (let* ((oauth-params `(("oauth_consumer_key" . ,exitter-oauth-consumer-key)
  284. ("oauth_nonce" . ,(exitter-nonce))
  285. ("oauth_signature_method" . "HMAC-SHA1")
  286. ("oauth_timestamp" . ,(format-time-string "%s" (current-time)))
  287. ("oauth_token" . ,exitter-oauth-token)
  288. ("oauth_version" . "1.0")))
  289. (all-params (sort (seq-concatenate 'list url-params oauth-params)
  290. (lambda (a b) (string< (car a) (car b)))))
  291. (method-up (upcase method))
  292. (hexed-link (url-hexify-string link))
  293. (param-to-sign-unencoded
  294. (mapconcat
  295. (lambda (pair)
  296. (format "%s=%s" (car pair) (url-hexify-string (cdr pair))))
  297. all-params
  298. "&"))
  299. (param-to-sign
  300. (replace-regexp-in-string
  301. "&" "%26"
  302. (replace-regexp-in-string
  303. "=" "%3d"
  304. (replace-regexp-in-string
  305. "%" "%25"
  306. (replace-regexp-in-string
  307. "\\+" "%20"
  308. param-to-sign-unencoded)))))
  309. (to-sign (format "%s&%s&%s" method-up hexed-link param-to-sign))
  310. (signature (url-hexify-string
  311. (base64-encode-string
  312. (hmac-sha1 (encode-coding-string
  313. (format "%s&%s"
  314. exitter-oauth-consumer-secret
  315. exitter-oauth-token-secret)
  316. 'utf-8)
  317. (encode-coding-string to-sign 'utf-8)))))
  318. )
  319. (message "PARAM-TO-SIGN-UNENC: %s" (prin1-to-string param-to-sign-unencoded))
  320. (message "PARAM-TO-SIGN: %s" (prin1-to-string param-to-sign))
  321. (message "TO-SIGN: %s" (prin1-to-string to-sign))
  322. (format "OAuth realm=\"http://api.twitter.com/\", oauth_signature=\"%s\", %s"
  323. signature
  324. (mapconcat
  325. (lambda (pair)
  326. (format "%s=\"%s\"" (car pair) (cdr pair)))
  327. oauth-params
  328. ", "))))
  329. (defun exitter-do-fetch (link params &optional headers)
  330. (when exitter-debug (message "entering exitter-do-fetch"))
  331. (let ((authorization (exitter-get-sign-oauth link params "GET")))
  332. (request link
  333. :headers `(,@headers
  334. ("Connection" . "Keep-Alive")
  335. ("Authorization" . ,authorization)
  336. ("Content-Type" . "application/json")
  337. ("X-Twitter-Active-User" . "yes")
  338. ("Authority" . "api.twitter.com")
  339. ("Accept-Encoding" . "gzip")
  340. ("Accept-Language" . "en-US,en;q=0.9")
  341. ("Accept" . "*/*")
  342. ("DNT" . "1")
  343. ("User-Agent" .
  344. "TwitterAndroid/10.10.0 (29950000-r-0) ONEPLUS+A3010/9 \
  345. (OnePlus;ONEPLUS+A3010;OnePlus;OnePlus3;0;;1;2016)")
  346. ("X-Twitter-API-Version" . "5")
  347. ("X-Twitter-Client" . "TwitterAndroid")
  348. ("X-Twitter-Client-Version" . "10.10.0")
  349. ("OS-Version" . "28")
  350. ("System-User-Agent" .
  351. "Dalvik/2.1.0 (Linux; U; Android 9; \
  352. ONEPLUS A3010 Build/PKQ1.181203.001)")
  353. )
  354. :params params
  355. :type "GET"
  356. :parser 'json-read
  357. :success (cl-function
  358. (lambda (&key data &allow-other-keys)
  359. (pp data)))
  360. :error
  361. (cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
  362. (message "Got error: %S" error-thrown)))
  363. )))
  364. (defun exitter-get-tweet (id)
  365. (let ((variables (json-encode
  366. `(
  367. ("focalTweetId" . ,id)
  368. ;; ("referrer" . "tweet")
  369. ;; ("with_rux_injections" . :json-false)
  370. ("includePromotedContent" . :json-false)
  371. ;; ("withCommunity" . t)
  372. ("withQuickPromoteEligibilityTweetFields" . :json-false)
  373. ("includeHasBirdwatchNotes" . :json-false)
  374. ("withBirdwatchNotes" . :json-false)
  375. ("withVoice" . :json-false)
  376. ("withV2Timeline" . t)
  377. ))))
  378. (message "VARIABLES: ")
  379. (exitter-do-fetch
  380. exitter-url-tweet-detail
  381. `(("variables" . ,variables)
  382. ("features" . ,exitter-default-features)))))
  383. (require 'bindat)
  384. (defun exitter-nonce ()
  385. (let ((xs))
  386. (dotimes (_ 32 xs)
  387. (setq xs (cons (random 256) xs)))
  388. (replace-regexp-in-string
  389. "[=/+]" ""
  390. (base64-encode-string
  391. (alist-get 'bs (bindat-unpack '((bs str 32)) (vconcat xs)))))))
  392. (require 'sha1)
  393. (defun hmac-sha1 (key message)
  394. "Return an HMAC-SHA1 authentication code for KEY and MESSAGE.
  395. KEY and MESSAGE must be unibyte strings. The result is a unibyte
  396. string. Use the function `encode-hex-string' or the function
  397. `base64-encode-string' to produce human-readable output.
  398. See URL:<http://en.wikipedia.org/wiki/HMAC> for more information
  399. on the HMAC-SHA1 algorithm.
  400. The Emacs multibyte representation actually uses a series of
  401. 8-bit values under the hood, so we could have allowed multibyte
  402. strings as arguments. However, internal 8-bit values don't
  403. correspond to any external representation \(at least for major
  404. version 22). This makes multibyte strings useless for generating
  405. hashes.
  406. Instead, callers must explicitly pick and use an encoding for
  407. their multibyte data. Most callers will want to use UTF-8
  408. encoding, which we can generate as follows:
  409. (let ((unibyte-key (encode-coding-string key 'utf-8 t))
  410. (unibyte-value (encode-coding-string value 'utf-8 t)))
  411. (hmac-sha1 unibyte-key unibyte-value))
  412. For keys and values that are already unibyte, the
  413. `encode-coding-string' calls just return the same string."
  414. (when (multibyte-string-p key)
  415. (error "key %s must be unibyte" key))
  416. (when (multibyte-string-p message)
  417. (error "message %s must be unibyte" message))
  418. ;; The key block is always exactly the block size of the hash
  419. ;; algorithm. If the key is too small, we pad it with zeroes (or
  420. ;; instead, we initialize the key block with zeroes and copy the
  421. ;; key onto the nulls). If the key is too large, we run it
  422. ;; through the hash algorithm and use the hashed value (strange
  423. ;; but true).
  424. (let ((+hmac-sha1-block-size-bytes+ 64)) ; SHA-1 uses 512-bit blocks
  425. (when (< +hmac-sha1-block-size-bytes+ (length key))
  426. (setq key (sha1 key nil nil t)))
  427. (let ((key-block (make-vector +hmac-sha1-block-size-bytes+ 0)))
  428. (dotimes (i (length key))
  429. (aset key-block i (aref key i)))
  430. (let ((opad (make-vector +hmac-sha1-block-size-bytes+ #x5c))
  431. (ipad (make-vector +hmac-sha1-block-size-bytes+ #x36)))
  432. (dotimes (i +hmac-sha1-block-size-bytes+)
  433. (aset ipad i (logxor (aref ipad i) (aref key-block i)))
  434. (aset opad i (logxor (aref opad i) (aref key-block i))))
  435. (when (fboundp 'unibyte-string)
  436. ;; `concat' of Emacs23 (and later?) generates a multi-byte
  437. ;; string from a vector of characters with eight bit.
  438. ;; Since `opad' and `ipad' must be unibyte, we have to
  439. ;; convert them by using `unibyte-string'.
  440. ;; We cannot use `string-as-unibyte' here because it encodes
  441. ;; bytes with the manner of UTF-8.
  442. (setq opad (apply 'unibyte-string (mapcar 'identity opad)))
  443. (setq ipad (apply 'unibyte-string (mapcar 'identity ipad))))
  444. (sha1 (concat opad
  445. (sha1 (concat ipad message)
  446. nil nil t))
  447. nil nil t)))))
  448. (provide 'exitter)