summaryrefslogtreecommitdiff
path: root/exitter.el
diff options
context:
space:
mode:
authorYuchen Pei <id@ypei.org>2024-11-05 11:14:05 +1100
committerYuchen Pei <id@ypei.org>2024-11-05 11:14:05 +1100
commite6e2fa517275faf5ada76ac40b44bd559d7a7aac (patch)
treefb024bd12b065173e3da39c6ac8f235d41979d13 /exitter.el
[wip] Initial implementation to go through the flow login process
Diffstat (limited to 'exitter.el')
-rw-r--r--exitter.el243
1 files changed, 243 insertions, 0 deletions
diff --git a/exitter.el b/exitter.el
new file mode 100644
index 0000000..4e3f293
--- /dev/null
+++ b/exitter.el
@@ -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)