|
|
@@ -0,0 +1,243 @@
|
|
|
+;; -*- 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)
|
|
|
+
|
|
|
+(setq exitter-url-endpoint "https://api.twitter.com/1.1"
|
|
|
+ exitter-url-activate
|
|
|
+ (format "%s/guest/activate.json" exitter-url-endpoint)
|
|
|
+ exitter-url-task
|
|
|
+ (format "%s/onboarding/task.json" exitter-url-endpoint)
|
|
|
+ exitter-url-token
|
|
|
+ (format "https://api.twitter.com/oauth2/token"))
|
|
|
+(setq exitter-agent-param "-A \"TwitterAndroid/10.10.0\"")
|
|
|
+(setq exitter-tor-param "-x socks5://127.0.0.1:9150/")
|
|
|
+(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)")
|
|
|
+ ("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-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)
|
|
|
+
|
|
|
+;;; for debugging
|
|
|
+;; (setq request-message-level 'blather)
|
|
|
+;;; disable
|
|
|
+;; (setq request-message-level -1)
|
|
|
+
|
|
|
+(defun exitter-get-access-token ()
|
|
|
+ (let ((request-curl-options `(,exitter-agent-param))
|
|
|
+ (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 ()
|
|
|
+ (message "entering exitter-get-guest-token")
|
|
|
+ (let ((request-curl-options `(,exitter-agent-param)))
|
|
|
+ (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-get-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-get-flow-token (guest-token)
|
|
|
+ (message "entering exitter-get-flow-token")
|
|
|
+ (let ((request-curl-options `(,exitter-agent-param)))
|
|
|
+ (request exitter-url-task
|
|
|
+ :param '(("flow_name" . "login"))
|
|
|
+ :headers `(("Authorization" . ,(format "Bearer %s" exitter-access-token))
|
|
|
+ ("Content-type" . "application/json")
|
|
|
+ ("X-Guest-Token" . ,guest-token))
|
|
|
+ :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)))
|
|
|
+ (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)
|
|
|
+ (exitter-enter-username guest-token
|
|
|
+ (alist-get 'flow_token data)
|
|
|
+ att)
|
|
|
+ )))
|
|
|
+ :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)
|
|
|
+ (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 (guest-token flow-token att)
|
|
|
+ (message "entering exitter-enter-username")
|
|
|
+ (let ((request-curl-options `(,exitter-agent-param)))
|
|
|
+ (request exitter-url-task
|
|
|
+ :params '(("lang" . "en"))
|
|
|
+ :headers `(("Authorization" . ,(format "Bearer %s" exitter-access-token))
|
|
|
+ ("Content-type" . "application/json")
|
|
|
+ ("X-Guest-Token" . ,guest-token)
|
|
|
+ ("att" . ,att)
|
|
|
+ ("cookie" . ,(format "att=%s" att)))
|
|
|
+ :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)
|
|
|
+ (cond
|
|
|
+ ((exitter-find-subtask data "LoginEnterPassword")
|
|
|
+ (message "LoginEnterPassword")
|
|
|
+ (exitter-enter-password guest-token flow-token att))
|
|
|
+ ((exitter-find-subtask data "LoginEnterAlternateIdentifierSubtask")
|
|
|
+ (message "LoginEnterAlternateIdentifierSubtask")
|
|
|
+ (exitter-enter-email guest-token flow-token att))
|
|
|
+ (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 (guest-token flow-token att)
|
|
|
+ (message "entering exitter-enter-password")
|
|
|
+ (let ((request-curl-options `(,exitter-agent-param)))
|
|
|
+ (request exitter-url-task
|
|
|
+ :params '(("lang" . "en"))
|
|
|
+ :headers `(("Authorization" . ,(format "Bearer %s" exitter-access-token))
|
|
|
+ ("Content-type" . "application/json")
|
|
|
+ ("X-Guest-Token" . ,guest-token)
|
|
|
+ ("att" . ,att)
|
|
|
+ ("cookie" . ,(format "att=%s" att)))
|
|
|
+ :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)
|
|
|
+ (print data)
|
|
|
+ ))
|
|
|
+ :error
|
|
|
+ (cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
|
|
|
+ (message "Got error: %S" error-thrown)))
|
|
|
+ )))
|
|
|
+
|
|
|
+(defun exitter-enter-email (guest-token flow-token att)
|
|
|
+ (message "entering exitter-enter-email")
|
|
|
+ (let ((request-curl-options `(,exitter-agent-param)))
|
|
|
+ (request exitter-url-task
|
|
|
+ :params '(("lang" . "en"))
|
|
|
+ :headers `(("Authorization" . ,(format "Bearer %s" exitter-access-token))
|
|
|
+ ("Content-type" . "application/json")
|
|
|
+ ("X-Guest-Token" . ,guest-token)
|
|
|
+ ("att" . ,att)
|
|
|
+ ("cookie" . ,(format "att=%s" att)))
|
|
|
+ :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)
|
|
|
+ (print data)
|
|
|
+ ))
|
|
|
+ :error
|
|
|
+ (cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
|
|
|
+ (message "Got error: %S" error-thrown)))
|
|
|
+ )))
|
|
|
+
|
|
|
+(provide 'exitter)
|