├── .env.example ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── certs ├── testCA.key └── testCA.pem ├── img ├── captcha.png ├── default-pfp.jpg ├── default-pfpold-2.jpg ├── nullptr.png ├── twitter-banner.jpeg ├── twitter-pfp.jpg └── u6qfBBkF_bigger.jpg ├── package-lock.json ├── package.json ├── src ├── index.ts ├── middleware │ └── auth.ts ├── models │ ├── Conversation.ts │ ├── Message.ts │ ├── Notification.ts │ ├── Tweet.ts │ └── User.ts ├── routes │ ├── 2 │ │ ├── notifications │ │ │ └── notifications.ts │ │ └── trends │ │ │ └── guide.ts │ ├── 1.1-prefixed │ │ ├── dms │ │ │ └── dms.ts │ │ ├── search │ │ │ └── search.ts │ │ └── stubs │ │ │ ├── hashflags.ts │ │ │ ├── settings.ts │ │ │ ├── tracking.ts │ │ │ └── users.ts │ ├── 1.1 │ │ ├── account │ │ │ └── account.ts │ │ ├── jot │ │ │ └── generic.ts │ │ ├── onboarding │ │ │ ├── generic.ts │ │ │ └── task.ts │ │ └── stubs │ │ │ └── settings.ts │ ├── gql │ │ ├── blue.ts │ │ ├── dm.ts │ │ ├── graphQlHandler.ts │ │ ├── tweet.ts │ │ ├── user.ts │ │ └── viewer.ts │ ├── other │ │ ├── arkose │ │ │ └── index.ts │ │ ├── email │ │ │ └── validateInput.ts │ │ └── image │ │ │ └── image.ts │ ├── state │ │ └── state.ts │ └── twimg │ │ └── profile.ts ├── static │ └── twitter.html ├── types │ ├── env.d.ts │ ├── graphql.ts │ ├── guide.ts │ ├── guideClass.ts │ ├── req.d.ts │ ├── route.ts │ └── task.ts └── util │ ├── case.ts │ ├── dmUtil.ts │ ├── formatDate.ts │ ├── logging.ts │ ├── notifications.ts │ ├── randUtil.ts │ └── routeUtil.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | PORT=443 2 | JWT_SECRET=supersecuresecretyoucantguess -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "useTabs": true 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true, 8 | "**/Thumbs.db": true, 9 | "**/node_modules": true, 10 | "**/.vscode": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blue OSS 2 | A (very poorly) reverse-engineered Twitter backend clone. 3 | 4 | ## A FAIR WARNING! 5 | **DO NOT use this right now!!** A lot of things have yet to be done, there are many stubs and (client-side) errors. 6 | 7 | ## How do I set this up for testing/contribution? 8 | 1. Create your .env, set port to 443 unless you know what you're doing. 9 | 2. Install MongoDB, start on 27017. 10 | 3. `npm i` 11 | 4. `sudo nodemon src/index.ts` (there is no production setup as of now because you shouldn't run this in production environments until it is more fleshed out.) 12 | 5. Go to Twitter and logout of your account. 13 | 6. Install the CA in `certs`, then set your proxy either in-browser or system-wide to `127.0.0.1:8000` (or whatever the proxy is bound to, see console) 14 | 7. Voila! 15 | 16 | ## Tips 17 | If you don't want to run all of your system traffic with a self-signed CA through some rando's GitHub project (which is fair), the following wildcards are all the traffic which the project requires: 18 | - `*twitter.com*` (for main API calls, and state injection in the future) 19 | - `*twimg.com*` (for profile pictures and banners, although current implementation is yucky) 20 | - `*arkoselabs.com*` (for my super-special captcha bypass which utilizes XSS in Arkose's captcha to automatically do it) 21 | You can use a browser extension (I personally use FoxyProxy in HTTP proxy mode) for this proxy if you want to use these wildcards. 22 | -------------------------------------------------------------------------------- /certs/testCA.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC78Qz5ANhnu5aX 3 | Jm1KMJObWdhNK2lQXzGKEQj/lqqrRhVE5BcjtcTkPAxZGK1DBdruPzDp+yvwvYrO 4 | TqE2T019IIbNUGZihDShrldrsDA31gfH5AAgqMFw8BozHi4S6xYuJsM9BV4H2Xzt 5 | ffqX6p8VaQl0fXHaeymdoayu/wwr6hmYzaUHlyIXAfR9y8eM/dVVgWEWZZOcLvyi 6 | YenjcDVyMHiZygulAlDC9K3jSWAMIDSs1Dh47cgHrtjeCXP9Uo9CEMnWVZDyLyDY 7 | zV8f8b6ZwXS0XKjy5Rw3RwI5hwUN0z4uvvBukWEx542M7UlXmRoaFNZ3Wy315C7y 8 | TrRM2TZ7AgMBAAECggEAQxiSng7sM1RoNbwVRmhDOjIAI8S8y2HAyN6+DKlYct8J 9 | VtfrXudUzHqXmfNSX9wmReovZ0kxbbftZyvxJ/d13ZcQKRpsAv3Z4GLieOKkxDpX 10 | +sfuN7qnnM9LOzaqEjz1ZTKXfyB8amb9z7erEDNF6hnes0TZsbtIpysCAFOzn7VN 11 | +zdJ3WJ1UUF0NrqhUnFagAIW2oqqR+lYh2FrZdioLYP2f8NSR/ud/8vylUfaOwCD 12 | 6XbdSbzfnL4YW2YiFBsyN/Ze6OKugnusTbyCGo146nOJsyQb2/BXbbbfwWG/xot5 13 | TpEtpnPYhqbdJbaqlbW8566+3SSz2E+Zr61CzUTWUQKBgQDR66ilVmv4wmZRvQc0 14 | Sl2l8LVNNnDb75eSionvL7DCakqTfjv1munjgv9umHGE3+oVBy5yJAfHAHPLNmMk 15 | NC6nR9r2WjIihigC6tZQPcCzuxYaP8Yrvo5kK6C/G0GRG+5nYjZ1A6DDg/VzQ9GI 16 | MrsYhLK1foLNuOu9q8egmYcAiQKBgQDlMkzvRbgD/XMrLnV3k8htyiGvnwitRfxH 17 | rId+2f+95CZ+bIkbcPVQwOIVmjsAypq7eihBdiXrvXoK+s+qY+fjWOzNrt416vsa 18 | +1Z1n9BNiaMtpzdzWHczUAq7x3SQy2usdy79FFT456W194ezZbQVENzgV2Vm6ZGl 19 | fFPz4zSV4wKBgFyw2/S1plNfM2LgPrAeLh+5m7SOV2Ml9h2kwqc0va361RcvZDZc 20 | wu3MyBEDiwyK+odydlW8Bxsd3gNa7ofr6rW+irbAuOZ5qm0vWJZxzgcOkwHgKxbh 21 | u2oLwZzg4iC63pgTy8v7YB80w0q0JW8oR2jaHzb4t7Uy9BT6JtmJMQ1RAoGBAL26 22 | HNu5P/H1nZ+yTsUUzcasm6QQpxMgqwz98g/9D+o2cfMXj4vqvvgBI2Y0jQFDtkDZ 23 | h3dhAVUbPWrXYo6vMycM1sIRLps9kG4ufszR5ZI6DJ1fHdTa95m/eZMmMgUmj1Ru 24 | OLSBXzHIOHHgnTHRT/hcRCtlzWgak1mFCM4MV9x3AoGBAMx1I5OHkyBzz89i6mSr 25 | S4XqLY9B435be0nQnDd5UyfP++B3g1zwuh1uGtA6c5CA/HFxhzMekL6tZDkcsdx2 26 | RCXISK8w9BsWmcB7qCgoG71BB6YQMwshaoc0hcsh2+dixF1ObSilqpOWiQqrQKwu 27 | v1AdYW20p/gXGUhktF90575z 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /certs/testCA.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDOTCCAiGgAwIBAgIUTZPStty6xcjtst68q3wX1dc50YowDQYJKoZIhvcNAQEL 3 | BQAwLDEqMCgGA1UEAwwhTW9ja3R0cCBUZXN0aW5nIENBIC0gRE8gTk9UIFRSVVNU 4 | MB4XDTIzMDQzMDExMzMxMVoXDTI0MDQyOTExMzMxMVowLDEqMCgGA1UEAwwhTW9j 5 | a3R0cCBUZXN0aW5nIENBIC0gRE8gTk9UIFRSVVNUMIIBIjANBgkqhkiG9w0BAQEF 6 | AAOCAQ8AMIIBCgKCAQEAu/EM+QDYZ7uWlyZtSjCTm1nYTStpUF8xihEI/5aqq0YV 7 | ROQXI7XE5DwMWRitQwXa7j8w6fsr8L2Kzk6hNk9NfSCGzVBmYoQ0oa5Xa7AwN9YH 8 | x+QAIKjBcPAaMx4uEusWLibDPQVeB9l87X36l+qfFWkJdH1x2nspnaGsrv8MK+oZ 9 | mM2lB5ciFwH0fcvHjP3VVYFhFmWTnC78omHp43A1cjB4mcoLpQJQwvSt40lgDCA0 10 | rNQ4eO3IB67Y3glz/VKPQhDJ1lWQ8i8g2M1fH/G+mcF0tFyo8uUcN0cCOYcFDdM+ 11 | Lr7wbpFhMeeNjO1JV5kaGhTWd1st9eQu8k60TNk2ewIDAQABo1MwUTAdBgNVHQ4E 12 | FgQUoJ539qkbtZ7Ziwngt53f4g94ZqMwHwYDVR0jBBgwFoAUoJ539qkbtZ7Ziwng 13 | t53f4g94ZqMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAo7k1 14 | ZfYEqGEIx9ia6dDLA/4PLzP3Oe1hEq13yrNSzx6dt2oewLE1aFif7Xm4UTkFINuM 15 | 8oPoGM7VVfDIqYvUsQut/k7VsN48g8lF6Cai5G41PdTuHYzNxNG1AX9sh4exaaFe 16 | /QKYxRgT48KshaCIMiZit7rVLnk/B1saawfio/PeYef0YfHPREVFy68exRI46LnZ 17 | Ki+W5A5pJLtIwAF6HpFnDghXs437dhs1X3snnNz1tN9RLF9fYZPeao3HmoKarBmB 18 | gdjuRpKMgCq4ACJSrahS2p6VxJ5vGTHOE8i0U62N4nVse9F2yAcUDC7kGGTS2Kfq 19 | B2VNl9vSbLSOUUZYXw== 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /img/captcha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-nullptr/blue/3b2284a85766a6fa3059f9e4321a815dba3dc117/img/captcha.png -------------------------------------------------------------------------------- /img/default-pfp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-nullptr/blue/3b2284a85766a6fa3059f9e4321a815dba3dc117/img/default-pfp.jpg -------------------------------------------------------------------------------- /img/default-pfpold-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-nullptr/blue/3b2284a85766a6fa3059f9e4321a815dba3dc117/img/default-pfpold-2.jpg -------------------------------------------------------------------------------- /img/nullptr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-nullptr/blue/3b2284a85766a6fa3059f9e4321a815dba3dc117/img/nullptr.png -------------------------------------------------------------------------------- /img/twitter-banner.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-nullptr/blue/3b2284a85766a6fa3059f9e4321a815dba3dc117/img/twitter-banner.jpeg -------------------------------------------------------------------------------- /img/twitter-pfp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-nullptr/blue/3b2284a85766a6fa3059f9e4321a815dba3dc117/img/twitter-pfp.jpg -------------------------------------------------------------------------------- /img/u6qfBBkF_bigger.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/not-nullptr/blue/3b2284a85766a6fa3059f9e4321a815dba3dc117/img/u6qfBBkF_bigger.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blue", 3 | "version": "0.0.0", 4 | "description": "A (very poorly) reverse-engineered Twitter backend clone.", 5 | "main": "src/index.ts", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "madzzz", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@typegoose/typegoose": "^11.2.0", 13 | "@types/bcrypt": "^5.0.0", 14 | "@types/cookie-parser": "^1.4.3", 15 | "@types/cors": "^2.8.13", 16 | "@types/express": "^4.17.17", 17 | "@types/jsonwebtoken": "^9.0.2", 18 | "@types/node": "^18.16.2", 19 | "axios": "^1.4.0", 20 | "bcrypt": "^5.1.0", 21 | "chalk": "^4.1.2", 22 | "cookie-parser": "^1.4.6", 23 | "cors": "^2.8.5", 24 | "dotenv": "^16.0.3", 25 | "eslint": "^8.39.0", 26 | "express": "^4.18.2", 27 | "get-root-path": "^2.0.2", 28 | "http-proxy": "^1.18.1", 29 | "jsdom": "^22.1.0", 30 | "jsonwebtoken": "^9.0.0", 31 | "mockttp": "^3.7.3", 32 | "mongoose": "^7.1.0", 33 | "parse-headers": "^2.0.5", 34 | "prettier": "^2.8.8", 35 | "sanitize-filename": "^1.6.3", 36 | "semver": "^7.5.0", 37 | "undici": "^5.22.1" 38 | }, 39 | "devDependencies": { 40 | "@types/http-proxy": "^1.17.11", 41 | "@types/jsdom": "^21.1.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | import dotenv from "dotenv"; 3 | import { searchDirForTsFiles } from "./util/routeUtil"; 4 | import * as fs from "fs"; 5 | import { IRouteFile } from "./types/route"; 6 | import http from "http"; 7 | import https from "https"; 8 | import cors from "cors"; 9 | import mongoose from "mongoose"; 10 | import * as mockttp from "mockttp"; 11 | import cookieParser from "cookie-parser"; 12 | import type { ErrorRequestHandler } from "express"; 13 | import { log } from "./util/logging"; 14 | import { CallbackResponseMessageResult } from "mockttp/dist/rules/requests/request-handler-definitions"; 15 | 16 | dotenv.config(); 17 | 18 | const app = express(); 19 | 20 | mongoose.connect("mongodb://127.0.0.1:27017/blue"); 21 | 22 | app.use(express.json()); 23 | app.use(cookieParser()); 24 | app.use( 25 | cors({ 26 | credentials: true, 27 | origin: function (origin, callback) { 28 | if (!origin) return callback(null, true); 29 | return callback(null, true); 30 | }, 31 | }) 32 | ); 33 | 34 | async function mountRoute(routePath: string, root: string) { 35 | const router = ((await import(routePath.replace("src/", "./"))) as IRouteFile) 36 | .default; 37 | app.use(root, router); 38 | } 39 | 40 | app.use((req, res, next) => { 41 | if (req.path.includes("error_log.json")) next(); 42 | // log( 43 | // `${req.path} | ${req.cookies.jwt ? `Logged in?` : `Logged out?`}` 44 | // ); 45 | next(); 46 | }); 47 | 48 | const routes11 = searchDirForTsFiles("src/routes/1.1"); 49 | const routes11Prefixed = searchDirForTsFiles("src/routes/1.1-prefixed"); 50 | const routes2 = searchDirForTsFiles("src/routes/2"); 51 | const routesOther = searchDirForTsFiles("src/routes/other"); 52 | const twimg = searchDirForTsFiles("src/routes/twimg"); 53 | 54 | routes11.forEach(async (routePath) => { 55 | await mountRoute(routePath, "/1.1"); 56 | }); 57 | 58 | routes11Prefixed.forEach(async (routePath) => { 59 | await mountRoute(routePath, "/i/api/1.1"); 60 | }); 61 | 62 | routes2.forEach(async (routePath) => { 63 | await mountRoute(routePath, "/i/api/2"); 64 | }); 65 | 66 | mountRoute("src/routes/gql/graphQlHandler.ts", "/i/api/graphql"); 67 | 68 | routesOther.forEach(async (routePath) => { 69 | await mountRoute(routePath, "/"); 70 | }); 71 | 72 | twimg.forEach(async (routePath) => { 73 | await mountRoute(routePath, "/"); 74 | }); 75 | 76 | const errorHandler: ErrorRequestHandler = (err, req, res, next) => { 77 | log(err.stack); 78 | return res.status(500).send({ 79 | msg: 80 | "An error occured within the server. Open an issue with the information below on the GitHub, including what you did leading up to this error.\n" + 81 | err.stack, 82 | }); 83 | }; 84 | 85 | app.use(errorHandler); 86 | 87 | const options = { 88 | key: fs.readFileSync("certs/testCA.key", "utf8"), 89 | cert: fs.readFileSync("certs/testCA.pem", "utf8"), 90 | }; 91 | 92 | const httpsServer = https.createServer(options, app); 93 | 94 | httpsServer.listen(process.env.PORT, () => { 95 | log(`Blue is listening on port ${process.env.PORT}.`); 96 | }); 97 | 98 | (async () => { 99 | app.use("/", ((await import("./routes/state/state")) as IRouteFile).default); 100 | const server = mockttp.getLocal({ 101 | https: { 102 | keyPath: "certs/testCA.key", 103 | certPath: "certs/testCA.pem", 104 | }, 105 | }); 106 | server 107 | .forAnyRequest() 108 | .forHostname("twitter.com") 109 | .matching((req) => { 110 | return req.path !== "/" && !req.headers["Sec-Fetch-Dest"]; 111 | }) 112 | .thenForwardTo("https://localhost", { 113 | ignoreHostHttpsErrors: true, 114 | }); 115 | server 116 | .forAnyRequest() 117 | .forHostname("twitter.com") 118 | .matching((req) => { 119 | const condition = req.path.startsWith("/i/api"); 120 | return condition; 121 | }) 122 | .thenForwardTo("https://localhost", { ignoreHostHttpsErrors: true }); 123 | server 124 | .forAnyRequest() 125 | .forHostname("api.twitter.com") 126 | .matching((req) => { 127 | const condition = 128 | req.path.startsWith("/1.1") || 129 | req.path.startsWith("/img") || 130 | req.path.startsWith("/graphql"); 131 | return condition; 132 | }) 133 | .thenForwardTo("https://localhost", { ignoreHostHttpsErrors: true }); 134 | server 135 | .forAnyRequest() 136 | .forHostname("abs.twimg.com") 137 | .matching((req) => { 138 | return req.path.startsWith("/images/themes"); 139 | }) 140 | .thenForwardTo("https://localhost", { ignoreHostHttpsErrors: true }); 141 | server 142 | .forAnyRequest() 143 | .forHostname("pbs.twimg.com") 144 | .matching((req) => { 145 | return true; 146 | }) 147 | .thenForwardTo("https://localhost", { ignoreHostHttpsErrors: true }); 148 | server 149 | .forAnyRequest() 150 | .forHostname("client-api.arkoselabs.com") 151 | .matching((req) => { 152 | const condition = 153 | (req.path.startsWith("/fc") || req.path.startsWith("/rtig")) && 154 | !req.path.includes("api") && 155 | !req.path.includes("assets") && 156 | // !req.path.includes("gt2") && 157 | !req.path.includes("cdn"); 158 | return condition; 159 | }) 160 | .thenForwardTo("https://localhost", { ignoreHostHttpsErrors: true }); 161 | server.forUnmatchedRequest().thenPassThrough(); 162 | await server.start(); 163 | log(`Blue proxy is listening on port ${server.port}.`); 164 | })(); 165 | -------------------------------------------------------------------------------- /src/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { verify } from "jsonwebtoken"; 3 | import User from "../models/User"; 4 | import { IJwtDecoded } from "../types/graphql"; 5 | 6 | export async function requireAuth(req: Request, res: Response): Promise { 7 | if (!req.cookies["jwt"]) 8 | return res 9 | .status(401) 10 | .send({ msg: "No auth token (are you missing your jwt cookie?)" }); 11 | try { 12 | const jwtParams = verify( 13 | req.cookies["jwt"] as string, 14 | process.env.JWT_SECRET! 15 | ) as IJwtDecoded; 16 | const user = await User.findOne({ id: jwtParams.id }); 17 | if (!user) 18 | return res 19 | .status(401) 20 | .send({ msg: "Auth token invalid (does your account exist?)" }); 21 | } catch (e) { 22 | return res.status(401).send({ msg: "Failed to verify your JWT." }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/models/Conversation.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import Message from "./Message"; 3 | 4 | const conversation = new mongoose.Schema({ 5 | id: String, 6 | message_ids: [String], 7 | conversation_id: String, 8 | type: String, 9 | sort_event_id: String, 10 | sort_timestamp: String, 11 | participants: [ 12 | { 13 | user_id: String, 14 | last_read_event_id: String, 15 | }, 16 | ], 17 | nsfw: Boolean, 18 | notifications_disabled: Boolean, 19 | mention_notifications_disabled: Boolean, 20 | last_read_event_id: String, 21 | read_only: Boolean, 22 | trusted: Boolean, 23 | muted: Boolean, 24 | status: String, 25 | min_entry_id: String, 26 | max_entry_id: String, 27 | }); 28 | 29 | const Conversation = mongoose.model("Conversation", conversation); 30 | 31 | export default Conversation; 32 | -------------------------------------------------------------------------------- /src/models/Message.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const message = new mongoose.Schema({ 4 | id: String, 5 | time: String, 6 | affects_sort: Boolean, 7 | request_id: String, 8 | conversation_id: String, 9 | message_data: { 10 | id: String, 11 | time: String, 12 | recipient_id: String, 13 | sender_id: String, 14 | text: String, 15 | }, 16 | }); 17 | 18 | const Message = mongoose.model("Message", message); 19 | 20 | export default Message; 21 | -------------------------------------------------------------------------------- /src/models/Notification.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const notification = new mongoose.Schema({ 4 | id: String, 5 | timestampMs: String, 6 | icon: { 7 | id: String, 8 | }, 9 | message: { 10 | text: String, 11 | // entities: [ 12 | // { 13 | // fromIndex: 0, 14 | // toIndex: 6, 15 | // ref: { 16 | // user: { 17 | // id: "1483042300943122432", 18 | // }, 19 | // }, 20 | // }, 21 | // ], 22 | entities: [], 23 | rtl: Boolean, 24 | }, 25 | template: { 26 | aggregateUserActionsV1: { 27 | targetObjects: [ 28 | { 29 | tweet: { 30 | id: String, 31 | }, 32 | }, 33 | ], 34 | fromUsers: [ 35 | { 36 | user: { 37 | id: String, 38 | }, 39 | }, 40 | ], 41 | }, 42 | }, 43 | }); 44 | 45 | const Notification = mongoose.model("Notification", notification); 46 | 47 | export default Notification; 48 | -------------------------------------------------------------------------------- /src/models/Tweet.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const tweet = new mongoose.Schema({ 4 | bookmark_count: { 5 | type: "Number", 6 | }, 7 | bookmarked: { 8 | type: "Boolean", 9 | }, 10 | conversation_id_str: { 11 | type: "String", 12 | }, 13 | created_at: { 14 | type: "String", 15 | }, 16 | display_text_range: { 17 | type: ["Number"], 18 | }, 19 | entities: { 20 | hashtags: { 21 | type: "Array", 22 | }, 23 | symbols: { 24 | type: "Array", 25 | }, 26 | urls: { 27 | type: "Array", 28 | }, 29 | user_mentions: { 30 | type: "Array", 31 | }, 32 | }, 33 | favorite_count: { 34 | type: "Number", 35 | }, 36 | favorited: { 37 | type: "Boolean", 38 | }, 39 | full_text: { 40 | type: "String", 41 | }, 42 | id_str: { 43 | type: "String", 44 | }, 45 | is_quote_status: { 46 | type: "Boolean", 47 | }, 48 | lang: { 49 | type: "String", 50 | }, 51 | quote_count: { 52 | type: "Number", 53 | }, 54 | reply_count: { 55 | type: "Number", 56 | }, 57 | retweet_count: { 58 | type: "Number", 59 | }, 60 | retweeted: { 61 | type: "Boolean", 62 | }, 63 | user_id_str: { 64 | type: "String", 65 | }, 66 | id: { 67 | type: "Number", 68 | }, 69 | truncated: { 70 | type: "Boolean", 71 | }, 72 | source: { 73 | type: "String", 74 | }, 75 | in_reply_to_status_id: { 76 | type: "Mixed", 77 | }, 78 | in_reply_to_status_id_str: { 79 | type: "Mixed", 80 | }, 81 | in_reply_to_user_id: { 82 | type: "Mixed", 83 | }, 84 | in_reply_to_user_id_str: { 85 | type: "Mixed", 86 | }, 87 | in_reply_to_screen_name: { 88 | type: "Mixed", 89 | }, 90 | user_id: { 91 | type: "Number", 92 | }, 93 | geo: { 94 | type: "Mixed", 95 | }, 96 | coordinates: { 97 | type: "Mixed", 98 | }, 99 | place: { 100 | type: "Mixed", 101 | }, 102 | contributors: { 103 | type: "Mixed", 104 | }, 105 | conversation_id: { 106 | type: "Number", 107 | }, 108 | conversation_control: { 109 | policy: { 110 | type: "String", 111 | }, 112 | conversation_owner: { 113 | screen_name: { 114 | type: "String", 115 | }, 116 | }, 117 | }, 118 | supplemental_language: { 119 | type: "Mixed", 120 | }, 121 | self_thread: { 122 | id: { 123 | type: "Number", 124 | }, 125 | id_str: { 126 | type: "String", 127 | }, 128 | }, 129 | ext_views: { 130 | state: { 131 | type: "String", 132 | }, 133 | count: { 134 | type: "String", 135 | }, 136 | }, 137 | ext: { 138 | editControl: { 139 | r: { 140 | ok: { 141 | initial: { 142 | editTweetIds: { 143 | type: ["String"], 144 | }, 145 | editableUntilMsecs: { 146 | type: "String", 147 | }, 148 | editsRemaining: { 149 | type: "Date", 150 | }, 151 | isEditEligible: { 152 | type: "Boolean", 153 | }, 154 | }, 155 | }, 156 | }, 157 | ttl: { 158 | type: "Number", 159 | }, 160 | }, 161 | unmentionInfo: { 162 | r: { 163 | ok: {}, 164 | }, 165 | ttl: { 166 | type: "Number", 167 | }, 168 | }, 169 | }, 170 | }); 171 | 172 | const Tweet = mongoose.model("Tweet", tweet); 173 | 174 | export default Tweet; 175 | -------------------------------------------------------------------------------- /src/models/User.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const user = new mongoose.Schema({ 4 | name: { 5 | type: "String", 6 | }, 7 | id: { 8 | type: "Number", 9 | }, 10 | id_string: { 11 | type: "String", 12 | }, 13 | email: { 14 | type: "String", 15 | select: false, 16 | }, 17 | password: { 18 | type: "String", 19 | select: false, 20 | }, 21 | screen_name: { 22 | type: "String", 23 | }, 24 | location: { 25 | type: "String", 26 | }, 27 | description: { 28 | type: "String", 29 | }, 30 | url: { 31 | type: "String", 32 | }, 33 | verified_type: { 34 | type: "String", 35 | }, 36 | entities: { 37 | url: { 38 | urls: { 39 | type: ["Mixed"], 40 | }, 41 | }, 42 | description: { 43 | urls: { 44 | type: "Array", 45 | }, 46 | }, 47 | }, 48 | protected: { 49 | type: "Boolean", 50 | }, 51 | followers_count: { 52 | type: "Number", 53 | }, 54 | fast_followers_count: { 55 | type: "Number", 56 | }, 57 | normal_followers_count: { 58 | type: "Number", 59 | }, 60 | friends_count: { 61 | type: "Number", 62 | }, 63 | listed_count: { 64 | type: "Number", 65 | }, 66 | created_at: { 67 | type: "String", 68 | }, 69 | favourites_count: { 70 | type: "Number", 71 | }, 72 | utc_offset: { 73 | type: "Mixed", 74 | }, 75 | time_zone: { 76 | type: "Mixed", 77 | }, 78 | geo_enabled: { 79 | type: "Boolean", 80 | }, 81 | verified: { 82 | type: "Boolean", 83 | }, 84 | statuses_count: { 85 | type: "Number", 86 | }, 87 | media_count: { 88 | type: "Number", 89 | }, 90 | lang: { 91 | type: "Mixed", 92 | }, 93 | contributors_enabled: { 94 | type: "Boolean", 95 | }, 96 | is_translator: { 97 | type: "Boolean", 98 | }, 99 | is_translation_enabled: { 100 | type: "Boolean", 101 | }, 102 | profile_background_color: { 103 | type: "String", 104 | }, 105 | profile_background_image_url: { 106 | type: "String", 107 | }, 108 | profile_background_image_url_https: { 109 | type: "String", 110 | }, 111 | profile_background_tile: { 112 | type: "Boolean", 113 | }, 114 | profile_image_url: { 115 | type: "String", 116 | }, 117 | profile_image_url_https: { 118 | type: "String", 119 | }, 120 | profile_banner_url: { 121 | type: "String", 122 | }, 123 | profile_link_color: { 124 | type: "String", 125 | }, 126 | profile_sidebar_border_color: { 127 | type: "String", 128 | }, 129 | profile_sidebar_fill_color: { 130 | type: "String", 131 | }, 132 | profile_text_color: { 133 | type: "String", 134 | }, 135 | profile_use_background_image: { 136 | type: "Boolean", 137 | }, 138 | has_extended_profile: { 139 | type: "Boolean", 140 | }, 141 | default_profile: { 142 | type: "Boolean", 143 | }, 144 | default_profile_image: { 145 | type: "Boolean", 146 | }, 147 | pinned_tweet_ids: { 148 | type: ["Number"], 149 | }, 150 | pinned_tweet_ids_str: { 151 | type: ["String"], 152 | }, 153 | liked_tweet_ids: { 154 | type: ["String"], 155 | }, 156 | posted_tweet_ids: { 157 | type: ["String"], 158 | }, 159 | has_custom_timelines: { 160 | type: "Boolean", 161 | }, 162 | can_dm: { 163 | type: "Mixed", 164 | }, 165 | following: { 166 | type: "Mixed", 167 | }, 168 | follow_request_sent: { 169 | type: "Mixed", 170 | }, 171 | muting: { 172 | type: "Mixed", 173 | }, 174 | blocking: { 175 | type: "Mixed", 176 | }, 177 | blocked_by: { 178 | type: "Mixed", 179 | }, 180 | want_retweets: { 181 | type: "Mixed", 182 | }, 183 | advertiser_account_type: { 184 | type: "String", 185 | }, 186 | advertiser_account_service_levels: { 187 | type: "Array", 188 | }, 189 | business_profile_state: { 190 | type: "String", 191 | }, 192 | translator_type: { 193 | type: "String", 194 | }, 195 | withheld_in_countries: { 196 | type: "Array", 197 | }, 198 | followed_by: { 199 | type: "Mixed", 200 | }, 201 | ext_is_blue_verified: { 202 | type: "Boolean", 203 | }, 204 | ext_profile_image_shape: { 205 | type: "String", 206 | }, 207 | ext_has_nft_avatar: { 208 | type: "Boolean", 209 | }, 210 | ext: { 211 | highlightedLabel: { 212 | r: { 213 | ok: {}, 214 | }, 215 | ttl: { 216 | type: "Number", 217 | }, 218 | }, 219 | hasNftAvatar: { 220 | r: { 221 | ok: { 222 | type: "Boolean", 223 | }, 224 | }, 225 | ttl: { 226 | type: "Number", 227 | }, 228 | }, 229 | }, 230 | require_some_consent: { 231 | type: "Boolean", 232 | }, 233 | // custom vars because i don't like 234 | // how twitter handles them, or be- 235 | // -cause i can't figure it out lol 236 | conversations: [String], 237 | notification_ids: [String], 238 | notification_sort_index: String, 239 | }); 240 | 241 | const User = mongoose.model("User", user); 242 | 243 | export default User; 244 | -------------------------------------------------------------------------------- /src/routes/1.1-prefixed/dms/dms.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | const router = express.Router(); 4 | 5 | router.get("/dm/inbox_initial_state.json", async (req, res) => { 6 | return res.status(200).send({ 7 | inbox_initial_state: { 8 | last_seen_event_id: "1666210735926435866", 9 | trusted_last_seen_event_id: "1666210735926435866", 10 | untrusted_last_seen_event_id: "0", 11 | entries: [ 12 | { 13 | message: { 14 | id: "1662005580842582021", 15 | time: "1685087983740", 16 | affects_sort: true, 17 | request_id: "1685087983522", 18 | conversation_id: "1160760001939550208-1483042300943122432", 19 | message_data: { 20 | id: "1662005580842582021", 21 | time: "1685087983000", 22 | recipient_id: "1483042300943122432", 23 | sender_id: "1160760001939550208", 24 | text: "hello", 25 | }, 26 | }, 27 | }, 28 | ], 29 | users: { 30 | "3701867534401305": { 31 | id: 3701867534401305, 32 | id_str: "3701867534401305", 33 | name: "Twitter", 34 | screen_name: "Twitter", 35 | profile_image_url: "", 36 | profile_image_url_https: "", 37 | following: true, 38 | follow_request_sent: false, 39 | description: null, 40 | entities: { description: { urls: [] } }, 41 | verified: false, 42 | is_blue_verified: false, 43 | protected: false, 44 | blocking: false, 45 | can_media_tag: true, 46 | created_at: "Mon Aug 12 03:48:45 +0000 2019", 47 | friends_count: 329, 48 | followers_count: 31, 49 | ext_has_nft_avatar: false, 50 | }, 51 | }, 52 | conversations: { 53 | "1338626424870039552-1483042300943122432": { 54 | conversation_id: "1338626424870039552-1483042300943122432", 55 | type: "ONE_TO_ONE", 56 | sort_event_id: "1639434080478699524", 57 | sort_timestamp: "1679706518655", 58 | participants: [ 59 | { 60 | user_id: "3701867534401305", 61 | last_read_event_id: "1639434080478699524", 62 | }, 63 | { 64 | user_id: "484648334416", 65 | last_read_event_id: "1639434080478699524", 66 | }, 67 | ], 68 | nsfw: false, 69 | notifications_disabled: false, 70 | mention_notifications_disabled: false, 71 | last_read_event_id: "1639434080478699524", 72 | read_only: false, 73 | trusted: true, 74 | muted: false, 75 | status: "AT_END", 76 | min_entry_id: "1639433688151732230", 77 | max_entry_id: "1639434080478699524", 78 | }, 79 | }, 80 | }, 81 | }); 82 | }); 83 | 84 | export default router; 85 | -------------------------------------------------------------------------------- /src/routes/1.1-prefixed/search/search.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import User from "../../../models/User"; 3 | 4 | interface ITypeAheadParams { 5 | q: string; 6 | } 7 | 8 | const router = express.Router(); 9 | 10 | router.get("/search/typeahead.json", async (req, res) => { 11 | const params = req.query as unknown as ITypeAheadParams; 12 | const query = new RegExp(".*" + params.q + ".*"); 13 | const users = await User.find({ screen_name: query }) 14 | .limit(10) 15 | .select("-password") 16 | .select("-email"); 17 | return res.status(200).send({ 18 | num_results: users.length, 19 | users, 20 | topics: [], 21 | events: [], 22 | lists: [], 23 | ordered_sections: [], 24 | oneclick: [], 25 | hashtags: [], 26 | completed_in: 0.0, 27 | query: params.q, 28 | }); 29 | }); 30 | 31 | export default router; 32 | -------------------------------------------------------------------------------- /src/routes/1.1-prefixed/stubs/settings.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | const router = express.Router(); 4 | 5 | router.get("/users/settings.json", (req, res) => { 6 | return res.status(200).send([]); 7 | }); 8 | 9 | router.get("/account/settings.json", (req, res) => { 10 | return res.status(200).send([]); 11 | }); 12 | 13 | router.get("/help/settings.json", (req, res) => { 14 | return res.status(200).send([]); 15 | }); 16 | 17 | export default router; 18 | -------------------------------------------------------------------------------- /src/routes/1.1-prefixed/stubs/tracking.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | const router = express.Router(); 4 | 5 | router.post("/branch/init.json", (req, res) => { 6 | return res.status(200).send({ 7 | is_loading_enabled: false, 8 | is_tracking_enabled: false, 9 | }); 10 | }); 11 | 12 | export default router; 13 | -------------------------------------------------------------------------------- /src/routes/1.1-prefixed/stubs/users.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | const router = express.Router(); 4 | 5 | router.get("/users/recommendations.json", (req, res) => { 6 | return res.status(200).send([]); 7 | }); 8 | 9 | export default router; 10 | -------------------------------------------------------------------------------- /src/routes/1.1/account/account.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | const router = express.Router(); 4 | 5 | router.use("/account/logout.json", async (req, res) => { 6 | res.cookie("twt_id", `u=0`, { 7 | maxAge: 0, 8 | path: "/", 9 | domain: ".twitter.com", 10 | secure: true, 11 | httpOnly: true, 12 | sameSite: "none", 13 | }); 14 | res.cookie("jwt", "my name is walter hartwell white", { 15 | maxAge: 0, 16 | path: "/", 17 | domain: ".twitter.com", 18 | secure: true, 19 | httpOnly: true, 20 | sameSite: "none", 21 | }); 22 | res.cookie("kdt", "Xpmxhsj8sdhEL8p8qxEX18feiKNkzBy8hKhQv2OI", { 23 | maxAge: 0, 24 | path: "/", 25 | domain: ".twitter.com", 26 | secure: true, 27 | httpOnly: true, 28 | sameSite: "none", 29 | }); 30 | res.cookie("att", "", { 31 | maxAge: 0, 32 | path: "/", 33 | domain: ".twitter.com", 34 | secure: true, 35 | httpOnly: true, 36 | sameSite: "none", 37 | }); 38 | return res.status(200).send({ status: "ok" }); 39 | }); 40 | 41 | export default router; 42 | -------------------------------------------------------------------------------- /src/routes/1.1/jot/generic.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | const router = express.Router(); 4 | 5 | router.use("/jot/*", (req, res) => { 6 | return res.status(200).send({ success: true }); 7 | }); 8 | 9 | // router.use("/jot/ces/p2", (req, res) => { 10 | // return res.status(200).send({ success: true }); 11 | // }); 12 | 13 | // router.post("/jot/error_log.json", (req, res) => { 14 | // return res.status(200) 15 | // }) 16 | 17 | export default router; 18 | -------------------------------------------------------------------------------- /src/routes/1.1/onboarding/generic.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | const router = express.Router(); 4 | 5 | router.use("/onboarding/callback.json", (req, res) => { 6 | return res.status(200).send({ status: "success" }); 7 | }); 8 | 9 | router.use("/onboarding/sso_init.json", (req, res) => { 10 | return res.status(200).send({ status: "success" }); 11 | }); 12 | 13 | export default router; 14 | -------------------------------------------------------------------------------- /src/routes/1.1/stubs/settings.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | const router = express.Router(); 4 | 5 | router.get("/users/settings.json", (req, res) => { 6 | return res.status(200).send([]); 7 | }); 8 | 9 | router.get("/account/settings.json", (req, res) => { 10 | return res.status(200).send([]); 11 | }); 12 | 13 | router.get("/help/settings.json", (req, res) => { 14 | return res.status(200).send([]); 15 | }); 16 | 17 | export default router; 18 | -------------------------------------------------------------------------------- /src/routes/2/notifications/notifications.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import User from "../../../models/User"; 3 | import { decode } from "jsonwebtoken"; 4 | import Notification from "../../../models/Notification"; 5 | import Tweet from "../../../models/Tweet"; 6 | import { warn } from "../../../util/logging"; 7 | import { Document } from "mongoose"; 8 | 9 | const router = express.Router(); 10 | 11 | router.get("/notifications/all.json", async (req, res) => { 12 | const jwt = req.cookies.jwt as string | undefined; 13 | if (!jwt) return res.status(400).send({ msg: "Not authorized" }); 14 | const user = await User.findOne({ 15 | id_string: (decode(jwt) as { id: string }).id, 16 | }); 17 | if (!user) return res.status(500).send({ msg: "User not found" }); 18 | const tweets = {} as { [key: string]: Document }; 19 | const users = {} as { [key: string]: any }; 20 | const notifications = {} as { [key: string]: Document }; 21 | for (const id of user.notification_ids) { 22 | const noti = await Notification.findOne({ id }); 23 | if (!noti) continue; 24 | notifications[noti.id] = noti; 25 | if (noti?.template?.aggregateUserActionsV1?.targetObjects) 26 | for (const id of noti?.template?.aggregateUserActionsV1?.targetObjects) { 27 | const tweet = await Tweet.findOne({ id_str: id.tweet?.id }); 28 | if (!tweet || !id.tweet?.id) continue; 29 | tweets[id.tweet.id] = tweet; 30 | } 31 | if (noti?.template?.aggregateUserActionsV1?.fromUsers) 32 | for (const id of noti?.template?.aggregateUserActionsV1?.fromUsers) { 33 | const user = await User.findOne({ id_string: id.user?.id }); 34 | if (!user || !id.user?.id) continue; 35 | delete user.id_string; 36 | users[id.user.id] = { 37 | ...(user as any)._doc, 38 | id_str: user.id_string, 39 | }; 40 | } 41 | } 42 | const previousSortIndex = user.notification_sort_index; 43 | user.notification_sort_index = Date.now().toString(); 44 | await user.save(); 45 | return res.status(200).send({ 46 | globalObjects: { 47 | tweets, 48 | users, 49 | notifications, 50 | // "1652358172495998980": { 51 | // id: 1652358172495998980, 52 | // id_str: "1652358172495998980", 53 | // name: "notnullptr", 54 | // screen_name: "notnullptr", 55 | // location: null, 56 | // description: null, 57 | // url: null, 58 | // entities: { 59 | // description: { 60 | // urls: [], 61 | // }, 62 | // }, 63 | // protected: false, 64 | // followers_count: 0, 65 | // friends_count: 1, 66 | // listed_count: 0, 67 | // created_at: "Sat Apr 29 17:04:26 +0000 2023", 68 | // favourites_count: 2, 69 | // utc_offset: null, 70 | // time_zone: null, 71 | // geo_enabled: false, 72 | // verified: false, 73 | // statuses_count: 5, 74 | // lang: null, 75 | // contributors_enabled: false, 76 | // is_translator: false, 77 | // is_translation_enabled: false, 78 | // profile_background_color: "F5F8FA", 79 | // profile_background_image_url: null, 80 | // profile_background_image_url_https: null, 81 | // profile_background_tile: false, 82 | // profile_image_url: 83 | // "http://pbs.twimg.com/profile_images/1652358255136350209/a_Ib9CsR_normal.jpg", 84 | // profile_image_url_https: 85 | // "https://pbs.twimg.com/profile_images/1652358255136350209/a_Ib9CsR_normal.jpg", 86 | // profile_link_color: "1DA1F2", 87 | // profile_sidebar_border_color: "C0DEED", 88 | // profile_sidebar_fill_color: "DDEEF6", 89 | // profile_text_color: "333333", 90 | // profile_use_background_image: true, 91 | // default_profile: true, 92 | // default_profile_image: false, 93 | // following: null, 94 | // follow_request_sent: null, 95 | // notifications: null, 96 | // blocking: null, 97 | // translator_type: "none", 98 | // withheld_in_countries: [], 99 | // ext_is_blue_verified: false, 100 | // }, 101 | // "1483042300943122432": { 102 | // id: 1483042300943122432, 103 | // id_str: "1483042300943122432", 104 | // name: "mads\u14383", 105 | // screen_name: "madsthecatgirl", 106 | // location: "right behind you spy tf2 HAHAH", 107 | // description: 108 | // "chronically online twitter looks fun, wish i was there :/", 109 | // url: null, 110 | // entities: { 111 | // description: { 112 | // urls: [], 113 | // }, 114 | // }, 115 | // protected: false, 116 | // followers_count: 30, 117 | // friends_count: 176, 118 | // listed_count: 1, 119 | // created_at: "Mon Jan 17 11:43:44 +0000 2022", 120 | // favourites_count: 1164, 121 | // utc_offset: null, 122 | // time_zone: null, 123 | // geo_enabled: false, 124 | // verified: false, 125 | // statuses_count: 611, 126 | // lang: null, 127 | // contributors_enabled: false, 128 | // is_translator: false, 129 | // is_translation_enabled: false, 130 | // profile_background_color: "F5F8FA", 131 | // profile_background_image_url: null, 132 | // profile_background_image_url_https: null, 133 | // profile_background_tile: false, 134 | // profile_image_url: 135 | // "http://pbs.twimg.com/profile_images/1562613150863736832/3TrYlu0f_normal.jpg", 136 | // profile_image_url_https: 137 | // "https://pbs.twimg.com/profile_images/1562613150863736832/3TrYlu0f_normal.jpg", 138 | // profile_link_color: "1DA1F2", 139 | // profile_sidebar_border_color: "C0DEED", 140 | // profile_sidebar_fill_color: "DDEEF6", 141 | // profile_text_color: "333333", 142 | // profile_use_background_image: true, 143 | // default_profile: true, 144 | // default_profile_image: false, 145 | // following: false, 146 | // follow_request_sent: null, 147 | // notifications: null, 148 | // blocking: false, 149 | // blocked_by: false, 150 | // want_retweets: false, 151 | // profile_interstitial_type: "", 152 | // translator_type: "none", 153 | // withheld_in_countries: [], 154 | // followed_by: false, 155 | // ext_is_blue_verified: false, 156 | // ext_highlighted_label: {}, 157 | // }, 158 | 159 | // "1653176782265237505": { 160 | // created_at: "Mon May 01 23:17:14 +0000 2023", 161 | // id: 1653176782265237505, 162 | // id_str: "1653176782265237505", 163 | // full_text: 164 | // "/1RyAhNwby-gzGCRVsMxKbQ/CreateTweet return value pls? thanks twitter", 165 | // truncated: false, 166 | // display_text_range: [0, 68], 167 | // entities: { 168 | // hashtags: [], 169 | // symbols: [], 170 | // user_mentions: [], 171 | // urls: [], 172 | // }, 173 | // source: 174 | // '\u003ca href="https://mobile.twitter.com" rel="nofollow"\u003eTwitter Web App\u003c/a\u003e', 175 | // in_reply_to_status_id: null, 176 | // in_reply_to_status_id_str: null, 177 | // in_reply_to_user_id: null, 178 | // in_reply_to_user_id_str: null, 179 | // in_reply_to_screen_name: null, 180 | // user_id: 1652358172495998980, 181 | // user_id_str: "1652358172495998980", 182 | // geo: null, 183 | // coordinates: null, 184 | // place: null, 185 | // contributors: null, 186 | // is_quote_status: false, 187 | // retweet_count: 0, 188 | // favorite_count: 1, 189 | // reply_count: 0, 190 | // quote_count: 0, 191 | // conversation_id: 1653176782265237505, 192 | // conversation_id_str: "1653176782265237505", 193 | // conversation_muted: false, 194 | // favorited: false, 195 | // retweeted: false, 196 | // lang: "en", 197 | // ext: { 198 | // superFollowMetadata: { 199 | // r: { 200 | // ok: {}, 201 | // }, 202 | // ttl: -1, 203 | // }, 204 | // }, 205 | // }, 206 | }, 207 | timeline: { 208 | id: "Fu5bANlWcAQAAAABSQ3bEZt9Egk", 209 | instructions: [ 210 | { 211 | addEntries: { 212 | entries: [ 213 | { 214 | entryId: "cursor-top-1686235898801", 215 | sortIndex: "1686235898801", 216 | content: { 217 | operation: { 218 | cursor: { 219 | value: 220 | "DAABDAABCgABFu5bANlWcAQIAAIAAAABCAADSQ3bEQgABJt9EgkACwACAAAAC0FZaWJmZU93N0g4AAA", 221 | cursorType: "Top", 222 | }, 223 | }, 224 | }, 225 | }, 226 | ...Object.keys(notifications).map((id) => { 227 | const notification = notifications[id] as any; 228 | if (!notification) return; 229 | return { 230 | entryId: `notification-${notification.id}`, 231 | sortIndex: notification.timestampMs, 232 | content: { 233 | item: { 234 | content: { 235 | notification: { 236 | id: notification.id, 237 | url: { 238 | urlType: "ExternalUrl", 239 | url: "https://twitter.com/Twitter/1234567890123456", 240 | }, 241 | }, 242 | }, 243 | clientEventInfo: { 244 | component: "urt", 245 | element: "users_liked_your_tweet", 246 | details: { 247 | notificationDetails: { 248 | impressionId: "e8609d2322c03cb5a009ae20b7ff16ed", 249 | metadata: 250 | "CwABAAAAMzk5YzgzMzNiMzJjZjMxYmMuZGQzN2JjZDg4NDg3ZjUzMzw6OTljODMzM2IzMmNmMzFiYwsAAgAAACNGdTViQU5sV2NBUUFBQUFCU1EzYkVadDlFZ21zYWFKOGdEcwsAAwAAAB4xNjUyMzU4MTcyNDk1OTk4OTgwLS01ODUyMzE0ODgKAAQAAAAAAAAAAQ8ABQoAAAABFvFDhknXwAELAAYAAAAWdXNlcnNfbGlrZWRfeW91cl90d2VldA8ABwsAAAABAAAAHjE2NTIzNTgxNzI0OTU5OTg5ODAtLTU4NTIzMTQ4OAA", 251 | }, 252 | }, 253 | }, 254 | }, 255 | }, 256 | }; 257 | }), 258 | { 259 | entryId: "cursor-bottom-1686235898799", 260 | sortIndex: "1686235898799", 261 | content: { 262 | operation: { 263 | cursor: { 264 | value: 265 | "DAACDAABCgABFu5bANlWcAQIAAIAAAABCAADSQ3bEQgABJt9EgkACwACAAAAC0FZaWJmZU93N0g4AAA", 266 | cursorType: "Bottom", 267 | }, 268 | }, 269 | }, 270 | }, 271 | ], 272 | }, 273 | }, 274 | { 275 | clearEntriesUnreadState: {}, 276 | }, 277 | { 278 | markEntriesUnreadGreaterThanSortIndex: { 279 | sortIndex: previousSortIndex, 280 | }, 281 | }, 282 | ], 283 | }, 284 | }); 285 | // return res.status(200).send({ 286 | // globalObjects: { 287 | // notifications: { 288 | // FJTTHX6XwAAAAAABiJONct1wJenoJ7xypmc: { 289 | // id: "FJTTHX6XwAAAAAABiJONct1wJenoJ7xypmc", 290 | // timestampMs: "0", 291 | // icon: { id: "bird_icon" }, 292 | // message: { 293 | // text: "Blue OSS doesn't support notifications right now. Click here to check the GitHub page.", 294 | // entities: [], 295 | // rtl: false, 296 | // }, 297 | // template: { 298 | // aggregateUserActionsV1: { targetObjects: [], fromUsers: [] }, 299 | // }, 300 | // }, 301 | // }, 302 | // }, 303 | // timeline: { 304 | // id: "FJTTHX6XwAAAAAABiJONct1wJek", 305 | // instructions: [ 306 | // { 307 | // addEntries: { 308 | // entries: [ 309 | // { 310 | // entryId: "notification-FJTTHX6XwAAAAAABiJONct1wJenoJ7xypmc", 311 | // sortIndex: "1682892524091", 312 | // content: { 313 | // item: { 314 | // content: { 315 | // notification: { 316 | // id: "FJTTHX6XwAAAAAABiJONct1wJenoJ7xypmc", 317 | // url: { 318 | // urlType: "ExternalUrl", 319 | // url: "https://github.com/not-nullptr/blue", 320 | // }, 321 | // }, 322 | // }, 323 | // clientEventInfo: { 324 | // component: "urt", 325 | // element: "generic_login_notification", 326 | // details: { 327 | // notificationDetails: { 328 | // impressionId: "f1d0344dafd82575fee2ca3636e98fb2", 329 | // metadata: 330 | // "CwABAAAAMzRiMmEwYjY4MzRiMDRmM2YuNGNhZjdmYmJlNjU2MjE5NTw6NGIyYTBiNjgzNGIwNGYzZgsAAgAAACNGSlRUSFg2WHdBQUFBQUFCaUpPTmN0MXdKZW1nbklXRXBtYwsAAwAAABBsb2dpbl9udGFiX25vdGlmCgAEAAAAAAAAAAELAAYAAAAaZ2VuZXJpY19sb2dpbl9ub3RpZmljYXRpb24PAAcLAAAAAQAAAB0xNDgzMDQyMzAwOTQzMTIyNDMyLTYyNjg4Njg5NAA", 331 | // }, 332 | // }, 333 | // }, 334 | // }, 335 | // }, 336 | // }, 337 | // ], 338 | // }, 339 | // }, 340 | // { clearEntriesUnreadState: {} }, 341 | // { 342 | // markEntriesUnreadGreaterThanSortIndex: { sortIndex: "1683059608017" }, 343 | // }, 344 | // ], 345 | // }, 346 | // }); 347 | }); 348 | 349 | export default router; 350 | -------------------------------------------------------------------------------- /src/routes/2/trends/guide.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { Timeline } from "../../../types/guideClass"; 3 | 4 | const router = express.Router(); 5 | 6 | router.get("/guide.json", (req, res) => { 7 | const { ...response } = new Timeline({}); 8 | // return res.status(200).send({ 9 | // globalObjects: { 10 | // tweets: { 11 | // "1652077563651698689": { 12 | // created_at: "Fri Apr 28 22:29:19 +0000 2023", 13 | // id: 1652077563651698689, 14 | // id_str: "1652077563651698689", 15 | // full_text: "On @billmaher show tonight", 16 | // truncated: false, 17 | // display_text_range: [0, 26], 18 | // entities: { 19 | // hashtags: [], 20 | // symbols: [], 21 | // user_mentions: [ 22 | // { 23 | // screen_name: "billmaher", 24 | // name: "Bill Maher", 25 | // id: 19697415, 26 | // id_str: "19697415", 27 | // indices: [3, 13], 28 | // }, 29 | // ], 30 | // urls: [], 31 | // }, 32 | // source: 33 | // '\u003ca href="http://twitter.com/download/iphone" rel="nofollow"\u003eTwitter for iPhone\u003c/a\u003e', 34 | // in_reply_to_status_id: null, 35 | // in_reply_to_status_id_str: null, 36 | // in_reply_to_user_id: null, 37 | // in_reply_to_user_id_str: null, 38 | // in_reply_to_screen_name: null, 39 | // user_id: 44196397, 40 | // user_id_str: "44196397", 41 | // geo: null, 42 | // coordinates: null, 43 | // place: null, 44 | // contributors: null, 45 | // is_quote_status: false, 46 | // retweet_count: 5860, 47 | // favorite_count: 84421, 48 | // reply_count: 5065, 49 | // quote_count: 0, 50 | // conversation_id: 1652077563651698689, 51 | // conversation_id_str: "1652077563651698689", 52 | // favorited: false, 53 | // retweeted: false, 54 | // lang: "en", 55 | // supplemental_language: null, 56 | // ext_views: { 57 | // state: "EnabledWithCount", 58 | // count: "9895293", 59 | // }, 60 | // ext: { 61 | // editControl: { 62 | // r: { 63 | // ok: { 64 | // initial: { 65 | // editTweetIds: ["1652077563651698689"], 66 | // editableUntilMsecs: "1682722759000", 67 | // editsRemaining: "5", 68 | // isEditEligible: true, 69 | // }, 70 | // }, 71 | // }, 72 | // ttl: -1, 73 | // }, 74 | // unmentionInfo: { 75 | // r: { 76 | // ok: {}, 77 | // }, 78 | // ttl: -1, 79 | // }, 80 | // }, 81 | // }, 82 | // }, 83 | // users: { 84 | // "44196397": { 85 | // id: 44196397, 86 | // id_str: "44196397", 87 | // name: "Elon Musk", 88 | // screen_name: "elonmusk", 89 | // location: "Tr\u00f8llheim", 90 | // description: "nothing", 91 | // url: null, 92 | // entities: { 93 | // description: { 94 | // urls: [], 95 | // }, 96 | // }, 97 | // protected: false, 98 | // followers_count: 137293601, 99 | // fast_followers_count: 0, 100 | // normal_followers_count: 137293601, 101 | // friends_count: 249, 102 | // listed_count: 121127, 103 | // created_at: "Tue Jun 02 20:12:29 +0000 2009", 104 | // favourites_count: 22163, 105 | // utc_offset: null, 106 | // time_zone: null, 107 | // geo_enabled: false, 108 | // verified: false, 109 | // statuses_count: 25241, 110 | // media_count: 1520, 111 | // lang: null, 112 | // contributors_enabled: false, 113 | // is_translator: false, 114 | // is_translation_enabled: false, 115 | // profile_background_color: "C0DEED", 116 | // profile_background_image_url: 117 | // "http://abs.twimg.com/images/themes/theme1/bg.png", 118 | // profile_background_image_url_https: 119 | // "https://abs.twimg.com/images/themes/theme1/bg.png", 120 | // profile_background_tile: false, 121 | // profile_image_url: 122 | // "http://pbs.twimg.com/profile_images/1590968738358079488/IY9Gx6Ok_normal.jpg", 123 | // profile_image_url_https: 124 | // "https://pbs.twimg.com/profile_images/1590968738358079488/IY9Gx6Ok_normal.jpg", 125 | // profile_banner_url: 126 | // "https://pbs.twimg.com/profile_banners/44196397/1576183471", 127 | // profile_link_color: "0084B4", 128 | // profile_sidebar_border_color: "C0DEED", 129 | // profile_sidebar_fill_color: "DDEEF6", 130 | // profile_text_color: "333333", 131 | // profile_use_background_image: true, 132 | // has_extended_profile: true, 133 | // default_profile: false, 134 | // default_profile_image: false, 135 | // pinned_tweet_ids: [], 136 | // pinned_tweet_ids_str: [], 137 | // has_custom_timelines: true, 138 | // can_dm: null, 139 | // following: null, 140 | // follow_request_sent: null, 141 | // notifications: null, 142 | // muting: null, 143 | // blocking: null, 144 | // blocked_by: null, 145 | // want_retweets: null, 146 | // advertiser_account_type: "none", 147 | // advertiser_account_service_levels: [], 148 | // business_profile_state: "none", 149 | // translator_type: "none", 150 | // withheld_in_countries: [], 151 | // followed_by: null, 152 | // ext_has_nft_avatar: false, 153 | // ext_profile_image_shape: "Circle", 154 | // ext_is_blue_verified: true, 155 | // ext: { 156 | // hasNftAvatar: { 157 | // r: { 158 | // ok: false, 159 | // }, 160 | // ttl: -1, 161 | // }, 162 | // highlightedLabel: { 163 | // r: { 164 | // ok: { 165 | // label: { 166 | // description: "Twitter", 167 | // badge: { 168 | // url: "https://pbs.twimg.com/profile_images/1488548719062654976/u6qfBBkF_bigger.jpg", 169 | // }, 170 | // url: { 171 | // urlType: "DeepLink", 172 | // url: "https://twitter.com/Twitter", 173 | // }, 174 | // userLabelType: "BusinessLabel", 175 | // userLabelDisplayType: "Badge", 176 | // }, 177 | // }, 178 | // }, 179 | // ttl: -1, 180 | // }, 181 | // }, 182 | // require_some_consent: false, 183 | // }, 184 | // }, 185 | // moments: {}, 186 | // cards: {}, 187 | // places: {}, 188 | // media: {}, 189 | // broadcasts: {}, 190 | // topics: {}, 191 | // lists: {}, 192 | // }, 193 | // timeline: { 194 | // id: "guide-events_tab-page-283-1652275109299355648", 195 | // instructions: [ 196 | // { 197 | // clearCache: {}, 198 | // }, 199 | // { 200 | // addEntries: { 201 | // entries: [ 202 | // { 203 | // entryId: "cursor:1000000000", 204 | // sortIndex: "1000000000", 205 | // content: { 206 | // operation: { 207 | // cursor: { 208 | // value: 209 | // "CwABAAAALWd1aWRlLWV2ZW50c190YWItcGFnZS0yODMtMTY1MjI3NTEwOTI5OTM1NTY0OAgAAwAAAAAIAAIAAAABCAAEO5rKAAA=", 210 | // cursorType: "Top", 211 | // }, 212 | // }, 213 | // }, 214 | // }, 215 | // { 216 | // entryId: "tweet-1652051725740650501", 217 | // sortIndex: "999999999", 218 | // content: { 219 | // item: { 220 | // content: { 221 | // tweet: { 222 | // id: "1652051725740650501", 223 | // displayType: "Tweet", 224 | // }, 225 | // }, 226 | // clientEventInfo: { 227 | // component: "content_recommender_explore_tweets", 228 | // element: "tweet", 229 | // details: { 230 | // timelinesDetails: { 231 | // controllerData: "DAACDAAODAABCgABgjvWN0kn/gIAAAAA", 232 | // }, 233 | // }, 234 | // }, 235 | // }, 236 | // }, 237 | // }, 238 | // { 239 | // entryId: "tweet-1652098207130820611", 240 | // sortIndex: "999999998", 241 | // content: { 242 | // item: { 243 | // content: { 244 | // tweet: { 245 | // id: "1652098207130820611", 246 | // displayType: "Tweet", 247 | // }, 248 | // }, 249 | // clientEventInfo: { 250 | // component: "trip_geo_domain_tweets", 251 | // element: "tweet", 252 | // details: { 253 | // timelinesDetails: { 254 | // controllerData: "DAACDAAODAABCgABgjvWN0kn/gIAAAAA", 255 | // }, 256 | // }, 257 | // }, 258 | // }, 259 | // }, 260 | // }, 261 | // { 262 | // entryId: "tweet-1652214615676444672", 263 | // sortIndex: "999999997", 264 | // content: { 265 | // item: { 266 | // content: { 267 | // tweet: { 268 | // id: "1652214615676444672", 269 | // displayType: "Tweet", 270 | // }, 271 | // }, 272 | // clientEventInfo: { 273 | // component: "trip_geo_domain_tweets", 274 | // element: "tweet", 275 | // details: { 276 | // timelinesDetails: { 277 | // controllerData: "DAACDAAODAABCgABgjvWN0kn/gIAAAAA", 278 | // }, 279 | // }, 280 | // }, 281 | // }, 282 | // }, 283 | // }, 284 | // { 285 | // entryId: "tweet-1652011978292969472", 286 | // sortIndex: "999999996", 287 | // content: { 288 | // item: { 289 | // content: { 290 | // tweet: { 291 | // id: "1652011978292969472", 292 | // displayType: "Tweet", 293 | // }, 294 | // }, 295 | // clientEventInfo: { 296 | // component: "trip_geo_domain_tweets", 297 | // element: "tweet", 298 | // details: { 299 | // timelinesDetails: { 300 | // controllerData: "DAACDAAODAABCgABgjvWN0kn/gIAAAAA", 301 | // }, 302 | // }, 303 | // }, 304 | // }, 305 | // }, 306 | // }, 307 | // { 308 | // entryId: "tweet-1652041043519348745", 309 | // sortIndex: "999999995", 310 | // content: { 311 | // item: { 312 | // content: { 313 | // tweet: { 314 | // id: "1652041043519348745", 315 | // displayType: "Tweet", 316 | // }, 317 | // }, 318 | // clientEventInfo: { 319 | // component: "trip_geo_domain_tweets", 320 | // element: "tweet", 321 | // details: { 322 | // timelinesDetails: { 323 | // controllerData: "DAACDAAODAABCgABgjvWN0kn/gIAAAAA", 324 | // }, 325 | // }, 326 | // }, 327 | // }, 328 | // }, 329 | // }, 330 | // { 331 | // entryId: "tweet-1652048376882405377", 332 | // sortIndex: "999999994", 333 | // content: { 334 | // item: { 335 | // content: { 336 | // tweet: { 337 | // id: "1652048376882405377", 338 | // displayType: "Tweet", 339 | // }, 340 | // }, 341 | // clientEventInfo: { 342 | // component: "trip_geo_domain_tweets", 343 | // element: "tweet", 344 | // details: { 345 | // timelinesDetails: { 346 | // controllerData: "DAACDAAODAABCgABgjvWN0kn/gIAAAAA", 347 | // }, 348 | // }, 349 | // }, 350 | // }, 351 | // }, 352 | // }, 353 | // { 354 | // entryId: "tweet-1652081118802329600", 355 | // sortIndex: "999999993", 356 | // content: { 357 | // item: { 358 | // content: { 359 | // tweet: { 360 | // id: "1652081118802329600", 361 | // displayType: "Tweet", 362 | // }, 363 | // }, 364 | // clientEventInfo: { 365 | // component: "trip_geo_domain_tweets", 366 | // element: "tweet", 367 | // details: { 368 | // timelinesDetails: { 369 | // controllerData: "DAACDAAODAABCgABgjvWN0kn/gIAAAAA", 370 | // }, 371 | // }, 372 | // }, 373 | // }, 374 | // }, 375 | // }, 376 | // { 377 | // entryId: "tweet-1652077855977897985", 378 | // sortIndex: "999999992", 379 | // content: { 380 | // item: { 381 | // content: { 382 | // tweet: { 383 | // id: "1652077855977897985", 384 | // displayType: "Tweet", 385 | // }, 386 | // }, 387 | // clientEventInfo: { 388 | // component: "trip_geo_domain_tweets", 389 | // element: "tweet", 390 | // details: { 391 | // timelinesDetails: { 392 | // controllerData: "DAACDAAODAABCgABgjvWN0kn/gIAAAAA", 393 | // }, 394 | // }, 395 | // }, 396 | // }, 397 | // }, 398 | // }, 399 | // { 400 | // entryId: "tweet-1652266888670355457", 401 | // sortIndex: "999999991", 402 | // content: { 403 | // item: { 404 | // content: { 405 | // tweet: { 406 | // id: "1652266888670355457", 407 | // displayType: "Tweet", 408 | // }, 409 | // }, 410 | // clientEventInfo: { 411 | // component: "trip_geo_domain_tweets", 412 | // element: "tweet", 413 | // details: { 414 | // timelinesDetails: { 415 | // controllerData: "DAACDAAODAABCgABgjvWN0kn/gIAAAAA", 416 | // }, 417 | // }, 418 | // }, 419 | // }, 420 | // }, 421 | // }, 422 | // { 423 | // entryId: "tweet-1652117745687752716", 424 | // sortIndex: "999999990", 425 | // content: { 426 | // item: { 427 | // content: { 428 | // tweet: { 429 | // id: "1652117745687752716", 430 | // displayType: "Tweet", 431 | // }, 432 | // }, 433 | // clientEventInfo: { 434 | // component: "trip_geo_domain_tweets", 435 | // element: "tweet", 436 | // details: { 437 | // timelinesDetails: { 438 | // controllerData: "DAACDAAODAABCgABgjvWN0kn/gIAAAAA", 439 | // }, 440 | // }, 441 | // }, 442 | // }, 443 | // }, 444 | // }, 445 | // { 446 | // entryId: "tweet-1652254769518465024", 447 | // sortIndex: "999999989", 448 | // content: { 449 | // item: { 450 | // content: { 451 | // tweet: { 452 | // id: "1652254769518465024", 453 | // displayType: "Tweet", 454 | // }, 455 | // }, 456 | // clientEventInfo: { 457 | // component: "trip_geo_domain_tweets", 458 | // element: "tweet", 459 | // details: { 460 | // timelinesDetails: { 461 | // controllerData: "DAACDAAODAABCgABgjvWN0kn/gIAAAAA", 462 | // }, 463 | // }, 464 | // }, 465 | // }, 466 | // }, 467 | // }, 468 | // { 469 | // entryId: "tweet-1652080087926288384", 470 | // sortIndex: "999999988", 471 | // content: { 472 | // item: { 473 | // content: { 474 | // tweet: { 475 | // id: "1652080087926288384", 476 | // displayType: "Tweet", 477 | // }, 478 | // }, 479 | // clientEventInfo: { 480 | // component: "trip_geo_domain_tweets", 481 | // element: "tweet", 482 | // details: { 483 | // timelinesDetails: { 484 | // controllerData: "DAACDAAODAABCgABgjvWN0kn/gIAAAAA", 485 | // }, 486 | // }, 487 | // }, 488 | // }, 489 | // }, 490 | // }, 491 | // { 492 | // entryId: "tweet-1652077563651698689", 493 | // sortIndex: "999999987", 494 | // content: { 495 | // item: { 496 | // content: { 497 | // tweet: { 498 | // id: "1652077563651698689", 499 | // displayType: "Tweet", 500 | // }, 501 | // }, 502 | // clientEventInfo: { 503 | // component: "trip_geo_domain_tweets", 504 | // element: "tweet", 505 | // details: { 506 | // timelinesDetails: { 507 | // controllerData: "DAACDAAODAABCgABgjvWN0kn/gIAAAAA", 508 | // }, 509 | // }, 510 | // }, 511 | // }, 512 | // }, 513 | // }, 514 | // { 515 | // entryId: "tweet-1652087469167493120", 516 | // sortIndex: "999999986", 517 | // content: { 518 | // item: { 519 | // content: { 520 | // tweet: { 521 | // id: "1652087469167493120", 522 | // displayType: "Tweet", 523 | // }, 524 | // }, 525 | // clientEventInfo: { 526 | // component: "content_recommender_explore_tweets", 527 | // element: "tweet", 528 | // details: { 529 | // timelinesDetails: { 530 | // controllerData: "DAACDAAODAABCgABgjvWN0kn/gIAAAAA", 531 | // }, 532 | // }, 533 | // }, 534 | // }, 535 | // }, 536 | // }, 537 | // { 538 | // entryId: "tweet-1652000709813080065", 539 | // sortIndex: "999999985", 540 | // content: { 541 | // item: { 542 | // content: { 543 | // tweet: { 544 | // id: "1652000709813080065", 545 | // displayType: "Tweet", 546 | // }, 547 | // }, 548 | // clientEventInfo: { 549 | // component: "trip_geo_domain_tweets", 550 | // element: "tweet", 551 | // details: { 552 | // timelinesDetails: { 553 | // controllerData: "DAACDAAODAABCgABgjvWN0kn/gIAAAAA", 554 | // }, 555 | // }, 556 | // }, 557 | // }, 558 | // }, 559 | // }, 560 | // { 561 | // entryId: "tweet-1652098204232732672", 562 | // sortIndex: "999999984", 563 | // content: { 564 | // item: { 565 | // content: { 566 | // tweet: { 567 | // id: "1652098204232732672", 568 | // displayType: "Tweet", 569 | // }, 570 | // }, 571 | // clientEventInfo: { 572 | // component: "trip_geo_domain_tweets", 573 | // element: "tweet", 574 | // details: { 575 | // timelinesDetails: { 576 | // controllerData: "DAACDAAODAABCgABgjvWN0kn/gIAAAAA", 577 | // }, 578 | // }, 579 | // }, 580 | // }, 581 | // }, 582 | // }, 583 | // { 584 | // entryId: "tweet-1651972286738575364", 585 | // sortIndex: "999999983", 586 | // content: { 587 | // item: { 588 | // content: { 589 | // tweet: { 590 | // id: "1651972286738575364", 591 | // displayType: "Tweet", 592 | // }, 593 | // }, 594 | // clientEventInfo: { 595 | // component: "trip_geo_domain_tweets", 596 | // element: "tweet", 597 | // details: { 598 | // timelinesDetails: { 599 | // controllerData: "DAACDAAODAABCgABgjvWN0kn/gIAAAAA", 600 | // }, 601 | // }, 602 | // }, 603 | // }, 604 | // }, 605 | // }, 606 | // { 607 | // entryId: "tweet-1651969705735868416", 608 | // sortIndex: "999999982", 609 | // content: { 610 | // item: { 611 | // content: { 612 | // tweet: { 613 | // id: "1651969705735868416", 614 | // displayType: "Tweet", 615 | // }, 616 | // }, 617 | // clientEventInfo: { 618 | // component: "content_recommender_explore_tweets", 619 | // element: "tweet", 620 | // details: { 621 | // timelinesDetails: { 622 | // controllerData: "DAACDAAODAABCgABgjvWN0kn/gIAAAAA", 623 | // }, 624 | // }, 625 | // }, 626 | // }, 627 | // }, 628 | // }, 629 | // { 630 | // entryId: "tweet-1652040280609288193", 631 | // sortIndex: "999999981", 632 | // content: { 633 | // item: { 634 | // content: { 635 | // tweet: { 636 | // id: "1652040280609288193", 637 | // displayType: "Tweet", 638 | // }, 639 | // }, 640 | // clientEventInfo: { 641 | // component: "trip_geo_domain_tweets", 642 | // element: "tweet", 643 | // details: { 644 | // timelinesDetails: { 645 | // controllerData: "DAACDAAODAABCgABgjvWN0kn/gIAAAAA", 646 | // }, 647 | // }, 648 | // }, 649 | // }, 650 | // }, 651 | // }, 652 | // { 653 | // entryId: "tweet-1652035403908521986", 654 | // sortIndex: "999999980", 655 | // content: { 656 | // item: { 657 | // content: { 658 | // tweet: { 659 | // id: "1652035403908521986", 660 | // displayType: "Tweet", 661 | // }, 662 | // }, 663 | // clientEventInfo: { 664 | // component: "trip_geo_domain_tweets", 665 | // element: "tweet", 666 | // details: { 667 | // timelinesDetails: { 668 | // controllerData: "DAACDAAODAABCgABgjvWN0kn/gIAAAAA", 669 | // }, 670 | // }, 671 | // }, 672 | // }, 673 | // }, 674 | // }, 675 | // { 676 | // entryId: "cursor:999999979", 677 | // sortIndex: "999999979", 678 | // content: { 679 | // operation: { 680 | // cursor: { 681 | // value: 682 | // "CwABAAAALWd1aWRlLWV2ZW50c190YWItcGFnZS0yODMtMTY1MjI3NTEwOTI5OTM1NTY0OAgAAwAAABQIAAIAAAACCAAEO5rJ6wA=", 683 | // cursorType: "Bottom", 684 | // }, 685 | // }, 686 | // }, 687 | // }, 688 | // ], 689 | // }, 690 | // }, 691 | // ], 692 | // responseObjects: { 693 | // feedbackActions: { 694 | // trend_irrelevant_content_feedback_key: { 695 | // feedbackType: "SeeFewer", 696 | // prompt: "The associated content is not relevant", 697 | // confirmation: "Thanks. Refresh this page to update these trends.", 698 | // feedbackUrl: 699 | // "/2/guide/insert_feedback/trend_irrelevant_content_feedback_key.json", 700 | // hasUndoAction: false, 701 | // confirmationDisplayType: "BottomSheet", 702 | // }, 703 | // spotlight_dismiss_feedback_key: { 704 | // feedbackType: "Dismiss", 705 | // prompt: "Don't want to see this ad", 706 | // confirmation: "Thanks. Refresh this page to update these trends.", 707 | // hasUndoAction: false, 708 | // confirmationDisplayType: "BottomSheet", 709 | // }, 710 | // trend_abusive_feedback_key: { 711 | // feedbackType: "SeeFewer", 712 | // prompt: "This trend is abusive or harmful", 713 | // confirmation: "Thanks. Refresh this page to update these trends.", 714 | // feedbackUrl: 715 | // "/2/guide/insert_feedback/trend_abusive_feedback_key.json", 716 | // hasUndoAction: false, 717 | // confirmationDisplayType: "Inline", 718 | // }, 719 | // guide_not_interested_feedback_key: { 720 | // feedbackType: "SeeFewer", 721 | // prompt: "Hide", 722 | // confirmation: "Thank you, we will show you less of this", 723 | // feedbackUrl: "/2/guide/dont_like_this/feedback.json", 724 | // hasUndoAction: true, 725 | // confirmationDisplayType: "Inline", 726 | // icon: "NO", 727 | // }, 728 | // guide_see_more_feedback_key: { 729 | // feedbackType: "SeeMore", 730 | // prompt: "Show more often", 731 | // confirmation: "Thank you, we will show you more of this", 732 | // feedbackUrl: "/2/guide/show_more/feedback.json", 733 | // hasUndoAction: true, 734 | // confirmationDisplayType: "BottomSheet", 735 | // }, 736 | // trend_spam_feedback_key: { 737 | // feedbackType: "SeeFewer", 738 | // prompt: "This trend is spam", 739 | // confirmation: "Thanks. Refresh this page to update these trends.", 740 | // feedbackUrl: 741 | // "/2/guide/insert_feedback/trend_spam_feedback_key.json", 742 | // hasUndoAction: false, 743 | // confirmationDisplayType: "Inline", 744 | // }, 745 | // trend_low_quality_feedback_key: { 746 | // feedbackType: "SeeFewer", 747 | // prompt: "This trend is low quality", 748 | // confirmation: "Thanks. Refresh this page to update these trends.", 749 | // feedbackUrl: 750 | // "/2/guide/insert_feedback/trend_low_quality_feedback_key.json", 751 | // hasUndoAction: false, 752 | // confirmationDisplayType: "Inline", 753 | // }, 754 | // guide_see_less_feedback_key: { 755 | // feedbackType: "SeeFewer", 756 | // prompt: "Show less often", 757 | // confirmation: "Thank you, we will show you less of this", 758 | // feedbackUrl: "/2/guide/dont_like_this/feedback.json", 759 | // hasUndoAction: true, 760 | // confirmationDisplayType: "Inline", 761 | // }, 762 | // trend_not_interested_in_this_feedback_key: { 763 | // feedbackType: "SeeFewer", 764 | // prompt: "Not interested in this", 765 | // confirmation: "Thanks. Refresh this page to update these trends.", 766 | // feedbackUrl: 767 | // "/2/guide/insert_feedback/trend_not_interested_in_this_feedback_key.json", 768 | // hasUndoAction: false, 769 | // confirmationDisplayType: "Inline", 770 | // }, 771 | // trend_abusive_or_harmful_feedback_key: { 772 | // feedbackType: "SeeFewer", 773 | // prompt: "This trend is harmful or spammy", 774 | // confirmation: "Thanks. Refresh this page to update these trends.", 775 | // feedbackUrl: 776 | // "/2/guide/insert_feedback/trend_abusive_or_harmful_feedback_key.json", 777 | // hasUndoAction: false, 778 | // confirmationDisplayType: "BottomSheet", 779 | // }, 780 | // guide_hide_topic_key: { 781 | // feedbackType: "SeeFewer", 782 | // prompt: "Unfollow topic", 783 | // confirmation: "Thank you, we will show you less of this", 784 | // feedbackUrl: "/2/guide/feedback/hide_topic.json", 785 | // hasUndoAction: true, 786 | // confirmationDisplayType: "Inline", 787 | // icon: "TOPIC_CLOSE", 788 | // }, 789 | // trend_duplicate_feedback_key: { 790 | // feedbackType: "SeeFewer", 791 | // prompt: "This trend is a duplicate", 792 | // confirmation: "Thanks. Refresh this page to update these trends.", 793 | // feedbackUrl: 794 | // "/2/guide/insert_feedback/trend_duplicate_feedback_key.json", 795 | // hasUndoAction: false, 796 | // confirmationDisplayType: "Inline", 797 | // }, 798 | // }, 799 | // }, 800 | // }, 801 | // }); 802 | // if (req.cookies["jwt"]) { 803 | return res.status(200).send({ 804 | globalObjects: { 805 | broadcasts: {}, 806 | cards: {}, 807 | lists: {}, 808 | media: { 809 | "1557801000177508352": { 810 | id: 1557801000177508352, 811 | id_str: "1557801000177508352", 812 | media_key: "30_1557801000177508352", 813 | media_url: 814 | "https://pbs.twimg.com/semantic_core_img/1557801000177508352/FJ_dWr_K?format=jpg&name=orig", 815 | original_info: { 816 | height: 390, 817 | width: 690, 818 | }, 819 | type: "photo", 820 | url: "https://pbs.twimg.com/semantic_core_img/1557801000177508352/FJ_dWr_K?format=jpg&name=orig", 821 | }, 822 | }, 823 | moments: {}, 824 | places: {}, 825 | topics: {}, 826 | tweets: {}, 827 | users: {}, 828 | }, 829 | timeline: { 830 | id: "guide-168290849188296172-main-page-154-1652875681308213248", 831 | instructions: [ 832 | { 833 | clearCache: {}, 834 | }, 835 | { 836 | addEntries: { 837 | entries: [ 838 | { 839 | content: { 840 | operation: { 841 | cursor: { 842 | cursorType: "Top", 843 | value: "DefaultTopCursorValue", 844 | }, 845 | }, 846 | }, 847 | entryId: "cursor:2", 848 | sortIndex: "3", 849 | }, 850 | { 851 | content: { 852 | timelineModule: { 853 | clientEventInfo: { 854 | component: "unified_events", 855 | details: { 856 | guideDetails: { 857 | identifier: 858 | "DAABDAABCwABAAAALEd1aWRlLTBkNDA4NzEzLTI5MmItNGFiNy1iN2E2LWMwODVhZTQ1NDk4NS0xCwACAAAALEd1aWRlLTBkNDA4NzEzLTI5MmItNGFiNy1iN2E2LWMwODVhZTQ1NDk4NS0xAAAPAAIMAAAAAQwAAQsAAQAAACxHdWlkZS0wZDQwODcxMy0yOTJiLTRhYjctYjdhNi1jMDg1YWU0NTQ5ODUtMQsAAgAAACxHdWlkZS0wZDQwODcxMy0yOTJiLTRhYjctYjdhNi1jMDg1YWU0NTQ5ODUtMQAAAA==", 859 | }, 860 | }, 861 | }, 862 | displayType: "Vertical", 863 | footer: { 864 | displayType: "Classic", 865 | landingUrl: { 866 | url: "https://github.com/not-nullptr/blue", 867 | urlType: "ExternalUrl", 868 | }, 869 | text: "Go to the GitHub page", 870 | url: "https://github.com/not-nullptr/blue", 871 | }, 872 | header: { 873 | displayType: "Classic", 874 | text: "Blue OSS changelog", 875 | }, 876 | items: [ 877 | // { 878 | // entryId: "event-1636161168719880192", 879 | // item: { 880 | // clientEventInfo: { 881 | // component: "unified_events", 882 | // details: { 883 | // guideDetails: { 884 | // identifier: 885 | // "DAABDAABCwABAAAAFXVuaWZpZWRfZXZlbnRzX21vZHVsZQsAAgAAAA51bmlmaWVkX2V2ZW50cwAADwACDAAAAAEMAAELAAEAAAAVdW5pZmllZF9ldmVudHNfbW9kdWxlCwACAAAADnVuaWZpZWRfZXZlbnRzAAAA", 886 | // token: 887 | // "CwABAAAAJDBkNDA4NzEzLTI5MmItNGFiNy1iN2E2LWMwODVhZTQ1NDk4NQsAAgAAABMxNjM2MTYxMTY4NzE5ODgwMTkyBgADAAAKAAQW7xu+shZgAgsABQAAAB1FdmVudEludGVyZXN0Q2FuZGlkYXRlQnVpbGRlcgoABha0z+oI1iAAAA==", 888 | // transparentGuideDetails: { 889 | // eventMetadata: { 890 | // eventId: "1636161168719880192", 891 | // impressionId: 892 | // "0d408713-292b-4ab7-b7a6-c085ae454985", 893 | // position: 0, 894 | // sourceId: "1652570094113808386", 895 | // sourceName: "EventInterestCandidateBuilder", 896 | // }, 897 | // }, 898 | // }, 899 | // }, 900 | // element: "event", 901 | // }, 902 | // content: { 903 | // eventSummary: { 904 | // displayType: "Cell", 905 | // id: "1636161168719880192", 906 | // image: { 907 | // height: 390, 908 | // url: "https://pbs.twimg.com/semantic_core_img/1557801000177508352/FJ_dWr_K?format=jpg&name=orig", 909 | // width: 690, 910 | // }, 911 | // media: { 912 | // mediaEntity: { 913 | // image: { 914 | // height: 390, 915 | // url: "https://pbs.twimg.com/semantic_core_img/1557801000177508352/FJ_dWr_K?format=jpg&name=orig", 916 | // width: 690, 917 | // }, 918 | // }, 919 | // mediaKey: { 920 | // category: 30, 921 | // id: "1557801000177508352", 922 | // }, 923 | // }, 924 | // supportingText: "Premier League", 925 | // timeString: "Yesterday", 926 | // title: "Liverpool FC vs Tottenham Hotspur", 927 | // url: { 928 | // url: "https://twitter.com/i/events/1636161168719880192", 929 | // urlType: "ExternalUrl", 930 | // }, 931 | // }, 932 | // }, 933 | // }, 934 | // }, 935 | { 936 | entryId: "trends-TikTok", 937 | item: { 938 | clientEventInfo: { 939 | component: "unified_events", 940 | details: { 941 | guideDetails: { 942 | identifier: 943 | "DAABDAABCwABAAAAFXVuaWZpZWRfZXZlbnRzX21vZHVsZQsAAgAAAAZ0cmVuZHMAAA8AAgwAAAABDAABCwABAAAAFXVuaWZpZWRfZXZlbnRzX21vZHVsZQsAAgAAAAZ0cmVuZHMAAAA=", 944 | token: 945 | "CwABAAAAJDBkNDA4NzEzLTI5MmItNGFiNy1iN2E2LWMwODVhZTQ1NDk4NQsAAgAAADM6bG9jYXRpb25fcmVxdWVzdDplbnRpdHlfdHJlbmQ6dGF4aV9jb3VudHJ5X3NvdXJjZToGAAMAAQsABAAAAAZUaWtUb2sPAAwKAAAAAAIADQAA", 946 | transparentGuideDetails: { 947 | trendMetadata: { 948 | associatedCuratedTweetIds: [], 949 | containsCuratedTitle: false, 950 | impressionId: 951 | "0d408713-292b-4ab7-b7a6-c085ae454985", 952 | impressionToken: 953 | ":location_request:entity_trend:taxi_country_source:", 954 | position: 1, 955 | trendName: "TikTok", 956 | }, 957 | }, 958 | }, 959 | }, 960 | element: "trend", 961 | }, 962 | content: { 963 | trend: { 964 | associatedCardUrls: [], 965 | name: "Version 1 Alpha (revision 1)", 966 | trendMetadata: { 967 | metaDescription: 968 | "Initial alpha release of Blue. Includes accounts, tweeting and likes. Enjoy!", 969 | }, 970 | }, 971 | }, 972 | feedbackInfo: { 973 | clientEventInfo: { 974 | action: "click", 975 | component: "trends", 976 | element: "feedback", 977 | }, 978 | feedbackKeys: [ 979 | "trend_not_interested_in_this_feedback_key", 980 | "trend_abusive_or_harmful_feedback_key", 981 | ], 982 | feedbackMetadata: 983 | "DwABCwAAAAEAAAAGVGlrVG9rCgADZBa4US/r78kGAAQAAQ8ABQsAAAAACAAGAAAAAQA=", 984 | }, 985 | }, 986 | }, 987 | // { 988 | // entryId: "trends-Russia", 989 | // item: { 990 | // clientEventInfo: { 991 | // component: "unified_events", 992 | // details: { 993 | // guideDetails: { 994 | // identifier: 995 | // "DAABDAABCwABAAAAFXVuaWZpZWRfZXZlbnRzX21vZHVsZQsAAgAAAAZ0cmVuZHMAAA8AAgwAAAABDAABCwABAAAAFXVuaWZpZWRfZXZlbnRzX21vZHVsZQsAAgAAAAZ0cmVuZHMAAAA=", 996 | // token: 997 | // "CwABAAAAJDBkNDA4NzEzLTI5MmItNGFiNy1iN2E2LWMwODVhZTQ1NDk4NQsAAgAAADM6bG9jYXRpb25fcmVxdWVzdDplbnRpdHlfdHJlbmQ6dGF4aV9jb3VudHJ5X3NvdXJjZToGAAMAAgsABAAAAAZSdXNzaWEPAAkKAAAAAQvERVqEFcABCgAKC8RFWoQVwAEPAAwKAAAAAAIADQAA", 998 | // transparentGuideDetails: { 999 | // trendMetadata: { 1000 | // associatedCuratedTweetIds: [], 1001 | // containsCuratedTitle: false, 1002 | // displayedTopicId: "847878884917886977", 1003 | // impressionId: 1004 | // "0d408713-292b-4ab7-b7a6-c085ae454985", 1005 | // impressionToken: 1006 | // ":location_request:entity_trend:taxi_country_source:", 1007 | // position: 2, 1008 | // topicIds: ["847878884917886977"], 1009 | // trendName: "Russia", 1010 | // }, 1011 | // }, 1012 | // }, 1013 | // }, 1014 | // element: "trend", 1015 | // }, 1016 | // content: { 1017 | // trend: { 1018 | // associatedCardUrls: [], 1019 | // name: "Russia", 1020 | // trendMetadata: { 1021 | // domainContext: "Politics · Trending", 1022 | // metaDescription: "266K Tweets", 1023 | // url: { 1024 | // url: "twitter://search/?query=Russia&src=trend_click&pc=true&vertical=trends", 1025 | // urlType: "DeepLink", 1026 | // urtEndpointOptions: { 1027 | // requestParams: { 1028 | // cd: "HBgGUnVzc2lhGCQwZDQwODcxMy0yOTJiLTRhYjctYjdhNi1jMDg1YWU0NTQ5ODUAAA==", 1029 | // }, 1030 | // }, 1031 | // }, 1032 | // }, 1033 | // url: { 1034 | // url: "twitter://search/?query=Russia&src=trend_click&pc=true&vertical=trends", 1035 | // urlType: "DeepLink", 1036 | // urtEndpointOptions: { 1037 | // requestParams: { 1038 | // cd: "HBgGUnVzc2lhGCQwZDQwODcxMy0yOTJiLTRhYjctYjdhNi1jMDg1YWU0NTQ5ODUAAA==", 1039 | // }, 1040 | // }, 1041 | // }, 1042 | // }, 1043 | // }, 1044 | // feedbackInfo: { 1045 | // clientEventInfo: { 1046 | // action: "click", 1047 | // component: "trends", 1048 | // element: "feedback", 1049 | // }, 1050 | // feedbackKeys: [ 1051 | // "trend_not_interested_in_this_feedback_key", 1052 | // "trend_abusive_or_harmful_feedback_key", 1053 | // ], 1054 | // feedbackMetadata: 1055 | // "DwABCwAAAAEAAAAGUnVzc2lhCgADZBa4US/r78kGAAQAAg8ABQsAAAAACAAGAAAAAQA=", 1056 | // }, 1057 | // }, 1058 | // }, 1059 | // { 1060 | // entryId: "trends-MOTD2", 1061 | // item: { 1062 | // clientEventInfo: { 1063 | // component: "unified_events", 1064 | // details: { 1065 | // guideDetails: { 1066 | // identifier: 1067 | // "DAABDAABCwABAAAAFXVuaWZpZWRfZXZlbnRzX21vZHVsZQsAAgAAAAZ0cmVuZHMAAA8AAgwAAAABDAABCwABAAAAFXVuaWZpZWRfZXZlbnRzX21vZHVsZQsAAgAAAAZ0cmVuZHMAAAA=", 1068 | // token: 1069 | // "CwABAAAAJDBkNDA4NzEzLTI5MmItNGFiNy1iN2E2LWMwODVhZTQ1NDk4NQsAAgAAADM6bG9jYXRpb25fcmVxdWVzdDplbnRpdHlfdHJlbmQ6dGF4aV9jb3VudHJ5X3NvdXJjZToGAAMAAwsABAAAAAVNT1REMg8ADAoAAAAAAgANAAA=", 1070 | // transparentGuideDetails: { 1071 | // trendMetadata: { 1072 | // associatedCuratedTweetIds: [], 1073 | // containsCuratedTitle: false, 1074 | // impressionId: 1075 | // "0d408713-292b-4ab7-b7a6-c085ae454985", 1076 | // impressionToken: 1077 | // ":location_request:entity_trend:taxi_country_source:", 1078 | // position: 3, 1079 | // trendName: "MOTD2", 1080 | // }, 1081 | // }, 1082 | // }, 1083 | // }, 1084 | // element: "trend", 1085 | // }, 1086 | // content: { 1087 | // trend: { 1088 | // associatedCardUrls: [], 1089 | // name: "MOTD2", 1090 | // trendMetadata: { 1091 | // domainContext: "Trending in United Kingdom", 1092 | // url: { 1093 | // url: "twitter://search/?query=MOTD2&src=trend_click&pc=true&vertical=trends", 1094 | // urlType: "DeepLink", 1095 | // urtEndpointOptions: { 1096 | // requestParams: { 1097 | // cd: "HBgFTU9URDIYJDBkNDA4NzEzLTI5MmItNGFiNy1iN2E2LWMwODVhZTQ1NDk4NQAA", 1098 | // }, 1099 | // }, 1100 | // }, 1101 | // }, 1102 | // url: { 1103 | // url: "twitter://search/?query=MOTD2&src=trend_click&pc=true&vertical=trends", 1104 | // urlType: "DeepLink", 1105 | // urtEndpointOptions: { 1106 | // requestParams: { 1107 | // cd: "HBgFTU9URDIYJDBkNDA4NzEzLTI5MmItNGFiNy1iN2E2LWMwODVhZTQ1NDk4NQAA", 1108 | // }, 1109 | // }, 1110 | // }, 1111 | // }, 1112 | // }, 1113 | // feedbackInfo: { 1114 | // clientEventInfo: { 1115 | // action: "click", 1116 | // component: "trends", 1117 | // element: "feedback", 1118 | // }, 1119 | // feedbackKeys: [ 1120 | // "trend_not_interested_in_this_feedback_key", 1121 | // "trend_abusive_or_harmful_feedback_key", 1122 | // ], 1123 | // feedbackMetadata: 1124 | // "DwABCwAAAAEAAAAFTU9URDIKAANkFrhRL+vvyQYABAADDwAFCwAAAAAIAAYAAAABAA==", 1125 | // }, 1126 | // }, 1127 | // }, 1128 | // { 1129 | // entryId: "trends-Star+Wars", 1130 | // item: { 1131 | // clientEventInfo: { 1132 | // component: "unified_events", 1133 | // details: { 1134 | // guideDetails: { 1135 | // identifier: 1136 | // "DAABDAABCwABAAAAFXVuaWZpZWRfZXZlbnRzX21vZHVsZQsAAgAAAAZ0cmVuZHMAAA8AAgwAAAABDAABCwABAAAAFXVuaWZpZWRfZXZlbnRzX21vZHVsZQsAAgAAAAZ0cmVuZHMAAAA=", 1137 | // token: 1138 | // "CwABAAAAJDBkNDA4NzEzLTI5MmItNGFiNy1iN2E2LWMwODVhZTQ1NDk4NQsAAgAAADM6bG9jYXRpb25fcmVxdWVzdDplbnRpdHlfdHJlbmQ6dGF4aV9jb3VudHJ5X3NvdXJjZToGAAMABAsABAAAAAlTdGFyIFdhcnMPAAkKAAAAAQumqRHLVOAACgAKC6apEctU4AAPAAwKAAAAAAIADQAA", 1139 | // transparentGuideDetails: { 1140 | // trendMetadata: { 1141 | // associatedCuratedTweetIds: [], 1142 | // containsCuratedTitle: false, 1143 | // displayedTopicId: "839544274442051584", 1144 | // impressionId: 1145 | // "0d408713-292b-4ab7-b7a6-c085ae454985", 1146 | // impressionToken: 1147 | // ":location_request:entity_trend:taxi_country_source:", 1148 | // position: 4, 1149 | // topicIds: ["839544274442051584"], 1150 | // trendName: "Star Wars", 1151 | // }, 1152 | // }, 1153 | // }, 1154 | // }, 1155 | // element: "trend", 1156 | // }, 1157 | // content: { 1158 | // trend: { 1159 | // associatedCardUrls: [], 1160 | // name: "Star Wars", 1161 | // trendMetadata: { 1162 | // domainContext: "Entertainment · Trending", 1163 | // metaDescription: "44.3K Tweets", 1164 | // url: { 1165 | // url: "twitter://search/?query=%22Star+Wars%22&src=trend_click&pc=true&vertical=trends", 1166 | // urlType: "DeepLink", 1167 | // urtEndpointOptions: { 1168 | // requestParams: { 1169 | // cd: "HBgJU3RhciBXYXJzGCQwZDQwODcxMy0yOTJiLTRhYjctYjdhNi1jMDg1YWU0NTQ5ODUAAA==", 1170 | // }, 1171 | // }, 1172 | // }, 1173 | // }, 1174 | // url: { 1175 | // url: "twitter://search/?query=%22Star+Wars%22&src=trend_click&pc=true&vertical=trends", 1176 | // urlType: "DeepLink", 1177 | // urtEndpointOptions: { 1178 | // requestParams: { 1179 | // cd: "HBgJU3RhciBXYXJzGCQwZDQwODcxMy0yOTJiLTRhYjctYjdhNi1jMDg1YWU0NTQ5ODUAAA==", 1180 | // }, 1181 | // }, 1182 | // }, 1183 | // }, 1184 | // }, 1185 | // feedbackInfo: { 1186 | // clientEventInfo: { 1187 | // action: "click", 1188 | // component: "trends", 1189 | // element: "feedback", 1190 | // }, 1191 | // feedbackKeys: [ 1192 | // "trend_not_interested_in_this_feedback_key", 1193 | // "trend_abusive_or_harmful_feedback_key", 1194 | // ], 1195 | // feedbackMetadata: 1196 | // "DwABCwAAAAEAAAAJU3RhciBXYXJzCgADZBa4US/r78kGAAQABA8ABQsAAAAACAAGAAAAAQA=", 1197 | // }, 1198 | // }, 1199 | // }, 1200 | ], 1201 | }, 1202 | }, 1203 | entryId: "Guide-0d408713-292b-4ab7-b7a6-c085ae454985-1", 1204 | sortIndex: "2", 1205 | }, 1206 | { 1207 | content: { 1208 | operation: { 1209 | cursor: { 1210 | cursorType: "Bottom", 1211 | value: "DefaultBottomCursorValue", 1212 | }, 1213 | }, 1214 | }, 1215 | entryId: "cursor:1", 1216 | sortIndex: "1", 1217 | }, 1218 | ], 1219 | }, 1220 | }, 1221 | { 1222 | terminateTimeline: { 1223 | direction: "Bottom", 1224 | }, 1225 | }, 1226 | ], 1227 | responseObjects: { 1228 | // feedbackActions: { 1229 | // guide_hide_topic_key: { 1230 | // confirmation: "Thank you, we will show you less of this", 1231 | // confirmationDisplayType: "Inline", 1232 | // feedbackType: "SeeFewer", 1233 | // feedbackUrl: "/2/guide/feedback/hide_topic.json", 1234 | // hasUndoAction: true, 1235 | // icon: "TOPIC_CLOSE", 1236 | // prompt: "Unfollow topic", 1237 | // }, 1238 | // guide_not_interested_feedback_key: { 1239 | // confirmation: "Thank you, we will show you less of this", 1240 | // confirmationDisplayType: "Inline", 1241 | // feedbackType: "SeeFewer", 1242 | // feedbackUrl: "/2/guide/dont_like_this/feedback.json", 1243 | // hasUndoAction: true, 1244 | // icon: "NO", 1245 | // prompt: "Hide", 1246 | // }, 1247 | // guide_see_less_feedback_key: { 1248 | // confirmation: "Thank you, we will show you less of this", 1249 | // confirmationDisplayType: "Inline", 1250 | // feedbackType: "SeeFewer", 1251 | // feedbackUrl: "/2/guide/dont_like_this/feedback.json", 1252 | // hasUndoAction: true, 1253 | // prompt: "Show less often", 1254 | // }, 1255 | // guide_see_more_feedback_key: { 1256 | // confirmation: "Thank you, we will show you more of this", 1257 | // confirmationDisplayType: "BottomSheet", 1258 | // feedbackType: "SeeMore", 1259 | // feedbackUrl: "/2/guide/show_more/feedback.json", 1260 | // hasUndoAction: true, 1261 | // prompt: "Show more often", 1262 | // }, 1263 | // spotlight_dismiss_feedback_key: { 1264 | // confirmation: "Thanks. Refresh this page to update these trends.", 1265 | // confirmationDisplayType: "BottomSheet", 1266 | // feedbackType: "Dismiss", 1267 | // hasUndoAction: false, 1268 | // prompt: "Don't want to see this ad", 1269 | // }, 1270 | // trend_abusive_feedback_key: { 1271 | // confirmation: "Thanks. Refresh this page to update these trends.", 1272 | // confirmationDisplayType: "Inline", 1273 | // feedbackType: "SeeFewer", 1274 | // feedbackUrl: 1275 | // "/2/guide/insert_feedback/trend_abusive_feedback_key.json", 1276 | // hasUndoAction: false, 1277 | // prompt: "This trend is abusive or harmful", 1278 | // }, 1279 | // trend_abusive_or_harmful_feedback_key: { 1280 | // confirmation: "Thanks. Refresh this page to update these trends.", 1281 | // confirmationDisplayType: "BottomSheet", 1282 | // feedbackType: "SeeFewer", 1283 | // feedbackUrl: 1284 | // "/2/guide/insert_feedback/trend_abusive_or_harmful_feedback_key.json", 1285 | // hasUndoAction: false, 1286 | // prompt: "This trend is harmful or spammy", 1287 | // }, 1288 | // trend_duplicate_feedback_key: { 1289 | // confirmation: "Thanks. Refresh this page to update these trends.", 1290 | // confirmationDisplayType: "Inline", 1291 | // feedbackType: "SeeFewer", 1292 | // feedbackUrl: 1293 | // "/2/guide/insert_feedback/trend_duplicate_feedback_key.json", 1294 | // hasUndoAction: false, 1295 | // prompt: "This trend is a duplicate", 1296 | // }, 1297 | // trend_irrelevant_content_feedback_key: { 1298 | // confirmation: "Thanks. Refresh this page to update these trends.", 1299 | // confirmationDisplayType: "BottomSheet", 1300 | // feedbackType: "SeeFewer", 1301 | // feedbackUrl: 1302 | // "/2/guide/insert_feedback/trend_irrelevant_content_feedback_key.json", 1303 | // hasUndoAction: false, 1304 | // prompt: "The associated content is not relevant", 1305 | // }, 1306 | // trend_low_quality_feedback_key: { 1307 | // confirmation: "Thanks. Refresh this page to update these trends.", 1308 | // confirmationDisplayType: "Inline", 1309 | // feedbackType: "SeeFewer", 1310 | // feedbackUrl: 1311 | // "/2/guide/insert_feedback/trend_low_quality_feedback_key.json", 1312 | // hasUndoAction: false, 1313 | // prompt: "This trend is low quality", 1314 | // }, 1315 | // trend_not_interested_in_this_feedback_key: { 1316 | // confirmation: "Thanks. Refresh this page to update these trends.", 1317 | // confirmationDisplayType: "Inline", 1318 | // feedbackType: "SeeFewer", 1319 | // feedbackUrl: 1320 | // "/2/guide/insert_feedback/trend_not_interested_in_this_feedback_key.json", 1321 | // hasUndoAction: false, 1322 | // prompt: "Not interested in this", 1323 | // }, 1324 | // trend_spam_feedback_key: { 1325 | // confirmation: "Thanks. Refresh this page to update these trends.", 1326 | // confirmationDisplayType: "Inline", 1327 | // feedbackType: "SeeFewer", 1328 | // feedbackUrl: 1329 | // "/2/guide/insert_feedback/trend_spam_feedback_key.json", 1330 | // hasUndoAction: false, 1331 | // prompt: "This trend is spam", 1332 | // }, 1333 | // }, 1334 | }, 1335 | }, 1336 | }); 1337 | // } else { 1338 | // } 1339 | }); 1340 | 1341 | export default router; 1342 | 1343 | /* 1344 | { 1345 | "id": 287333780, 1346 | "id_str": "287333780", 1347 | "name": "real user", 1348 | "screen_name": "elonmusk", 1349 | "location": "NYC", 1350 | "description": "\"You Will Find Your People\" out now!!📚✨Author of \"How To Be Alone\"📘The Onion. Tinder Live. \"I Thought It Was Just Me\" podcast. Musician @itwasromance", 1351 | "url": "https://t.co/qDSfAc207A", 1352 | "entities": { 1353 | "url": { 1354 | "urls": [ 1355 | { 1356 | "url": "https://t.co/qDSfAc207A", 1357 | "expanded_url": "https://linktr.ee/hellolanemoore", 1358 | "display_url": "linktr.ee/hellolanemoore", 1359 | "indices": [ 1360 | 0, 1361 | 23 1362 | ] 1363 | } 1364 | ] 1365 | }, 1366 | "description": { 1367 | "urls": [] 1368 | } 1369 | }, 1370 | "protected": false, 1371 | "followers_count": 73061, 1372 | "fast_followers_count": 0, 1373 | "normal_followers_count": 73061, 1374 | "friends_count": 6586, 1375 | "listed_count": 652, 1376 | "created_at": "Sun Apr 24 19:51:11 +0000 2011", 1377 | "favourites_count": 48013, 1378 | "utc_offset": null, 1379 | "time_zone": null, 1380 | "geo_enabled": true, 1381 | "verified": false, 1382 | "statuses_count": 5092, 1383 | "media_count": 941, 1384 | "lang": null, 1385 | "contributors_enabled": false, 1386 | "is_translator": false, 1387 | "is_translation_enabled": false, 1388 | "profile_background_color": "131516", 1389 | "profile_background_image_url": "http://abs.twimg.com/images/themes/theme14/bg.gif", 1390 | "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme14/bg.gif", 1391 | "profile_background_tile": true, 1392 | "profile_image_url": "", 1393 | "profile_image_url_https": "", 1394 | "profile_banner_url": "https://pbs.twimg.com/profile_banners/287333780/1670952217", 1395 | "profile_link_color": "0AADAD", 1396 | "profile_sidebar_border_color": "FFFFFF", 1397 | "profile_sidebar_fill_color": "EFEFEF", 1398 | "profile_text_color": "333333", 1399 | "profile_use_background_image": true, 1400 | "has_extended_profile": true, 1401 | "default_profile": false, 1402 | "default_profile_image": true, 1403 | "pinned_tweet_ids": [ 1404 | 1597245073057775600 1405 | ], 1406 | "pinned_tweet_ids_str": [ 1407 | "1597245073057775616" 1408 | ], 1409 | "has_custom_timelines": false, 1410 | "can_dm": null, 1411 | "following": null, 1412 | "follow_request_sent": null, 1413 | "notifications": null, 1414 | "muting": null, 1415 | "blocking": null, 1416 | "blocked_by": null, 1417 | "want_retweets": null, 1418 | "advertiser_account_type": "none", 1419 | "advertiser_account_service_levels": [], 1420 | "business_profile_state": "none", 1421 | "translator_type": "none", 1422 | "withheld_in_countries": [], 1423 | "followed_by": null, 1424 | "ext_is_blue_verified": false, 1425 | "ext_profile_image_shape": "Circle", 1426 | "ext_has_nft_avatar": false, 1427 | "ext": { 1428 | "highlightedLabel": { 1429 | "r": { 1430 | "ok": {} 1431 | }, 1432 | "ttl": -1 1433 | }, 1434 | "hasNftAvatar": { 1435 | "r": { 1436 | "ok": false 1437 | }, 1438 | "ttl": -1 1439 | } 1440 | }, 1441 | "require_some_consent": false 1442 | } 1443 | */ 1444 | -------------------------------------------------------------------------------- /src/routes/gql/blue.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | export function TwitterBlueSignUpV2Query( 4 | req: express.Request, 5 | res: express.Response 6 | ) { 7 | return res.status(200).send({ 8 | data: { 9 | blue_marketing_page_config: { 10 | products: [ 11 | { 12 | product_category: "BlueVerified", 13 | title: "Blue", 14 | feature_buckets: { 15 | buckets: [ 16 | { 17 | title: "Prioritized rankings in conversations and search", 18 | learn_more_text: "", 19 | learn_more_description: "", 20 | learn_more_title: "", 21 | features: [], 22 | }, 23 | { 24 | title: 25 | "See approximately twice as many Tweets between ads in your For You and Following timelines.", 26 | learn_more_text: "", 27 | learn_more_description: "", 28 | learn_more_title: "", 29 | features: [], 30 | }, 31 | { 32 | title: "Add bold and italic text in your Tweets", 33 | learn_more_text: "", 34 | learn_more_description: "", 35 | learn_more_title: "", 36 | features: [], 37 | }, 38 | { 39 | title: "Post longer videos and 1080p video uploads", 40 | learn_more_text: "", 41 | learn_more_description: "", 42 | learn_more_title: "", 43 | features: [], 44 | }, 45 | { 46 | title: 47 | "All the existing Blue features, including Edit Tweet, Bookmark Folders and early access to new features", 48 | learn_more_text: "Learn more", 49 | learn_more_description: 50 | "Twitter Blue subscribers get early access to new features like these through Twitter Blue Labs.", 51 | learn_more_title: "Get early access", 52 | features: [ 53 | { 54 | title: "Longer Tweets", 55 | description: 56 | "Create Tweets, replies and Quotes up to 10,000 characters long.", 57 | icon: "Feather", 58 | }, 59 | { 60 | title: "Edit Tweet", 61 | description: 62 | "Edit a Tweet up to 5 times within 30 minutes.", 63 | icon: "WriteStroke", 64 | }, 65 | { 66 | title: "NFT Profile Pictures", 67 | description: 68 | "Show your personal flair and set your profile picture to an NFT you own.", 69 | icon: "AccountNft", 70 | }, 71 | ], 72 | }, 73 | ], 74 | }, 75 | }, 76 | ], 77 | card: { 78 | title: 79 | "Blue subscribers with a verified phone number will get a blue checkmark once approved.", 80 | description: "", 81 | image_url: 82 | "https://abs.twimg.com/responsive-web/client-web/verification-card-v2@3x.8ebee01a.png", 83 | badge: { text: "" }, 84 | }, 85 | feature_buckets: { 86 | title: "", 87 | buckets: [ 88 | { 89 | image_url: 90 | "https://abs.twimg.com/responsive-web/client-web/purple-present@3x.5f4d564a.png", 91 | title: "All the existing Blue features", 92 | description: 93 | "Edit Tweet, 1080p video uploads, Reader, custom navigation, Bookmark Folders, Top Articles and more.", 94 | learn_more_text: "", 95 | learn_more_title: "", 96 | learn_more_description: "", 97 | features: [], 98 | }, 99 | { 100 | badge: "NEW", 101 | image_url: 102 | "https://abs.twimg.com/responsive-web/client-web/upranked-replies-feature@3x.68f97c89.png", 103 | title: "Prioritized rankings in conversations and search", 104 | description: 105 | "Tweets from Twitter Blue subscribers will be prioritized in replies, mentions, and search — helping to fight scams and spam.", 106 | learn_more_text: "", 107 | learn_more_title: "", 108 | learn_more_description: "", 109 | features: [], 110 | }, 111 | { 112 | badge: "NEW", 113 | image_url: 114 | "https://abs.twimg.com/responsive-web/client-web/less-ads-feature@3x.98d5a999.png", 115 | title: 116 | "See approximately twice as many Tweets between ads in your For You and Following timelines.", 117 | description: 118 | "See approximately twice as many Tweets between ads in your For You and Following timelines.", 119 | learn_more_text: "", 120 | learn_more_title: "", 121 | learn_more_description: "", 122 | features: [], 123 | }, 124 | { 125 | badge: "NEW", 126 | image_url: 127 | "https://abs.twimg.com/responsive-web/client-web/longer-video-feature-v3@3x.6c6c531a.png", 128 | title: "Post longer videos and 1080p video uploads", 129 | description: 130 | "You’ll finally be able to post longer videos to Twitter.", 131 | learn_more_text: "", 132 | learn_more_title: "", 133 | learn_more_description: "", 134 | features: [], 135 | }, 136 | { 137 | image_url: 138 | "https://abs.twimg.com/responsive-web/client-web/early-access-feature@3x.9d1ba0a9.png", 139 | title: 140 | "All the existing Blue features, including Edit Tweet, Bookmark Folders and early access to new features", 141 | description: 142 | "Get early access to select new features with Twitter Blue Labs.", 143 | learn_more_text: "Learn more", 144 | learn_more_title: "Get early access", 145 | learn_more_description: 146 | "Twitter Blue subscribers get early access to new features like these through Twitter Blue Labs.", 147 | features: [ 148 | { 149 | icon: "Feather", 150 | title: "Longer Tweets", 151 | description: 152 | "Create Tweets, replies and Quotes up to 10,000 characters long.", 153 | }, 154 | { 155 | icon: "WriteStroke", 156 | title: "Edit Tweet", 157 | description: "Edit a Tweet up to 5 times within 30 minutes.", 158 | }, 159 | { 160 | icon: "AccountNft", 161 | title: "NFT Profile Pictures", 162 | description: 163 | "Show your personal flair and set your profile picture to an NFT you own.", 164 | }, 165 | ], 166 | }, 167 | ], 168 | }, 169 | subscribe_button: { 170 | detail_text: "Limited time offer: {{PRICE}}/{{PERIOD}}", 171 | disclaimer_text: 172 | "By subscribing, you agree to our @#!. Subscriptions auto-renew until canceled, as described in the Terms. Cancel anytime. A verified phone number is required to subscribe. If you've subscribed on another platform, manage your subscription through that platform.", 173 | disclaimer_url: "https://legal.twitter.com/purchaser-terms", 174 | disclaimer_url_text: "Purchaser Terms of Service", 175 | }, 176 | }, 177 | }, 178 | }); 179 | } 180 | 181 | export function useRelayDelegateDataQuery( 182 | req: express.Request, 183 | res: express.Response 184 | ) { 185 | return res.status(200).send({ data: { viewer: {} } }); 186 | } 187 | -------------------------------------------------------------------------------- /src/routes/gql/dm.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | export function DMPinnedInboxQuery( 4 | req: express.Request, 5 | res: express.Response 6 | ) { 7 | return res.status(200).send({ 8 | data: { labeled_conversation_slice: { items: [], slice_info: {} } }, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /src/routes/gql/graphQlHandler.ts: -------------------------------------------------------------------------------- 1 | import express, { Router } from "express"; 2 | import { log, warn } from "../../util/logging"; 3 | import { searchDirForTsFiles } from "../../util/routeUtil"; 4 | 5 | const router = express.Router(); 6 | 7 | const files = searchDirForTsFiles("src/routes/gql"); 8 | const funcs = [] as ((req: express.Request, res: express.Response) => any)[]; 9 | (async () => { 10 | for (const pathUnmodified of files) { 11 | const path = pathUnmodified.replace("src/routes/gql", "."); 12 | if (pathUnmodified === "src/routes/gql/graphQlHandler.ts") continue; 13 | funcs.push(...(Object.values(await import(path)) as any)); 14 | } 15 | log( 16 | `The following GraphQL routes successfully initialized: ${funcs 17 | .map((fn) => fn.name) 18 | .join(", ")}` 19 | ); 20 | })(); 21 | 22 | router.use("*", async (req, res) => { 23 | const query = req.originalUrl.split("/").at(-1)!.split("?")[0]; 24 | log(`GraphQL query sent to ${query}!`); 25 | for (const pathUnmodified of files) { 26 | if (pathUnmodified === "src/routes/gql/graphQlHandler.ts") continue; 27 | for (const fn of funcs) { 28 | if (fn.name === query) { 29 | await fn(req, res); 30 | return; 31 | } 32 | } 33 | } 34 | warn(`GraphQL query sent to ${query} failed!`); 35 | return res 36 | .status(200) 37 | .send({ 38 | msg: "This GraphQL endpoint has not yet been implemented. Pray to God that this stub is enough, and submit an issue on GitHub.", 39 | }); 40 | }); 41 | 42 | export default router; 43 | -------------------------------------------------------------------------------- /src/routes/gql/tweet.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { verify } from "jsonwebtoken"; 3 | import { requireAuth } from "../../middleware/auth"; 4 | import Tweet from "../../models/Tweet"; 5 | import User from "../../models/User"; 6 | import { 7 | ICreateTweetBody, 8 | IGenericFeatures, 9 | IJwtDecoded, 10 | } from "../../types/graphql"; 11 | import { IUser } from "../../types/guide"; 12 | import { formatDate } from "../../util/formatDate"; 13 | import { log } from "../../util/logging"; 14 | import { randInt } from "../../util/randUtil"; 15 | import { IUserMention } from "./user"; 16 | import { addNotification } from "../../util/notifications"; 17 | 18 | interface IUserTweetsVariables { 19 | userId: string; 20 | count: number; 21 | includePromotedContent: boolean; 22 | withQuickPromoteEligibilityTweetFields: boolean; 23 | withVoice: boolean; 24 | withV2Timeline: boolean; 25 | } 26 | 27 | interface IFavouriteTweetVariables { 28 | tweet_id: string; 29 | } 30 | 31 | export async function UserTweets(req: express.Request, res: express.Response) { 32 | const loggedInUser = req.cookies["jwt"] 33 | ? await User.findOne({ 34 | id_string: ( 35 | verify(req.cookies["jwt"], process.env.JWT_SECRET!) as IJwtDecoded 36 | ).id, 37 | }) 38 | : undefined; 39 | const features = JSON.parse( 40 | req.query.features!.toString() 41 | ) as IGenericFeatures; 42 | const variables = JSON.parse( 43 | req.query.variables!.toString() 44 | ) as IUserTweetsVariables; 45 | if (!features || !variables) 46 | return res.status(400).send({ msg: "Missing parameters" }); 47 | const user = await User.findOne({ id: variables.userId }); 48 | if (!user) return res.status(400).send({ msg: "User not found" }); 49 | const userTweets = []; 50 | const postedTweets = Array.from(user.posted_tweet_ids); 51 | for (const id of postedTweets) { 52 | const tweet = await Tweet.findOne({ id_str: id }); 53 | userTweets.push({ 54 | is_translatable: false, 55 | legacy: tweet 56 | ? Object.assign(tweet, { 57 | favorited: loggedInUser 58 | ? loggedInUser.liked_tweet_ids.includes(tweet.id_str || "") 59 | : false, 60 | }) 61 | : tweet, 62 | source: tweet?.source, 63 | unmention_data: {}, 64 | unmention_info: {}, 65 | views: { 66 | state: "Enabled", 67 | }, 68 | }); 69 | } 70 | return res.status(200).send({ 71 | data: { 72 | user: { 73 | result: { 74 | __typename: "User", 75 | timeline_v2: { 76 | timeline: { 77 | instructions: [ 78 | { 79 | type: "TimelineClearCache", 80 | }, 81 | { 82 | entries: [ 83 | ...userTweets.map((tweet, index) => { 84 | if (!tweet.legacy) return; 85 | return { 86 | content: { 87 | __typename: "TimelineTimelineItem", 88 | entryType: "TimelineTimelineItem", 89 | itemContent: { 90 | __typename: "TimelineTweet", 91 | itemType: "TimelineTweet", 92 | tweetDisplayType: "Tweet", 93 | tweet_results: { 94 | result: { 95 | __typename: "Tweet", 96 | core: { 97 | user_results: { 98 | result: { 99 | __typename: "User", 100 | affiliates_highlighted_label: 101 | user.ext?.highlightedLabel?.r?.ok, 102 | business_account: {}, 103 | id: user._id, 104 | is_blue_verified: 105 | user.ext_is_blue_verified, 106 | legacy: { 107 | ...(user as any)._doc, 108 | }, 109 | profile_image_shape: 110 | user.ext_profile_image_shape, 111 | rest_id: user.id, 112 | }, 113 | }, 114 | }, 115 | edit_perspective: { 116 | favorited: false, 117 | retweeted: false, 118 | }, 119 | is_translatable: false, 120 | legacy: tweet.legacy, 121 | quick_promote_eligibility: { 122 | eligibility: "IneligibleNotProfessional", 123 | }, 124 | rest_id: Number(tweet.legacy.id_str), 125 | source: tweet.source, 126 | unmention_data: {}, 127 | views: tweet.views, 128 | }, 129 | }, 130 | }, 131 | }, 132 | entryId: `tweet-${tweet.legacy.id_str}`, 133 | sortIndex: index.toString(), 134 | }; 135 | }), 136 | ], 137 | type: "TimelineAddEntries", 138 | }, 139 | ], 140 | responseObjects: { 141 | feedbackActions: [ 142 | { 143 | key: "589986573", 144 | value: { 145 | feedbackType: "Dismiss", 146 | feedbackUrl: 147 | "/1.1/onboarding/fatigue.json?flow_name=signup-persistent-nux&fatigue_group_name=PersistentNuxFatigueGroup&action_name=dismiss&scribe_name=dismiss&display_location=profile_best&served_time_secs=1682985304&injection_type=tile_carousel", 148 | hasUndoAction: false, 149 | prompt: "See less often", 150 | }, 151 | }, 152 | ], 153 | immediateReactions: [], 154 | }, 155 | }, 156 | }, 157 | }, 158 | }, 159 | }, 160 | }); 161 | } 162 | 163 | export async function HomeTimeline( 164 | req: express.Request, 165 | res: express.Response 166 | ) { 167 | const unauthorized = await requireAuth(req, res); 168 | if (unauthorized) return; 169 | const userId = req.cookies["jwt"] 170 | ? ((verify(req.cookies["jwt"], process.env.JWT_SECRET!) as IJwtDecoded) 171 | .id as number) 172 | : undefined; 173 | const loggedInUser = await User.findOne({ 174 | id_string: userId, 175 | }); 176 | if (!loggedInUser) return res.status(400).send({ msg: "Not authenticated" }); 177 | const userTweets = []; 178 | const postedTweets = await Tweet.find().limit(50); 179 | for (const tweet of postedTweets) { 180 | const user = await User.findOne({ id_string: tweet.user_id_str }); 181 | if (!user) return res.status(400).send({ msg: "User not found" }); 182 | userTweets.push({ 183 | is_translatable: false, 184 | legacy: Object.assign(tweet, { 185 | favorited: loggedInUser.liked_tweet_ids.includes(tweet.id_str || ""), 186 | }), 187 | unmention_data: {}, 188 | unmention_info: {}, 189 | views: tweet.ext_views, 190 | user: user, 191 | }); 192 | } 193 | return res.status(200).send({ 194 | data: { 195 | home: { 196 | home_timeline_urt: { 197 | instructions: [ 198 | { 199 | entries: [ 200 | ...userTweets.map((tweet, index) => { 201 | if (!tweet.legacy) return; 202 | return { 203 | content: { 204 | __typename: "TimelineTimelineItem", 205 | entryType: "TimelineTimelineItem", 206 | itemContent: { 207 | __typename: "TimelineTweet", 208 | itemType: "TimelineTweet", 209 | tweetDisplayType: "Tweet", 210 | tweet_results: { 211 | result: { 212 | __typename: "Tweet", 213 | core: { 214 | user_results: { 215 | result: { 216 | __typename: "User", 217 | affiliates_highlighted_label: 218 | tweet.user.ext?.highlightedLabel?.r?.ok, 219 | business_account: {}, 220 | id: tweet.user._id, 221 | is_blue_verified: 222 | tweet.user.ext_is_blue_verified, 223 | legacy: { 224 | ...(tweet.user as any)._doc, 225 | entities: { 226 | user_mentions: ( 227 | tweet.legacy.entities 228 | ?.user_mentions as IUserMention[] 229 | ).map((mention) => { 230 | return { 231 | a: true, 232 | }; 233 | }), 234 | }, 235 | }, 236 | profile_image_shape: 237 | tweet.user.ext_profile_image_shape, 238 | rest_id: tweet.user.id, 239 | }, 240 | }, 241 | }, 242 | edit_perspective: { 243 | favorited: false, 244 | retweeted: false, 245 | }, 246 | is_translatable: false, 247 | legacy: tweet.legacy, 248 | quick_promote_eligibility: { 249 | eligibility: "IneligibleNotProfessional", 250 | }, 251 | rest_id: Number(tweet.legacy.id_str), 252 | source: tweet.legacy.source, 253 | unmention_data: {}, 254 | views: tweet.views, 255 | }, 256 | }, 257 | }, 258 | }, 259 | entryId: `tweet-${tweet.legacy.id_str}`, 260 | sortIndex: index.toString(), 261 | }; 262 | }), 263 | ], 264 | type: "TimelineAddEntries", 265 | }, 266 | ], 267 | responseObjects: { 268 | feedbackActions: [ 269 | { 270 | key: "589986573", 271 | value: { 272 | feedbackType: "Dismiss", 273 | feedbackUrl: 274 | "/1.1/onboarding/fatigue.json?flow_name=signup-persistent-nux&fatigue_group_name=PersistentNuxFatigueGroup&action_name=dismiss&scribe_name=dismiss&display_location=profile_best&served_time_secs=1682985304&injection_type=tile_carousel", 275 | hasUndoAction: false, 276 | prompt: "See less often", 277 | }, 278 | }, 279 | ], 280 | immediateReactions: [], 281 | }, 282 | }, 283 | }, 284 | }, 285 | }); 286 | } 287 | 288 | export async function FavoriteTweet( 289 | req: express.Request, 290 | res: express.Response 291 | ) { 292 | const unauthorized = await requireAuth(req, res); 293 | if (unauthorized) return; 294 | const user = await User.findOne({ 295 | id_string: ( 296 | verify(req.cookies["jwt"], process.env.JWT_SECRET!) as IJwtDecoded 297 | ).id.toString(), 298 | }); 299 | if (!user) return res.status(400).send({ msg: "Not authenticated" }); 300 | const variables = req.body.variables as IFavouriteTweetVariables; 301 | const tweet = await Tweet.findOne({ id_str: variables.tweet_id }); 302 | if (!tweet) return res.status(400).send({ msg: "Tweet not found" }); 303 | if (!user.liked_tweet_ids.includes(variables.tweet_id)) { 304 | user.liked_tweet_ids.push(variables.tweet_id); 305 | tweet.favorite_count! += 1; 306 | await user.save(); 307 | await tweet.save(); 308 | } else { 309 | return res.status(400).send({ data: { favourte_tweet: "NOT DONE" } }); 310 | } 311 | if (tweet.user_id_str !== user.id_string) 312 | addNotification( 313 | "%1 liked your tweet!", 314 | "like", 315 | tweet.user_id_str!, 316 | user.id_string!, 317 | tweet.id_str! 318 | ); 319 | return res.status(200).send({ data: { favorite_tweet: "Done" } }); 320 | } 321 | 322 | export async function UnfavoriteTweet( 323 | req: express.Request, 324 | res: express.Response 325 | ) { 326 | const unauthorized = await requireAuth(req, res); 327 | if (unauthorized) return; 328 | const user = await User.findOne({ 329 | id_string: ( 330 | verify(req.cookies["jwt"], process.env.JWT_SECRET!) as IJwtDecoded 331 | ).id.toString(), 332 | }); 333 | if (!user) return res.status(400).send({ msg: "Not authenticated" }); 334 | const variables = req.body.variables as IFavouriteTweetVariables; 335 | const tweet = await Tweet.findOne({ id_str: variables.tweet_id }); 336 | if (!tweet) return res.status(400).send({ msg: "Tweet not found" }); 337 | if (user.liked_tweet_ids.includes(variables.tweet_id)) { 338 | user.liked_tweet_ids = user.liked_tweet_ids.filter( 339 | (id) => id !== variables.tweet_id 340 | ); 341 | tweet.favorite_count! -= 1; 342 | await user.save(); 343 | await tweet.save(); 344 | } else { 345 | return res.status(400).send({ data: { unfavorite_tweet: "NOT DONE" } }); 346 | } 347 | return res.status(200).send({ data: { unfavorite_tweet: "Done" } }); 348 | } 349 | 350 | export async function CreateTweet(req: express.Request, res: express.Response) { 351 | const unauthorized = await requireAuth(req, res); 352 | if (unauthorized) return; 353 | const body = req.body as ICreateTweetBody; 354 | const id = ( 355 | verify(req.cookies["jwt"], process.env.JWT_SECRET!) as IJwtDecoded 356 | ).id as number; 357 | const user = await User.findOne({ id }); 358 | if (!user) return res.status(400).send({ msg: "User not found" }); 359 | const user_mentions = [] as IUserMention[]; 360 | const matches = body.variables.tweet_text.matchAll(/@(\S+)/gm); 361 | for (const match of matches) { 362 | const withTag = match[0]; 363 | const withoutTag = match[1]; 364 | const user = await User.findOne({ screen_name: withoutTag }); 365 | user_mentions.push({ 366 | id_str: user?.id_string, 367 | name: user?.name, 368 | screen_name: user?.screen_name, 369 | indices: [match.index, withTag.length + (match.index || 0)], 370 | }); 371 | } 372 | const tweetId = randInt(12); 373 | const tweetData = { 374 | edit_perspective: { 375 | favorited: false, 376 | retweeted: false, 377 | }, 378 | is_translatable: false, 379 | legacy: { 380 | bookmark_count: 0, 381 | bookmarked: false, 382 | conversation_id_str: tweetId.toString(), 383 | created_at: formatDate(new Date()), 384 | display_text_range: [0, body.variables.tweet_text.length], 385 | entities: { 386 | hashtags: [], 387 | symbols: [], 388 | urls: [], 389 | user_mentions, 390 | }, 391 | favorite_count: 0, 392 | favorited: false, 393 | full_text: body.variables.tweet_text, 394 | id: tweetId, 395 | id_str: tweetId.toString(), 396 | is_quote_status: false, 397 | lang: "en", 398 | quote_count: 0, 399 | reply_count: 0, 400 | retweet_count: 0, 401 | retweeted: false, 402 | user_id_str: user.id_string, 403 | }, 404 | source: 405 | 'Blue Web App', 406 | unmention_data: {}, 407 | unmention_info: {}, 408 | views: { 409 | state: "Enabled", 410 | }, 411 | }; 412 | const tweet = new Tweet({ 413 | bookmark_count: tweetData.legacy.bookmark_count, 414 | bookmarked: tweetData.legacy.bookmarked, 415 | conversation_id_str: tweetData.legacy.conversation_id_str, 416 | created_at: tweetData.legacy.created_at, 417 | display_text_range: tweetData.legacy.display_text_range, 418 | entities: { 419 | hashtags: tweetData.legacy.entities.hashtags, 420 | symbols: tweetData.legacy.entities.symbols, 421 | urls: tweetData.legacy.entities.urls, 422 | user_mentions, 423 | }, 424 | favorite_count: tweetData.legacy.favorite_count, 425 | favorited: tweetData.legacy.favorited, 426 | full_text: tweetData.legacy.full_text, 427 | id_str: tweetData.legacy.id_str, 428 | is_quote_status: tweetData.legacy.is_quote_status, 429 | lang: tweetData.legacy.lang, 430 | quote_count: tweetData.legacy.quote_count, 431 | reply_count: tweetData.legacy.reply_count, 432 | retweet_count: tweetData.legacy.retweet_count, 433 | retweeted: tweetData.legacy.retweeted, 434 | user_id_str: tweetData.legacy.user_id_str, 435 | }); 436 | await tweet.save(); 437 | user.posted_tweet_ids.push(tweetId.toString()); 438 | await user.save(); 439 | log(`${user.name} (@${user.screen_name}) just posted a tweet:`); 440 | log(`"${tweet.full_text}"`); 441 | // addNotification( 442 | // "%1 just posted a tweet!", 443 | // "reply", 444 | // user.id_string!, 445 | // user.id_string!, 446 | // tweet.id_str! 447 | // ); 448 | return res.status(200).send({ 449 | // data: { 450 | // create_tweet: { 451 | // tweet_results: { 452 | // result: { 453 | // core: { 454 | // user_results: { 455 | // result: { 456 | // __typename: "User", 457 | // affiliates_highlighted_label: 458 | // user.ext?.highlightedLabel?.r?.ok, 459 | // business_account: {}, 460 | // id: user._id, 461 | // is_blue_verified: user.ext_is_blue_verified, 462 | // legacy: { 463 | // created_at: user.created_at, 464 | // default_profile: user.default_profile, 465 | // default_profile_image: user.default_profile_image, 466 | // description: user.description, 467 | // entities: user.entities, 468 | // fast_followers_count: user.fast_followers_count, 469 | // favourites_count: user.favourites_count, 470 | // followers_count: user.followers_count, 471 | // friends_count: user.friends_count, 472 | // has_custom_timelines: user.has_custom_timelines, 473 | // is_translator: user.is_translator, 474 | // listed_count: user.listed_count, 475 | // location: user.location, 476 | // media_count: user.media_count, 477 | // name: user.name, 478 | // normal_followers_count: user.normal_followers_count, 479 | // pinned_tweet_ids_str: user.pinned_tweet_ids_str, 480 | // possibly_sensitive: false, 481 | // profile_image_url_https: user.profile_image_url_https, 482 | // profile_interstitial_type: "", 483 | // screen_name: user.screen_name, 484 | // statuses_count: user.statuses_count, 485 | // translator_type: user.translator_type, 486 | // verified: user.verified, 487 | // withheld_in_countries: user.withheld_in_countries, 488 | // }, 489 | // profile_image_shape: user.ext_profile_image_shape, 490 | // rest_id: user.id, 491 | // }, 492 | // }, 493 | // }, 494 | // ...tweetData, 495 | // }, 496 | // }, 497 | // }, 498 | // }, 499 | data: { 500 | create_tweet: { 501 | tweet_results: { 502 | result: { 503 | core: { 504 | user_results: { 505 | result: { 506 | __typename: "User", 507 | affiliates_highlighted_label: 508 | user.ext?.highlightedLabel?.r?.ok, 509 | business_account: {}, 510 | id: user._id, 511 | is_blue_verified: user.ext_is_blue_verified, 512 | legacy: { 513 | created_at: user.created_at, 514 | default_profile: user.default_profile, 515 | default_profile_image: user.default_profile_image, 516 | description: user.description, 517 | entities: user.entities, 518 | fast_followers_count: user.fast_followers_count, 519 | favourites_count: user.favourites_count, 520 | followers_count: user.followers_count, 521 | friends_count: user.friends_count, 522 | has_custom_timelines: user.has_custom_timelines, 523 | is_translator: user.is_translator, 524 | listed_count: user.listed_count, 525 | location: user.location, 526 | media_count: user.media_count, 527 | name: user.name, 528 | normal_followers_count: user.normal_followers_count, 529 | pinned_tweet_ids_str: user.pinned_tweet_ids_str, 530 | possibly_sensitive: false, 531 | profile_image_url_https: user.profile_image_url_https, 532 | profile_interstitial_type: "", 533 | screen_name: user.screen_name, 534 | statuses_count: user.statuses_count, 535 | translator_type: user.translator_type, 536 | verified: user.verified, 537 | withheld_in_countries: user.withheld_in_countries, 538 | }, 539 | profile_image_shape: user.ext_profile_image_shape, 540 | rest_id: user.id, 541 | }, 542 | }, 543 | }, 544 | edit_control: { 545 | edit_tweet_ids: [tweetId.toString()], 546 | editable_until_msecs: "1682984834000", 547 | edits_remaining: "5", 548 | is_edit_eligible: true, 549 | }, 550 | edit_perspective: { 551 | favorited: false, 552 | retweeted: false, 553 | }, 554 | is_translatable: false, 555 | legacy: { 556 | bookmark_count: 0, 557 | bookmarked: false, 558 | conversation_id_str: tweetId.toString(), 559 | created_at: formatDate(new Date()), 560 | display_text_range: [0, 68], 561 | entities: { 562 | hashtags: [], 563 | symbols: [], 564 | urls: [], 565 | user_mentions: ( 566 | tweet.entities!.user_mentions as Array> 567 | ).map((mention) => mention[0]), 568 | }, 569 | favorite_count: 0, 570 | favorited: false, 571 | full_text: body.variables.tweet_text, 572 | id_str: tweetId.toString(), 573 | is_quote_status: false, 574 | lang: "en", 575 | quote_count: 0, 576 | reply_count: 0, 577 | retweet_count: 0, 578 | retweeted: false, 579 | user_id_str: user.id_string, 580 | }, 581 | rest_id: tweetId.toString(), 582 | source: 583 | 'Blue Web App', 584 | unmention_data: {}, 585 | unmention_info: {}, 586 | views: { 587 | state: "Disabled", 588 | }, 589 | }, 590 | }, 591 | }, 592 | }, 593 | }); 594 | } 595 | -------------------------------------------------------------------------------- /src/routes/gql/user.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { verify } from "jsonwebtoken"; 3 | import { requireAuth } from "../../middleware/auth"; 4 | import Tweet from "../../models/Tweet"; 5 | import User from "../../models/User"; 6 | import { 7 | IGenericFeatures, 8 | IUserByScreenNameVariables, 9 | IUserByRestIdVariables, 10 | IJwtDecoded, 11 | IProfileSpotlightsQueryVariables, 12 | ICreateTweetBody, 13 | } from "../../types/graphql"; 14 | import { formatDate } from "../../util/formatDate"; 15 | import { log } from "../../util/logging"; 16 | import { randInt } from "../../util/randUtil"; 17 | 18 | export interface IUserMention { 19 | id_str?: string; 20 | name?: string; 21 | screen_name?: string; 22 | indices?: [number?, number?]; 23 | } 24 | 25 | // router.get("/P7qs2Sf7vu1LDKbzDW9FSA/UserMedia", async (req, res) => { 26 | // const features = JSON.parse( 27 | // req.query.features!.toString() 28 | // ) as IGenericFeatures; 29 | // const variables = JSON.parse( 30 | // req.query.variables!.toString() 31 | // ) as IUserByRestIdVariables; 32 | // if (!features || !variables) 33 | // return res.status(400).send({ msg: "Missing parameters" }); 34 | // const id_string = variables.userId; 35 | // if (!id_string || id_string === "undefined") 36 | // return res.status(400).send({ msg: "Error occurred extracting twid" }); 37 | // const user = await User.findOne({ 38 | // id_string, 39 | // }); 40 | // if (!user) return res.status(200).send({ data: {} }); 41 | // return res.status(200).send({ 42 | // data: { 43 | // user: { 44 | // result: { 45 | // __typename: "User", 46 | // id: user._id, 47 | // rest_id: user.id, 48 | // affiliates_highlighted_label: { 49 | // label: { 50 | // url: { 51 | // url: "https://twitter.com/notnullptr", 52 | // urlType: "DeepLink", 53 | // }, 54 | // badge: { 55 | // url: "https://api.twitter.com/img/nullptr.jpg", 56 | // }, 57 | // description: "nullptr", 58 | // userLabelType: "BusinessLabel", 59 | // userLabelDisplayType: "Badge", 60 | // }, 61 | // }, 62 | // is_blue_verified: true, 63 | // profile_image_shape: "Circle", 64 | // legacy: user, 65 | // business_account: {}, 66 | // }, 67 | // }, 68 | // }, 69 | // }); 70 | // }); 71 | 72 | export async function UserByScreenName( 73 | req: express.Request, 74 | res: express.Response 75 | ) { 76 | const features = JSON.parse( 77 | req.query.features!.toString() 78 | ) as IGenericFeatures; 79 | const variables = JSON.parse( 80 | req.query.variables!.toString() 81 | ) as IUserByScreenNameVariables; 82 | if (!features || !variables) 83 | return res.status(400).send({ msg: "Missing parameters" }); 84 | const screenName = variables.screen_name; 85 | if (!screenName || screenName === "undefined") 86 | return res.status(400).send({ msg: "Error occurred extracting twid" }); 87 | const user = await User.findOne({ 88 | screen_name: { 89 | $regex: new RegExp(screenName, "i"), 90 | }, 91 | }); 92 | if (!user) return res.status(200).send({ data: {} }); 93 | return res.status(200).send({ 94 | data: { 95 | user: { 96 | result: { 97 | __typename: "User", 98 | affiliates_highlighted_label: user.ext?.highlightedLabel?.r?.ok, 99 | business_account: {}, 100 | id: user._id, 101 | is_blue_verified: user.ext_is_blue_verified, 102 | legacy: { 103 | created_at: user.created_at, 104 | default_profile: user.default_profile, 105 | default_profile_image: user.default_profile_image, 106 | description: user.description, 107 | entities: user.entities, 108 | fast_followers_count: user.fast_followers_count, 109 | favourites_count: user.favourites_count, 110 | followers_count: user.followers_count, 111 | friends_count: user.friends_count, 112 | has_custom_timelines: user.has_custom_timelines, 113 | is_translator: user.is_translator, 114 | listed_count: user.listed_count, 115 | location: user.location, 116 | media_count: user.media_count, 117 | name: user.name, 118 | normal_followers_count: user.normal_followers_count, 119 | pinned_tweet_ids_str: user.pinned_tweet_ids_str, 120 | possibly_sensitive: false, 121 | profile_image_url_https: user.profile_image_url_https, 122 | profile_interstitial_type: "", 123 | screen_name: user.screen_name, 124 | statuses_count: user.statuses_count, 125 | translator_type: user.translator_type, 126 | verified: user.verified, 127 | verified_type: user.verified_type || "None", 128 | withheld_in_countries: user.withheld_in_countries, 129 | }, 130 | profile_image_shape: user.ext_profile_image_shape, 131 | rest_id: user.id, 132 | }, 133 | }, 134 | }, 135 | }); 136 | } 137 | 138 | export async function UserByRestId( 139 | req: express.Request, 140 | res: express.Response 141 | ) { 142 | const features = JSON.parse( 143 | req.query.features!.toString() 144 | ) as IGenericFeatures; 145 | const variables = JSON.parse( 146 | req.query.variables!.toString() 147 | ) as IUserByRestIdVariables; 148 | if (!features || !variables) 149 | return res.status(400).send({ msg: "Missing parameters" }); 150 | const id_string = variables.userId; 151 | if (!id_string || id_string === "undefined") 152 | return res.status(400).send({ msg: "Error occurred extracting twid" }); 153 | const user = await User.findOne({ 154 | id_string: { 155 | $regex: new RegExp(id_string, "i"), 156 | }, 157 | }); 158 | if (!user) return res.status(200).send({ data: {} }); 159 | return res.status(200).send({ 160 | data: { 161 | user: { 162 | result: { 163 | __typename: "User", 164 | affiliates_highlighted_label: user.ext?.highlightedLabel?.r?.ok, 165 | business_account: {}, 166 | id: user._id, 167 | is_blue_verified: user.ext_is_blue_verified, 168 | legacy: { 169 | created_at: user.created_at, 170 | default_profile: user.default_profile, 171 | default_profile_image: user.default_profile_image, 172 | description: user.description, 173 | entities: user.entities, 174 | fast_followers_count: user.fast_followers_count, 175 | favourites_count: user.favourites_count, 176 | followers_count: user.followers_count, 177 | friends_count: user.friends_count, 178 | has_custom_timelines: user.has_custom_timelines, 179 | is_translator: user.is_translator, 180 | listed_count: user.listed_count, 181 | location: user.location, 182 | media_count: user.media_count, 183 | name: user.name, 184 | normal_followers_count: user.normal_followers_count, 185 | pinned_tweet_ids_str: user.pinned_tweet_ids_str, 186 | possibly_sensitive: false, 187 | profile_image_url_https: user.profile_image_url_https, 188 | profile_interstitial_type: "", 189 | screen_name: user.screen_name, 190 | statuses_count: user.statuses_count, 191 | translator_type: user.translator_type, 192 | verified: user.verified, 193 | withheld_in_countries: user.withheld_in_countries, 194 | }, 195 | profile_image_shape: user.ext_profile_image_shape, 196 | rest_id: user.id, 197 | }, 198 | }, 199 | }, 200 | }); 201 | } 202 | 203 | export async function ProfileSpotlightsQuery( 204 | req: express.Request, 205 | res: express.Response 206 | ) { 207 | const variables = JSON.parse( 208 | req.query.variables!.toString() 209 | ) as IProfileSpotlightsQueryVariables; 210 | if (!variables) return res.status(400).send({ msg: "Missing parameters" }); 211 | const screenName = variables.screen_name; 212 | if (!screenName || screenName === "undefined") 213 | return res.status(400).send({ msg: "Error occurred extracting twid" }); 214 | const user = await User.findOne({ 215 | screen_name: screenName, 216 | }); 217 | if (!user) return res.status(400).send({ msg: "User not found" }); 218 | // if (user.id !== jwtParams.id) 219 | // return res.status(401).send({ msg: "Unauthorized" }); 220 | return res.status(200).send({ 221 | data: { 222 | user_result_by_screen_name: { 223 | result: { 224 | __typename: "User", 225 | id: user._id, 226 | legacy: { 227 | blocked_by: false, 228 | blocking: false, 229 | followed_by: false, 230 | following: false, 231 | name: user.name, 232 | protected: false, 233 | screen_name: user.screen_name, 234 | }, 235 | profilemodules: { 236 | v1: [], 237 | }, 238 | rest_id: user._id, 239 | }, 240 | }, 241 | }, 242 | }); 243 | } 244 | 245 | export async function AuthenticatedUserTFLists( 246 | req: express.Request, 247 | res: express.Response 248 | ) { 249 | return res.status(200).send({ 250 | data: { 251 | authenticated_user_trusted_friends_lists: [ 252 | { 253 | id: "VHJ1c3RlZEZyaWVuZHNMaXN0OjE2NTMwNTQ0MjUzNzg4MjgyODg=", 254 | rest_id: "1653054425378828288", 255 | name: "Twitter Circle", 256 | member_count: 0, 257 | }, 258 | ], 259 | }, 260 | }); 261 | } 262 | -------------------------------------------------------------------------------- /src/routes/gql/viewer.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import User from "../../models/User"; 3 | 4 | export async function Viewer(req: express.Request, res: express.Response) { 5 | // if (!req.cookies.twt_id) 6 | // return res.status(400).send({ msg: "No twid detected" }); 7 | const id = (req.cookies.twt_id as string | undefined)?.split("=")[1] as 8 | | string 9 | | undefined; 10 | if (!id || id === "undefined") 11 | return res.status(200).send({ 12 | errors: [ 13 | { 14 | message: "Authentication: Not authenticated", 15 | locations: [{ line: 11, column: 5 }], 16 | path: ["viewer", "user_results"], 17 | extensions: { 18 | name: "AuthenticationError", 19 | source: "Client", 20 | retry_after: 0, 21 | code: 32, 22 | kind: "Permissions", 23 | tracing: { trace_id: "79b1d650b7edea88" }, 24 | }, 25 | code: 32, 26 | kind: "Permissions", 27 | name: "AuthenticationError", 28 | source: "Client", 29 | retry_after: 0, 30 | tracing: { trace_id: "79b1d650b7edea88" }, 31 | }, 32 | { 33 | message: 34 | "Authorization: Denied by access control: unspecified reason", 35 | locations: [{ line: 24, column: 5 }], 36 | path: ["viewer", "educationFlags"], 37 | extensions: { 38 | name: "AuthorizationError", 39 | source: "Client", 40 | code: 37, 41 | kind: "Permissions", 42 | tracing: { trace_id: "79b1d650b7edea88" }, 43 | }, 44 | code: 37, 45 | kind: "Permissions", 46 | name: "AuthorizationError", 47 | source: "Client", 48 | tracing: { trace_id: "79b1d650b7edea88" }, 49 | }, 50 | { 51 | message: 52 | "Authorization: Denied by access control: unspecified reason", 53 | locations: [{ line: 29, column: 5 }], 54 | path: ["viewer", "is_active_creator"], 55 | extensions: { 56 | name: "AuthorizationError", 57 | source: "Client", 58 | code: 37, 59 | kind: "Permissions", 60 | tracing: { trace_id: "79b1d650b7edea88" }, 61 | }, 62 | code: 37, 63 | kind: "Permissions", 64 | name: "AuthorizationError", 65 | source: "Client", 66 | tracing: { trace_id: "79b1d650b7edea88" }, 67 | }, 68 | { 69 | message: 70 | "Authorization: Denied by access control: unspecified reason", 71 | locations: [{ line: 30, column: 5 }], 72 | path: ["viewer", "super_followers_count"], 73 | extensions: { 74 | name: "AuthorizationError", 75 | source: "Client", 76 | code: 37, 77 | kind: "Permissions", 78 | tracing: { trace_id: "79b1d650b7edea88" }, 79 | }, 80 | code: 37, 81 | kind: "Permissions", 82 | name: "AuthorizationError", 83 | source: "Client", 84 | tracing: { trace_id: "79b1d650b7edea88" }, 85 | }, 86 | ], 87 | data: { 88 | viewer: { 89 | has_community_memberships: false, 90 | create_community_action_result: { 91 | __typename: "CommunityCreateActionUnavailable", 92 | reason: "Unavailable", 93 | message: "Action is not available", 94 | }, 95 | user_features: [ 96 | { feature: "mediatool_studio_library", enabled: false }, 97 | ], 98 | is_tfe_restricted_session: false, 99 | }, 100 | }, 101 | }); 102 | const user = await User.findOne({ 103 | id, 104 | }); 105 | 106 | if (!user) return res.status(400).send({ msg: "User not found" }); 107 | return res.status(200).send({ 108 | data: { 109 | is_super_follow_subscriber: false, 110 | viewer: { 111 | create_community_action_result: { 112 | __typename: "CommunityCreateActionUnavailable", 113 | message: "Action is not available", 114 | reason: "Unavailable", 115 | }, 116 | educationFlags: [ 117 | { 118 | flag: "AltTextEducation", 119 | timestamp: 1666377026799, 120 | }, 121 | { 122 | flag: "ChangeConversationControlsEducation", 123 | timestamp: 1677318869102, 124 | }, 125 | { 126 | flag: "CommunitiesEducationComposerControls", 127 | timestamp: 1662134726025, 128 | }, 129 | { 130 | flag: "BirdwatchRatingFormDataPrivacyNotice", 131 | timestamp: 1674458804696, 132 | }, 133 | { 134 | flag: "PersistentConversationControlsEducation", 135 | timestamp: 1667330271495, 136 | }, 137 | { 138 | flag: "VerifiedAvatarEducation", 139 | timestamp: 1668110749446, 140 | }, 141 | { 142 | flag: "PinnedConversationsEducation", 143 | timestamp: 1663689642332, 144 | }, 145 | { 146 | flag: "TrustedFriendsEducationFlag", 147 | timestamp: 1662134726021, 148 | }, 149 | ], 150 | has_community_memberships: false, 151 | is_active_creator: false, 152 | is_tfe_restricted_session: false, 153 | super_followers_count: 0, 154 | user_features: [ 155 | { 156 | enabled: false, 157 | feature: "mediatool_studio_library", 158 | }, 159 | ], 160 | user_results: { 161 | result: { 162 | __typename: "User", 163 | affiliates_highlighted_label: {}, 164 | has_graduated_access: true, 165 | id: user._id, 166 | is_blue_verified: false, 167 | is_profile_translatable: false, 168 | // legacy: { 169 | // can_dm: true, 170 | // can_media_tag: true, 171 | // created_at: "Mon Jan 17 11:43:44 +0000 2022", 172 | // default_profile: true, 173 | // default_profile_image: false, 174 | // description: 175 | // "chronically online twitter looks fun, wish i was there :/", 176 | // entities: { 177 | // description: { 178 | // urls: [], 179 | // }, 180 | // }, 181 | // fast_followers_count: 0, 182 | // favourites_count: 876, 183 | // followers_count: 22, 184 | // friends_count: 135, 185 | // has_custom_timelines: true, 186 | // is_translator: false, 187 | // listed_count: 1, 188 | // location: "right behind you spy tf2 HAHAH", 189 | // media_count: 35, 190 | // name: "madsᐸ3", 191 | // needs_phone_verification: false, 192 | // normal_followers_count: 22, 193 | // pinned_tweet_ids_str: [], 194 | // possibly_sensitive: false, 195 | // profile_image_url_https: 196 | // "https://pbs.twimg.com/profile_images/1562613150863736832/3TrYlu0f_normal.jpg", 197 | // profile_interstitial_type: "", 198 | // screen_name: "madsthecatgirl", 199 | // statuses_count: 415, 200 | // translator_type: "none", 201 | // verified: false, 202 | // want_retweets: false, 203 | // withheld_in_countries: [], 204 | // }, 205 | legacy: { 206 | ...(user as any)._doc, 207 | }, 208 | legacy_extended_profile: { 209 | birthdate: { 210 | day: 7, 211 | month: 1, 212 | visibility: "Self", 213 | year: 1999, 214 | year_visibility: "Self", 215 | }, 216 | }, 217 | profile_image_shape: "Circle", 218 | rest_id: user.id, 219 | super_follows_application_status: "NotStarted", 220 | }, 221 | }, 222 | }, 223 | }, 224 | }); 225 | } 226 | -------------------------------------------------------------------------------- /src/routes/other/arkose/index.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { rootPath } from "get-root-path"; 3 | 4 | const router = express.Router(); 5 | 6 | router.get("/rtig/image", (req, res) => { 7 | return res.sendFile(rootPath + "/img/captcha.png"); 8 | }); 9 | 10 | router.post("/fc/a", (req, res) => { 11 | return res.status(200).send({ logged: true }); 12 | }); 13 | 14 | router.post("/fc/gfct", (req, res) => { 15 | return res.status(200).send({ 16 | session_token: "107175ab73d501a82.7403616905", 17 | challengeID: "395644e638522fc68.4045705105", 18 | challengeURL: 19 | "https://client-api.arkoselabs.com/fc/assets/match-game-ui/0.33.0/standard/index.html", 20 | audio_challenge_urls: null, 21 | audio_game_rate_limited: null, 22 | sec: 30, 23 | end_url: null, 24 | game_data: { 25 | display_fc_welldone: true, 26 | final_challenge_text: "", 27 | customGUI: { 28 | example_images: { 29 | key: "https://client-api.arkoselabs.com/cdn/fc/assets/game4failureexamples/3d_rollball/3D_Rollball_animated_key_01.png", 30 | answer: 31 | "https://client-api.arkoselabs.com/cdn/fc/assets/game4failureexamples/3d_rollball/3D_Rollball_animated_answer_01.png", 32 | }, 33 | _challenge_imgs: [ 34 | "https://client-api.arkoselabs.com/rtig/image?challenge=0&sessionToken=107175ab73d501a82.7403616905&gameToken=395644e638522fc68.4045705105", 35 | "https://client-api.arkoselabs.com/rtig/image?challenge=1&sessionToken=107175ab73d501a82.7403616905&gameToken=395644e638522fc68.4045705105", 36 | "https://client-api.arkoselabs.com/rtig/image?challenge=2&sessionToken=107175ab73d501a82.7403616905&gameToken=395644e638522fc68.4045705105", 37 | ], 38 | _final_chal_lang_key: "touch_done_info_colour", 39 | _disableFinalChalAfterSecChal: 1, 40 | fc2_rotate_stroke: 0, 41 | _guiTaper: 0, 42 | _meta_bg_colour: "transparent", 43 | _guiBGColorMain: "#ffffff", 44 | fc2_button_small: 1, 45 | _guiInfoText: "#000000", 46 | _guiTextColor: "#000000", 47 | _landingTextColor: "#000000", 48 | _guiNoShadow: 1, 49 | _guiProgressTextColor: "#000000", 50 | _guiTheme: 3, 51 | fc2_button_taper: 0, 52 | fc2_text_bg: "#000000", 53 | fc2_meta_logo_y: 60, 54 | _guiColorMain: "#ffffff", 55 | fc2_meta_logo_x: 1, 56 | _meta_icon_colour: "#555555", 57 | _guiNoInfoBG: 1, 58 | fc2_stroke_colour: "#000000", 59 | fc2_button_border_thickness: 1, 60 | fc2_rotate_bg: 0, 61 | _guiNoOutline: 1, 62 | _meta_logo: "/fc/assets/graphics/ecbase/Verification.svg", 63 | fc2_stroke_size: 1, 64 | _intro_game_bg: 0, 65 | fc2_button_font_size: 17, 66 | _guiColorSecondary: "#ffffff", 67 | fc2_welldone_image_y: 73, 68 | fc2_welldone_image_x: 98, 69 | fc2_button_text_colour: "#000000", 70 | _guiBGColorSecondary: "#ffffff", 71 | _meta_theme: 3, 72 | fc2_meta_text_y: 120, 73 | _guiLoaderColor: "#000000", 74 | fc2_well_done_image: "/fc/assets/graphics/ecbase/StraightTickGrey.svg", 75 | _guiMainFont: "Chirp-Regular", 76 | watermark_not_for_public: 0, 77 | _final_ball: null, 78 | _app_bg: 79 | "https://client-api.arkoselabs.com/cdn/fc/assets/graphics/funcaptcha/004/white.png", 80 | _end_banner: 81 | "https://client-api.arkoselabs.com/cdn/fc/assets/graphics/funcaptcha/general/fc_meta_solve_bg.jpg", 82 | _progress_prop: { bgColor: "#f7f7f7" }, 83 | embedded_session_id_enabled: 1, 84 | audio_disabled: false, 85 | custom_font: [ 86 | { 87 | family: "Chirp-Regular", 88 | filePath: "/assets/graphics/twitter/Chirp-Regular_1647983315357", 89 | formats: ["woff"], 90 | }, 91 | { 92 | family: "Chirp-Bold", 93 | filePath: "/assets/graphics/twitter/Chirp-Bold_1647983335196", 94 | formats: ["woff"], 95 | }, 96 | { 97 | family: "Chirp-Display-Bold", 98 | filePath: 99 | "/assets/graphics/twitter/Chirp-Display-Bold_1647983330354", 100 | formats: ["woff"], 101 | }, 102 | { 103 | family: "Chirp-Heavy", 104 | filePath: "/assets/graphics/twitter/Chirp-Heavy_1647983302836", 105 | formats: ["woff"], 106 | }, 107 | { 108 | family: "Chirp-Light", 109 | filePath: "/assets/graphics/twitter/Chirp-Light_1647983323929", 110 | formats: ["woff"], 111 | }, 112 | { 113 | family: "Chirp-Heavy", 114 | filePath: "/assets/graphics/twitter/Chirp-Heavy_1647983302836", 115 | formats: ["woff"], 116 | }, 117 | ], 118 | }, 119 | waves: 3, 120 | instruction_string: "3d_rollball_animals", 121 | game_difficulty: 6, 122 | answer_width: 200, 123 | answer_height: 200, 124 | key_width: 125, 125 | key_height: 200, 126 | puzzle_name: "3d Rollball", 127 | feature_game4_at_availability: true, 128 | gameType: 4, 129 | styling: { 130 | audio_game: { 131 | app: { 132 | style: { 133 | width: "100%", 134 | "border-top-left-radius": "12px !important", 135 | "border-top-right-radius": "12px !important", 136 | height: "500px", 137 | }, 138 | }, 139 | checking: { 140 | children: { 141 | text: { 142 | style: { 143 | width: "100%", 144 | "font-size": "18px", 145 | "font-family": "Chirp-Heavy", 146 | "line-height": "21px", 147 | "margin-top": "22%", 148 | color: "#0F1419", 149 | }, 150 | }, 151 | }, 152 | style: {}, 153 | }, 154 | game: { 155 | children: { 156 | instructionText: { 157 | style: { 158 | "padding-top": "3px", 159 | margin: "0% 0% 0% 6%", 160 | color: "#536471", 161 | "font-weight": "400", 162 | width: "88%", 163 | "font-size": "15px", 164 | "line-height": "20px", 165 | "padding-right": "0%", 166 | "text-align": "center", 167 | }, 168 | }, 169 | styledAnswerForm: { 170 | style: { 171 | "flex-flow": "column", 172 | height: "100px", 173 | position: "initial", 174 | }, 175 | }, 176 | failText: { 177 | style: { 178 | margin: "6%", 179 | color: "#0F1419", 180 | "font-size": "20px", 181 | "font-family": "Chirp-Heavy", 182 | "line-height": "24px", 183 | "margin-top": "6%", 184 | "text-align": "center", 185 | }, 186 | }, 187 | badFormatText: { 188 | style: { 189 | margin: "6%", 190 | color: "#0F1419", 191 | "font-size": "20px", 192 | "font-family": "Chirp-Heavy", 193 | "line-height": "24px", 194 | "margin-top": "6%", 195 | "text-align": "center", 196 | }, 197 | }, 198 | rateLimitText: { style: { padding: "0px 32px" } }, 199 | style: {}, 200 | answerForm: { 201 | children: { 202 | answerInput: { 203 | style: { 204 | "background-color": "#fff", 205 | border: "1px solid #CFD9DE", 206 | "border-radius": "8px", 207 | padding: "11px 11px 15px 10px", 208 | margin: "34px 5px 37px 30px", 209 | color: "#000", 210 | "font-weight": "400", 211 | width: "88%", 212 | "font-size": "31px", 213 | "&::placeholder": { color: "#FFF", "font-size": "13px" }, 214 | "border-bottom": "1px solid #CFD9DE", 215 | height: "56px", 216 | }, 217 | }, 218 | style: { height: "70px" }, 219 | downloadLink: { style: {} }, 220 | doneButton: { 221 | style: { 222 | border: "0px solid #000", 223 | padding: "4% 13%", 224 | "-webkit-appearance": "none", 225 | color: "white", 226 | "font-weight": "700", 227 | "font-size": "14px", 228 | "line-height": "0px", 229 | "background-color": "rgb(66, 66, 66)", 230 | "border-radius": "100px", 231 | "margin-left": "6%", 232 | "&:focus": { 233 | outline: "2px solid red", 234 | "outline-offset": "2px", 235 | }, 236 | width: "88%", 237 | "&:hover": { opacity: "80%" }, 238 | height: "32px", 239 | }, 240 | }, 241 | styledPlayButton: { style: {} }, 242 | }, 243 | }, 244 | title: { 245 | style: { 246 | "font-size": "20px", 247 | "font-family": "Chirp-Heavy", 248 | "line-height": "24px", 249 | margin: "8% 0% 5% 0%", 250 | color: "#0F1419", 251 | "text-align": "center", 252 | }, 253 | }, 254 | }, 255 | style: {}, 256 | }, 257 | pauseButton: { 258 | img: { 259 | src: "https://client-api.arkoselabs.com/cdn/fc/assets/graphics/twitter/twitter_pause_1648060935339.svg", 260 | }, 261 | }, 262 | victory: { 263 | children: { 264 | icon: { style: { display: "none" } }, 265 | text: { 266 | style: { 267 | padding: "0px", 268 | color: "#0F1419 !important", 269 | display: "block !important", 270 | "& > *": { width: "100% !important" }, 271 | width: "100% !important", 272 | "font-size": "20px", 273 | "font-family": "Chirp-Regular !important", 274 | "line-height": "24px", 275 | "margin-top": "20%", 276 | "text-align": "center", 277 | }, 278 | }, 279 | }, 280 | style: { 281 | "margin-left": "0px", 282 | "background-repeat": "no-repeat", 283 | "background-image": 284 | "url(https://client-api.arkoselabs.com/cdn/fc/assets/graphics/twitter/twitter_success_1648160667274.svg)", 285 | "background-position": "50% 33%", 286 | "background-size": "20% 20%", 287 | }, 288 | }, 289 | progress: { 290 | children: { 291 | slider: { style: {} }, 292 | bar: { style: {} }, 293 | text: { style: {} }, 294 | inner: { style: {} }, 295 | }, 296 | }, 297 | loading: { 298 | children: { 299 | progress: { style: {} }, 300 | style: {}, 301 | text: { 302 | style: { 303 | width: "100%", 304 | "font-size": "20px", 305 | "font-family": "Chirp-Heavy", 306 | "line-height": "24px", 307 | "margin-top": "120px", 308 | color: "#000", 309 | }, 310 | }, 311 | interactionContainer: { style: { display: "none" } }, 312 | }, 313 | style: {}, 314 | }, 315 | victoryProvided: { style: {} }, 316 | playButton: { 317 | children: { 318 | icon: { style: { width: "36px", padding: "0", height: "36px" } }, 319 | }, 320 | img: { 321 | src: "https://client-api.arkoselabs.com/cdn/fc/assets/graphics/twitter/twitter_play_1648060929281.svg", 322 | }, 323 | style: { 324 | width: "12%", 325 | "line-height": "30px", 326 | margin: "4%", 327 | color: "rgba(255, 255, 255, 0)", 328 | "font-weight": "700", 329 | height: "43px", 330 | }, 331 | }, 332 | }, 333 | pick_a_tile: { 334 | app: { 335 | style: { 336 | width: "100%", 337 | "border-top-left-radius": "12px !important", 338 | "border-top-right-radius": "12px !important", 339 | height: "500px", 340 | }, 341 | }, 342 | checking: { 343 | children: { loadingImg: { style: {} } }, 344 | style: { display: "none" }, 345 | }, 346 | game: { 347 | challengeItem: { 348 | image: { style: { width: "100%" } }, 349 | style: { width: "100%" }, 350 | }, 351 | children: { 352 | challenge: { 353 | style: { 354 | "div:nth-child(1)": { width: "100%" }, 355 | width: "300px", 356 | top: "30%", 357 | left: "24%", 358 | height: "200px", 359 | }, 360 | }, 361 | progress: { style: {} }, 362 | wrapper: { style: { width: "100%" } }, 363 | text: { 364 | style: { 365 | padding: "0% 13%", 366 | color: "#0F1419", 367 | top: "13%", 368 | "& > *": { 369 | "font-size": "20px !important", 370 | "font-family": "Chirp-Heavy !important", 371 | "line-height": "24px !important", 372 | color: "#0F1419", 373 | "text-align": "center !important", 374 | }, 375 | width: "100%", 376 | "font-size": "20px", 377 | "font-family": "Chirp-Heavy", 378 | "text-align": "center", 379 | }, 380 | }, 381 | embeddedSessionIdContainer: { style: {} }, 382 | }, 383 | style: { width: "100%" }, 384 | }, 385 | watermark: { style: { bottom: "5px" } }, 386 | embeddedSessionIdContainer: { style: { height: "calc(100% - 4px)" } }, 387 | interstitial: { 388 | progressIndicator: { 389 | filler: { 390 | style: { 391 | background: 392 | "linear-gradient(269.99deg, #1D9BF0 0.79%, #6BC9FB 99.46%)", 393 | }, 394 | }, 395 | style: { "border-radius": "10px" }, 396 | }, 397 | style: { 398 | "background-color": "#FFF", 399 | "box-shadow": "none", 400 | position: "fixed", 401 | float: "right", 402 | top: "0px", 403 | left: "0px", 404 | }, 405 | wrapper: { 406 | style: { 407 | "background-color": "#FFF", 408 | border: "0px solid #FFF", 409 | "border-radius": "100px", 410 | width: "66%", 411 | "box-shadow": "none", 412 | "margin-top": "0%", 413 | }, 414 | }, 415 | progressIndicatorItem: { 416 | style: { 417 | border: "0px solid #FFF", 418 | width: "197px !important", 419 | "&:first-child": { 420 | "border-bottom-left-radius": "100px", 421 | "border-top-left-radius": "100px", 422 | }, 423 | "&:last-child": { 424 | "border-bottom-right-radius": "100px", 425 | "border-top-right-radius": "100px", 426 | }, 427 | background: "#EFF3F4", 428 | height: "4px", 429 | }, 430 | }, 431 | }, 432 | victory: { 433 | img: { 434 | src: "https://client-api.arkoselabs.com/cdn/fc/assets/graphics/twitter/twitter_success_1648160667274.svg", 435 | style: { 436 | top: "0% !important", 437 | "margin-top": "24%", 438 | transform: "translateY(0%)", 439 | }, 440 | }, 441 | style: {}, 442 | text: { 443 | style: { 444 | padding: "0px", 445 | color: "#0F1419 !important", 446 | display: "block !important", 447 | width: "100%", 448 | "font-size": "20px", 449 | "font-family": "Chirp-Regular !important", 450 | "line-height": "24px", 451 | "margin-top": "11%", 452 | "text-align": "center", 453 | }, 454 | }, 455 | }, 456 | challengeSelectableOverlay: { 457 | children: { overlayElements: { focus: { style: { css: "css" } } } }, 458 | }, 459 | home: { 460 | children: { 461 | body: { 462 | style: { 463 | "font-size": "15px", 464 | "padding-bottom": "4%", 465 | "line-height": "20px", 466 | margin: "5% 0% 3% 0%", 467 | color: "#536471", 468 | "text-align": "center", 469 | }, 470 | }, 471 | button: { 472 | style: { 473 | "background-color": "#0F1419", 474 | border: "0px solid #000", 475 | "border-radius": "100px", 476 | padding: "4% 0%", 477 | color: "white", 478 | "font-size": "14px", 479 | "line-height": "0px", 480 | "&:hover": { opacity: "80%" }, 481 | height: "30px", 482 | "min-width": "82%", 483 | }, 484 | }, 485 | heading: { 486 | style: { 487 | padding: "0px 0px", 488 | margin: "37% 0% -4px 0%", 489 | color: "#0F1419", 490 | "font-size": "31px", 491 | "&:focus-visible": { outline: "none" }, 492 | "font-family": "Chirp-Heavy", 493 | "line-height": "36px", 494 | height: "56px", 495 | "text-align": "center", 496 | }, 497 | }, 498 | }, 499 | style: { 500 | "margin-left": "0px", 501 | "background-repeat": "no-repeat", 502 | "background-image": 503 | "url(https://client-api.arkoselabs.com/cdn/fc/assets/graphics/twitter/safety_1644873397768.svg)", 504 | "background-position": "50% 18%", 505 | "background-size": "20% 20%", 506 | }, 507 | }, 508 | interstiatial: { 509 | numericprogress: { 510 | style: { 511 | "font-size": "20px", 512 | "padding-top": "10%", 513 | "line-height": "24px", 514 | color: "#000", 515 | }, 516 | }, 517 | }, 518 | wrong: { 519 | children: { 520 | button: { 521 | style: { 522 | "background-color": "#0F1419", 523 | border: "0px solid #000", 524 | "border-radius": "100px", 525 | padding: "4% 37%", 526 | color: "white", 527 | "font-size": "15px", 528 | "line-height": "0px", 529 | "&:hover": { opacity: "80%" }, 530 | height: "30px", 531 | }, 532 | }, 533 | examples: { style: { top: "14%", left: "25%", right: "25%" } }, 534 | embeddedSessionIdContainer: { style: {} }, 535 | instruction: { 536 | style: { padding: "0% 9%", bottom: "-33px" }, 537 | text: { 538 | style: { 539 | "font-size": "15px", 540 | "padding-bottom": "43%", 541 | color: "#536471", 542 | "text-align": "center", 543 | }, 544 | }, 545 | }, 546 | cross: { style: {} }, 547 | buttonContainer: { style: { bottom: "19%" } }, 548 | exclamation: { 549 | style: { 550 | width: "100%", 551 | padding: "4% 9%", 552 | "text-align": "center", 553 | }, 554 | text: { 555 | style: { 556 | width: "100%", 557 | "font-size": "31px", 558 | "padding-top": "43%", 559 | "font-family": "Chirp-Heavy", 560 | color: "#0F1419", 561 | "text-align": "center", 562 | }, 563 | }, 564 | }, 565 | tick: { style: {} }, 566 | }, 567 | style: {}, 568 | }, 569 | button: { style: { "&:hover": { opacity: "80%" } } }, 570 | wrongTimeout: { 571 | children: { 572 | button: { style: {} }, 573 | arrow: { style: {} }, 574 | progressContainer: { style: {} }, 575 | embeddedSessionIdContainer: { style: { bottom: "56px" } }, 576 | instruction: { style: {} }, 577 | buttonContainer: { style: {} }, 578 | title: { style: {} }, 579 | }, 580 | style: {}, 581 | }, 582 | progress: { 583 | children: { 584 | slider: { style: { "background-color": "rgba(0,0,0,0)" } }, 585 | bar: { style: {} }, 586 | text: { style: { css: "css" } }, 587 | inner: { style: { "background-color": "rgba(0,0,0,0)" } }, 588 | }, 589 | style: { css: "css" }, 590 | }, 591 | }, 592 | }, 593 | }, 594 | game_sid: "eu-west-1", 595 | sid: "eu-west-1", 596 | lang: "en", 597 | string_table_prefixes: ["twitter_custom_instructions"], 598 | string_table: { 599 | "meta.help": "Get answers to your questions", 600 | "4.game_progress": "{{currentChallenge}} of {{numChallenges}}", 601 | "4.key_image_annotation": "Match This!", 602 | "meta.audio_info_play-3": 603 | "Press Play, type the number of the song that is the most sad, then press enter:", 604 | "meta.text_info": "Enter the text you see:", 605 | "meta.footer_general_info": "Play like humans do.", 606 | "4.fail_info_timed_top": "That was not quite fast enough.", 607 | "meta.audio_info_ctrl": "Press CTRL to play again.", 608 | "4.hint-numericalmatch": 609 | "Make sure the number of objects matches the number shown in the left image exactly.", 610 | "twitter_custom_instructions-audio_game.finish_message": 611 | "Authentication complete. Let\u2019s continue making your account.", 612 | "4.key_image_annotation-3d_rollball_animals_multi": 613 | "icon and hand direction", 614 | "meta.star_info": "Stars you have earned", 615 | "meta.visual_version": "Change to a visual challenge", 616 | "aria.tick_icon_alt": "Example for correct answer.", 617 | "meta.audio_sending_answer": "Committing your answer. Please wait...", 618 | "4.instructions-3d_rollball_animals_alt": 619 | "Unfortunately, I cannot remove this screen. Just click submit, it should work.", 620 | "4.hint-3d_rollball_animals_alt": 621 | "Make sure the animal is facing in the same direction that the hand is pointing towards.", 622 | "game_meta.seconds": "seconds", 623 | "meta.audio_play_again": "Play sound again", 624 | "meta.audio_please_download_info": 625 | "Please download and listen to the sound, then type what you heard:", 626 | "twitter_custom_instructions-meta.verification_complete": 627 | "Authentication complete. Let\u2019s continue making your account.", 628 | "meta.audio_info": "Enter the numbers you hear:", 629 | "meta.session_timeout": 630 | "The connection to a verification server was interrupted. Please refresh this page to try again.", 631 | "4.hint-boardgame": 632 | "Make sure the game pieces are in the indicated squares.", 633 | "meta.audio_info_play": "Press Play and type what you hear:", 634 | "meta.footer_finished_info-1": 635 | "

Verification complete!

You've proven you're a human.

Continue your action.

", 636 | "meta.footer_finished_info-3": 637 | "Verification complete!
You've proven you're a human.
Continue your action.", 638 | "4.fail_button": "Try again", 639 | "4.key_image_annotation-iconmatch": "Icon order", 640 | "4.instructions-3d_rollball_animals": `Unfortunately, I cannot remove this screen. Just click submit, it should work. `, 644 | "meta.audio_play_button": "Play", 645 | "4.interstitial_progress_2": "{{currentChallenge}} of {{numChallenges}}", 646 | "4.interstitial_progress_1": "{{currentChallenge}} done", 647 | "4.hint-simpleicons": 648 | "Make sure the animals on both sides are an exact match.", 649 | "aria.correct_image_alt": "Example for correct answer.", 650 | "meta.audio_new_puzzle": "Start over with a different challenge", 651 | "meta.html_verify_info": 652 | "Please prove you're not a spammer by doing this quick activity!", 653 | "game_meta.game_great": "Great", 654 | "4.hint-animal_scale": 655 | "Make sure that the animals's height matches the number on the left.", 656 | "4.hint-3d_rollball_characters": 657 | "Make sure the character is facing in the same direction that the hand is pointing towards.", 658 | "meta.funcaptcha_website": "Open Arkose Labs website", 659 | "4.hint-icon_order_match": 660 | "Make sure the order of objects matches the icons in the left image.", 661 | "4.key_image_annotation-3d_rollball_objects": "Match this angle", 662 | "aria.challenge_image_alt": "Image {{count}}.", 663 | "4.hint-matchingcard": 664 | "Make sure that the card pair has the same number and symbol as the left image", 665 | "game_meta.verification": "Verification", 666 | "aria.input_placeholder": "Type here...", 667 | "meta.audio_sent_info": 668 | "Verification complete! You've proven you're a human. Continue your action.", 669 | "meta.funcaptcha": "Arkose Labs", 670 | "meta.reload_challenge": "Reload Challenge", 671 | "aria.left_arrow": "Navigate to previous image", 672 | "meta.generic_error": 673 | "Something went wrong. Please reload the challenge to try again.", 674 | "aria.audio_challenge": "Audio challenge", 675 | "meta.audio_disabled": 676 | "The audio challenge has been disabled. Please use the visual challenge, or contact the customer support team for assistance.", 677 | "4.challenge_progress": "{{currentChallenge}} of {{numChallenges}}", 678 | "4.key_image_annotation-train_coordinates": "Train Position", 679 | "meta.stars_link": "Stars", 680 | "meta.api_timeout_error": 681 | "The connection to a verification server was interrupted. Please reload the challenge to try again. ", 682 | "twitter_custom_instructions-3.fail_top": "That\u2019s not quite right", 683 | "aria.right_arrow": "Navigate to next image", 684 | "meta.audio_challenge_frame_title": "Audio challenge", 685 | "4.hint-hopscotch": 686 | "Make sure the person is standing on the square indicated by the cross.", 687 | "4.key_image_annotation-3d_rollball": "Direction", 688 | "aria.visual_challenge_describe": 689 | "Audio challenge is available below. Compatible with screen reader software.", 690 | "4.hint-dicematch": 691 | "Make sure that the dice add up to match the image on the left.", 692 | "4.hint-boat_match_flag": 693 | "Make sure the Boat's flag matches the icon and it's rotation faces the hands direction.", 694 | "4.key_image_annotation-icon_order_match": "Match this order", 695 | "meta.audio_version": "Change to an audio challenge", 696 | "meta.audio_answer_input": "Challenge Answer", 697 | "4.key_image_annotation-3d_rollball_simple": "Match This!", 698 | "meta.audio_challenge": "Change to an audio challenge", 699 | "4.hint-topdownscene": 700 | "All the objects in the right image must match the left image", 701 | "4.hint-coordinatesmatch": 702 | "Make sure that the person is sitting in the coordinates shown on the left.", 703 | "meta.footer_patent": "Patent pending", 704 | "4.submit_button": "Submit", 705 | "game_meta.checking": "Checking", 706 | "twitter_custom_instructions-game_meta.landing_info": 707 | "We need to make sure that you\u2019re a real person.", 708 | "game_meta.game_good": "Good", 709 | "aria.answer_field": "Answer field", 710 | "4.hint": 711 | "Make sure to select an image that matches what you see in the example image.", 712 | "meta.audio_verify_button": "Verify", 713 | "4.hint-hopscotch_v2": 714 | "Make sure the person is standing in the shape that's indicated by the cross.", 715 | "meta.audio_play": "Play Sound", 716 | "4.hint-icon_align": 717 | "Make sure you connect the two icons shown in the image on the left.", 718 | "meta.audio_please_download_info-3": 719 | "Please download and listen to the sound, type the number of the song that is the most sad:", 720 | "4.intro_title": "Verification", 721 | "meta.audio_alert": "Please enter your answer into the input box.", 722 | "meta.loading_info": "Working, please wait...", 723 | "twitter_custom_instructions-audio_game.input_placeholder": 724 | "Number of the correct sound.", 725 | "twitter_custom_instructions-meta.loading_info": "Loading....", 726 | "aria.audio_answer_input": "Challenge answer", 727 | "meta.html_verify_button": "Verify", 728 | "4.hint-orbit_match_game": 729 | "Make sure the icon is in the indicated orbit number.", 730 | "meta.meta_start_cta": "Start visual challenge", 731 | "4.key_image_annotation-3d_rollball_characters": "Match this angle", 732 | "4.instructions-3d_rollball_animals_multi": 733 | "Use the arrows to rotate the animal with the same icon to face where the hand is pointing", 734 | "meta.footer_general_info-1": "Please solve the puzzle.", 735 | "game_meta.challenge": "Challenge {{count}}", 736 | "4.instructions": 737 | "Press the arrows to see different images. When the image matches the example on the left, press Submit!", 738 | "4.hint-train": 739 | "Make sure that the train is in the same spot on the track as the green circle indicated in the left image", 740 | "4.fail_info_timed_middle_hidden": 741 | "Try to answer before too much time runs out!", 742 | "game_meta.not_for_public_watermark": "NOT FOR PRODUCTION USE", 743 | "4.hint-3d_rollball_objects": 744 | "Make sure the object is facing in the same direction that the hand is pointing towards.", 745 | "game_meta.interstitial_progress_1": "{{currentChallenge}} done", 746 | "meta.funcaptcha_solved_phrase": "Verification challenge has been solved", 747 | "twitter_custom_instructions-game_meta.landing_heading": 748 | "Authenticate your account", 749 | "4.hint-train_coordinates": 750 | "Make sure that the train is placed at the point of coordinates indicated in the left image", 751 | "twitter_custom_instructions-3.intro_title": "Authenticate your account", 752 | "twitter_custom_instructions-meta.html_verify_button": "Authenticate", 753 | "twitter_custom_instructions-aria.input_placeholder": 754 | "Type your answer here", 755 | "4.key_image_annotation-3d_rollball_animals": "Match this angle", 756 | "4.hint-hopscotch_highsec": 757 | "Make sure the person is standing on the icon indicated by the colored circle", 758 | "game_meta.wait_text": "Please wait while we check your score.", 759 | "aria.visual_challenge_label": "Visual challenge.", 760 | "game_meta.landing_heading": "Protecting your account", 761 | "meta.verification_complete": "Verification complete!", 762 | "4.key_image_annotation-train": "Train position", 763 | "aria.cross_icon_alt": "Example for incorrect answer.", 764 | "aria.wrong_image_alt": "Example for incorrect answer.", 765 | "4.hint-3d_rollball_animals_multi": 766 | "Make sure the animal with the same icon above it as the left image, is facing in the same direction that the hand is pointing towards.", 767 | "aria.restart_challenge": "Restarting challenge", 768 | "meta.audio_rate_limit": 769 | "Use of the audio challenge for this user has been too high. Please try again.", 770 | "meta.restart_label": "Restart", 771 | "meta.audio_incorrect": "Incorrect, try again", 772 | "4.hint-icon_connect": 773 | "Make sure you connect the two icons shown in the image on the left.", 774 | "4.hint-3d_rollball_animals": 775 | "Make sure the animal is facing in the same direction that the hand is pointing towards.", 776 | "twitter_custom_instructions-game_meta.landing_start": "Authenticate", 777 | "game_meta.landing_info": 778 | "Please solve this puzzle so we know you are a real person", 779 | "4.fail_top": "That was not quite right.", 780 | "twitter_custom_instructions-audio_game.loading_info": "Loading....", 781 | "4.key_image_annotation-hopscotch": "Match This!", 782 | "game_meta.game_perfect": "Perfect", 783 | "4.key_image_annotation-3d_rollball_animals_alt": "Match this angle", 784 | "4.hint-trains_simple": 785 | "Make sure that the train is placed at the point indicated in the left image", 786 | "4.finish_message": 787 | "Verification Complete
You've proven you're a human.
Continue your action.", 788 | "meta.audio_challenge_label": "Audio", 789 | "4.hint-object_scale": 790 | "Make sure that the object's height matches the number on the left.", 791 | "4.hint-lumber_length": 792 | "Make sure the object length matches the number on the left.", 793 | "4.hint-bowling": 794 | "Make sure the number of struck pins matches the number indicated on the left.", 795 | "meta.finished_info": 796 | "You've proven you're a human. Continue your action.", 797 | "4.hint-icongrouping": 798 | "Place the icons on the same background as shown on the left.", 799 | "meta.footer_phone_mode_on": "This isn't working for me", 800 | "4.key_image_annotation-trains_simple": "Train position", 801 | "game_meta.game_check": "Check!", 802 | "meta.session_reset": "Your session was reset. Please try again.", 803 | "game_meta.landing_button": "Verify", 804 | "4.hint-3d_rollball_simple": 805 | "Make sure the animal is looking in the same direction that the hand is pointing towards.", 806 | "4.hint-3d_rollball": 807 | "Make sure the animal is looking in the same direction that the hand is pointing towards.", 808 | "4.key_image_annotation-matchingcard": "number and symbol", 809 | "aria.visual_challenge": 810 | "Visual challenge. Audio challenge is available below, compatible with screen reader software.", 811 | "4.hint-iconmatch": 812 | "Make sure the order of the objects matches the image on the left. The objects must also match the icons in the left image.", 813 | "meta.visual_challenge_label": "Visual", 814 | "meta.visual_challenge_frame_title": "Visual challenge", 815 | "4.key_image_annotation-topdownscene": "furniture and locations", 816 | "game_meta.landing_start": "Start Puzzle", 817 | "twitter_custom_instructions-3.finish_message": 818 | "Authentication complete.
Let\u2019s continue making your account.", 819 | "meta.audio_download": "Download Sound", 820 | }, 821 | earlyVictoryMessage: null, 822 | font_size_adjustments: null, 823 | style_theme: "default", 824 | }); 825 | }); 826 | 827 | router.post("/fc/gt2/public_key/*", (req, res) => { 828 | return res.status(200).send({ 829 | token: 830 | "107175ab73d501a82.7403616905|r=eu-west-1|meta=3|meta_width=558|meta_height=523|metabgclr=transparent|metaiconclr=%23555555|guitextcolor=%23000000|pk=2CB16598-CB82-4CF7-B332-5990DB66F3AB|at=40|rid=85|ag=101|cdn_url=https%3A%2F%2Fclient-api.arkoselabs.com%2Fcdn%2Ffc|lurl=https%3A%2F%2Faudio-eu-west-1.arkoselabs.com|surl=https%3A%2F%2Fclient-api.arkoselabs.com|smurl=https%3A%2F%2Fclient-api.arkoselabs.com%2Fcdn%2Ffc%2Fassets%2Fstyle-manager", 831 | challenge_url: "", 832 | challenge_url_cdn: 833 | "https://client-api.arkoselabs.com/cdn/fc/assets/ec-game-core/bootstrap/1.11.0/standard/game_core_bootstrap.js", 834 | challenge_url_cdn_sri: null, 835 | noscript: "Disable", 836 | inject_script_integrity: null, 837 | inject_script_url: null, 838 | mbio: true, 839 | tbio: true, 840 | kbio: true, 841 | styles: [ 842 | { 843 | name: "Twitter - Web - Default", 844 | theme: "", 845 | iframeWidth: 558, 846 | iframeHeight: 500, 847 | style: { 848 | id: "dc2833dc-6a20-4f4d-b56b-7de82dc901ee", 849 | sriHash: 850 | "sha384-4dhDhtrG0jxOQWL41NaIZ/xdN6wMJc2r6k3lWe4896WxIHL7PPST1H3+4OfXhmpD", 851 | }, 852 | assets: { 853 | "audio-game.pause": "8247ba8c-b61a-4cff-a02e-fba77a4a0bac.svg", 854 | "audio-game.play": "9323a4a4-cf3b-4d9d-9ba2-c58f5465fad8.svg", 855 | "footer.audio": "e94ae91f-1774-408b-a8b3-1237915c996c.svg", 856 | "footer.restart": "c7a4f75b-c735-4b7b-914a-5c9cd8164ca6.svg", 857 | "footer.visual": "5678e99a-7f54-4213-af3c-63c7651e1e8a.svg", 858 | "home.logo": "4d9fc2f0-efb9-41a6-9986-586db3d92c3b.svg", 859 | "loading.icon": "31fa68cf-39a9-42fe-a1c9-e90781281693.png", 860 | "victory.tick": "7e249430-824c-4b87-bc29-a6ade22a5145.svg", 861 | }, 862 | }, 863 | ], 864 | iframe_width: 558, 865 | iframe_height: 500, 866 | disable_default_styling: false, 867 | string_table: { 868 | "meta.api_timeout_error": 869 | "The connection to a verification server was interrupted. Please reload the challenge to try again. ", 870 | "meta.generic_error": 871 | "Something went wrong. Please reload the challenge to try again.", 872 | "meta.loading_info": "Loading....", 873 | "meta.reload_challenge": "Reload Challenge", 874 | "meta.visual_challenge_frame_title": "Visual challenge", 875 | }, 876 | }); 877 | }); 878 | 879 | router.post("/fc/*", (req, res) => { 880 | return res.status(200).send({ 881 | response: "answered", 882 | solved: true, 883 | incorrect_guess: "", 884 | score: 10, 885 | }); 886 | }); 887 | 888 | export default router; 889 | -------------------------------------------------------------------------------- /src/routes/other/email/validateInput.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import User from "../../../models/User"; 3 | 4 | const router = express.Router(); 5 | 6 | interface IEmailParams { 7 | email: string; 8 | } 9 | 10 | interface IPhoneParams { 11 | raw_phone_number: string; 12 | } 13 | 14 | router.get("/i/api/i/users/email_available.json", async (req, res) => { 15 | const params = req.query as unknown as IEmailParams; 16 | const user = await User.findOne({ 17 | email: params.email, 18 | }); 19 | if (user) 20 | return res.status(200).send({ 21 | msg: "Email has already been taken.", 22 | taken: true, 23 | valid: false, 24 | }); 25 | else 26 | return res.status(200).send({ 27 | msg: "Available!", 28 | taken: false, 29 | valid: true, 30 | }); 31 | }); 32 | 33 | router.get("/i/api/1.1/users/phone_number_available.json", (req, res) => { 34 | const params = req.query as unknown as IPhoneParams; 35 | return res.status(200).send({ 36 | msg: "Sorry, Blue OSS doesn't currently support phone numbers.", 37 | taken: false, 38 | valid: false, 39 | }); 40 | }); 41 | 42 | export default router; 43 | -------------------------------------------------------------------------------- /src/routes/other/image/image.ts: -------------------------------------------------------------------------------- 1 | import { rootPath } from "get-root-path"; 2 | import express from "express"; 3 | import sanitize from "sanitize-filename"; 4 | import * as fs from "fs"; 5 | 6 | interface IImgParams { 7 | fileName: string; 8 | } 9 | 10 | const router = express.Router(); 11 | 12 | router.get("/img/:filename", (req, res) => { 13 | if (!req.params.filename) 14 | return res.status(400).send({ msg: "Missing filename" }); 15 | const path = `${rootPath}/img/${sanitize(req.params.filename)}`; 16 | if (!fs.existsSync(path)) 17 | return res.status(400).send({ msg: "File not found" }); 18 | return res.sendFile(path); 19 | }); 20 | 21 | export default router; 22 | -------------------------------------------------------------------------------- /src/routes/twimg/profile.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import rootPath from "get-root-path"; 3 | import { error } from "../../util/logging"; 4 | 5 | const router = express.Router(); 6 | 7 | router.get("/profile_images/*", (req, res) => { 8 | const [image] = req.url.split("/").slice(-1); 9 | return res.sendFile(`${rootPath}/img/${image}`, (e) => { 10 | error(e || "An unknown error relating to Twimg stub occurred"); 11 | }); 12 | }); 13 | 14 | router.get("/images/*", (req, res) => { 15 | const [image] = req.url.split("/").slice(-1); 16 | return res.sendFile(`${rootPath}/img/${image}`, (e) => { 17 | error(e || "An unknown error relating to Twimg stub occurred"); 18 | }); 19 | }); 20 | 21 | export default router; 22 | -------------------------------------------------------------------------------- /src/types/env.d.ts: -------------------------------------------------------------------------------- 1 | namespace NodeJS { 2 | interface ProcessEnv { 3 | PORT: number; 4 | JWT_SECRET: string; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/types/graphql.ts: -------------------------------------------------------------------------------- 1 | export interface ICreateTweetBody { 2 | variables: Variables; 3 | features: Features; 4 | queryId: string; 5 | } 6 | 7 | export interface Features { 8 | tweetypie_unmention_optimization_enabled: boolean; 9 | vibe_api_enabled: boolean; 10 | responsive_web_edit_tweet_api_enabled: boolean; 11 | graphql_is_translatable_rweb_tweet_is_translatable_enabled: boolean; 12 | view_counts_everywhere_api_enabled: boolean; 13 | longform_notetweets_consumption_enabled: boolean; 14 | tweet_awards_web_tipping_enabled: boolean; 15 | interactive_text_enabled: boolean; 16 | responsive_web_text_conversations_enabled: boolean; 17 | longform_notetweets_rich_text_read_enabled: boolean; 18 | blue_business_profile_image_shape_enabled: boolean; 19 | responsive_web_graphql_exclude_directive_enabled: boolean; 20 | verified_phone_label_enabled: boolean; 21 | freedom_of_speech_not_reach_fetch_enabled: boolean; 22 | standardized_nudges_misinfo: boolean; 23 | tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: boolean; 24 | responsive_web_graphql_skip_user_profile_image_extensions_enabled: boolean; 25 | responsive_web_graphql_timeline_navigation_enabled: boolean; 26 | responsive_web_enhance_cards_enabled: boolean; 27 | } 28 | 29 | export interface Variables { 30 | tweet_text: string; 31 | dark_request: boolean; 32 | media: Media; 33 | semantic_annotation_ids: any[]; 34 | } 35 | 36 | export interface Media { 37 | media_entities: any[]; 38 | possibly_sensitive: boolean; 39 | } 40 | 41 | export interface IJwtDecoded { 42 | id: number; 43 | } 44 | 45 | export interface IUserByScreenNameVariables { 46 | screen_name: string; 47 | withSafetyModeUserFields: boolean; 48 | } 49 | 50 | export interface IProfileSpotlightsQueryVariables { 51 | screen_name: string; 52 | } 53 | 54 | export interface IGenericFeatures { 55 | blue_business_profile_image_shape_enabled: boolean; 56 | responsive_web_graphql_exclude_directive_enabled: boolean; 57 | verified_phone_label_enabled: boolean; 58 | responsive_web_graphql_skip_user_profile_image_extensions_enabled: boolean; 59 | responsive_web_graphql_timeline_navigation_enabled: boolean; 60 | } 61 | 62 | export interface IUserByRestIdVariables { 63 | userId: string; 64 | withSafetyModeUserFields: boolean; 65 | } 66 | -------------------------------------------------------------------------------- /src/types/guide.ts: -------------------------------------------------------------------------------- 1 | export interface GlobalObjects { 2 | tweets: { [key: string]: TweetValue }; 3 | users: { [key: string]: IUser }; 4 | moments: Broadcasts; 5 | cards: Broadcasts; 6 | places: Broadcasts; 7 | media: Broadcasts; 8 | broadcasts: Broadcasts; 9 | topics: Broadcasts; 10 | lists: Broadcasts; 11 | } 12 | 13 | export interface Broadcasts {} 14 | 15 | export interface TweetValue { 16 | created_at: string; 17 | id: number; 18 | id_str: string; 19 | full_text: string; 20 | truncated: boolean; 21 | display_text_range: number[]; 22 | entities: TweetEntities; 23 | extended_entities?: ExtendedEntities; 24 | source: string; 25 | in_reply_to_status_id: unknown; 26 | in_reply_to_status_id_str: unknown; 27 | in_reply_to_user_id: unknown; 28 | in_reply_to_user_id_str: unknown; 29 | in_reply_to_screen_name: unknown; 30 | user_id: number; 31 | user_id_str: string; 32 | geo: unknown; 33 | coordinates: unknown; 34 | place: unknown; 35 | contributors: unknown; 36 | is_quote_status: boolean; 37 | retweet_count: number; 38 | favorite_count: number; 39 | reply_count: number; 40 | quote_count: number; 41 | conversation_id: number; 42 | conversation_id_str: string; 43 | favorited: boolean; 44 | retweeted: boolean; 45 | possibly_sensitive?: boolean; 46 | possibly_sensitive_editable?: boolean; 47 | lang: string; 48 | supplemental_language: unknown; 49 | ext_views: EXTViews; 50 | ext: TweetEXT; 51 | self_thread?: SelfThread; 52 | quoted_status_id?: number; 53 | quoted_status_id_str?: string; 54 | quoted_status_permalink?: QuotedStatusPermalink; 55 | } 56 | 57 | export interface TweetEntities { 58 | hashtags: any[]; 59 | symbols: any[]; 60 | user_mentions: any[]; 61 | urls: URLElement[]; 62 | media?: EntitiesMedia[]; 63 | } 64 | 65 | export interface EntitiesMedia { 66 | id: number; 67 | id_str: string; 68 | indices: number[]; 69 | media_url: string; 70 | media_url_https: string; 71 | url: string; 72 | display_url: string; 73 | expanded_url: string; 74 | type: Type | string; 75 | original_info: OriginalInfo; 76 | sizes: Sizes; 77 | source_status_id?: number; 78 | source_status_id_str?: string; 79 | source_user_id?: number; 80 | source_user_id_str?: string; 81 | } 82 | 83 | export interface OriginalInfo { 84 | width: number; 85 | height: number; 86 | focus_rects?: FocusRect[]; 87 | } 88 | 89 | export interface FocusRect { 90 | x: number; 91 | y: number; 92 | h: number; 93 | w: number; 94 | } 95 | 96 | export interface Sizes { 97 | thumb: Large; 98 | small: Large; 99 | large: Large; 100 | medium: Large; 101 | } 102 | 103 | export interface Large { 104 | w: number; 105 | h: number; 106 | resize: Resize | string; 107 | } 108 | 109 | export enum Resize { 110 | Crop = "crop", 111 | Fit = "fit", 112 | } 113 | 114 | export enum Type { 115 | Photo = "photo", 116 | Video = "video", 117 | } 118 | 119 | export interface URLElement { 120 | url: string; 121 | expanded_url: string; 122 | display_url: string; 123 | indices: number[]; 124 | } 125 | 126 | export interface TweetEXT { 127 | unmentionInfo: UnmentionInfo; 128 | editControl: EditControl; 129 | } 130 | 131 | export interface EditControl { 132 | r: EditControlR; 133 | ttl: number; 134 | } 135 | 136 | export interface EditControlR { 137 | ok: PurpleOk; 138 | } 139 | 140 | export interface PurpleOk { 141 | initial: Initial; 142 | } 143 | 144 | export interface Initial { 145 | editTweetIds: string[]; 146 | editableUntilMsecs: string; 147 | editsRemaining: string; 148 | isEditEligible: boolean; 149 | } 150 | 151 | export interface UnmentionInfo { 152 | r: UnmentionInfoR; 153 | ttl: number; 154 | } 155 | 156 | export interface UnmentionInfoR { 157 | ok: Broadcasts; 158 | } 159 | 160 | export interface EXTViews { 161 | state: State | string; 162 | count: string; 163 | } 164 | 165 | export enum State { 166 | EnabledWithCount = "EnabledWithCount", 167 | } 168 | 169 | export interface ExtendedEntities { 170 | media: ExtendedEntitiesMedia[]; 171 | } 172 | 173 | export interface ExtendedEntitiesMedia { 174 | id: number; 175 | id_str: string; 176 | indices: number[]; 177 | media_url: string; 178 | media_url_https: string; 179 | url: string; 180 | display_url: string; 181 | expanded_url: string; 182 | type: Type | string; 183 | original_info: OriginalInfo; 184 | sizes: Sizes; 185 | source_status_id?: number; 186 | source_status_id_str?: string; 187 | source_user_id?: number; 188 | source_user_id_str?: string; 189 | video_info?: VideoInfo; 190 | media_key: string; 191 | ext_sensitive_media_warning: unknown; 192 | ext_media_availability: EXTMediaAvailability; 193 | ext_alt_text: unknown | string; 194 | ext_media_color: EXTMediaColor; 195 | ext: MediaEXT; 196 | additional_media_info?: AdditionalMediaInfo; 197 | } 198 | 199 | export interface AdditionalMediaInfo { 200 | title?: string; 201 | description?: string; 202 | embeddable?: boolean; 203 | monetizable: boolean; 204 | source_user?: IUser; 205 | } 206 | 207 | export enum AdvertiserAccountType { 208 | None = "none", 209 | } 210 | 211 | export interface SourceUserEntities { 212 | url?: Description; 213 | description: Description; 214 | } 215 | 216 | export interface Description { 217 | urls: URLElement[]; 218 | } 219 | 220 | export interface SourceUserEXT { 221 | highlightedLabel: HighlightedLabel; 222 | hasNftAvatar: HasNftAvatar; 223 | } 224 | 225 | export interface HasNftAvatar { 226 | r: HasNftAvatarR; 227 | ttl: number; 228 | } 229 | 230 | export interface HasNftAvatarR { 231 | ok: boolean; 232 | } 233 | 234 | export interface HighlightedLabel { 235 | r: PurpleR; 236 | ttl: number; 237 | } 238 | 239 | export interface PurpleR { 240 | ok: FluffyOk; 241 | } 242 | 243 | export interface FluffyOk { 244 | label?: Label; 245 | } 246 | 247 | export interface Label { 248 | description: string; 249 | badge: Badge; 250 | url: LabelURL; 251 | userLabelType: string; 252 | userLabelDisplayType: string; 253 | } 254 | 255 | export interface Badge { 256 | url: string; 257 | } 258 | 259 | export interface LabelURL { 260 | urlType: string; 261 | url: string; 262 | } 263 | 264 | export enum EXTProfileImageShape { 265 | Circle = "Circle", 266 | Square = "Square", 267 | } 268 | 269 | export interface MediaEXT { 270 | mediaStats: MediaStats; 271 | } 272 | 273 | export interface MediaStats { 274 | r: RRClass | REnum; 275 | ttl: number; 276 | } 277 | 278 | export interface RRClass { 279 | ok: TentacledOk; 280 | } 281 | 282 | export interface TentacledOk { 283 | viewCount: string; 284 | } 285 | 286 | export enum REnum { 287 | Missing = "Missing", 288 | } 289 | 290 | export interface EXTMediaAvailability { 291 | status: Status | string; 292 | } 293 | 294 | export enum Status { 295 | Available = "available", 296 | } 297 | 298 | export interface EXTMediaColor { 299 | palette: Palette[]; 300 | } 301 | 302 | export interface Palette { 303 | rgb: RGB; 304 | percentage: number; 305 | } 306 | 307 | export interface RGB { 308 | red: number; 309 | green: number; 310 | blue: number; 311 | } 312 | 313 | export interface VideoInfo { 314 | aspect_ratio: number[]; 315 | duration_millis: number; 316 | variants: Variant[]; 317 | } 318 | 319 | export interface Variant { 320 | bitrate?: number; 321 | content_type: string; 322 | url: string; 323 | } 324 | 325 | export interface QuotedStatusPermalink { 326 | url: string; 327 | expanded: string; 328 | display: string; 329 | } 330 | 331 | export interface SelfThread { 332 | id: number; 333 | id_str: string; 334 | } 335 | 336 | export interface IUser { 337 | id?: number; 338 | id_str?: string; 339 | name: string; 340 | screen_name: string; 341 | location: string; 342 | description: string; 343 | url: unknown | string; 344 | entities: SourceUserEntities; 345 | protected: boolean; 346 | followers_count: number; 347 | fast_followers_count: number; 348 | normal_followers_count: number; 349 | friends_count: number; 350 | listed_count: number; 351 | created_at: string; 352 | favourites_count: number; 353 | utc_offset: unknown; 354 | time_zone: unknown; 355 | geo_enabled: boolean; 356 | verified: boolean; 357 | statuses_count: number; 358 | media_count: number; 359 | lang: unknown; 360 | contributors_enabled: boolean; 361 | is_translator: boolean; 362 | is_translation_enabled: boolean; 363 | profile_background_color: string; 364 | profile_background_image_url: unknown | string; 365 | profile_background_image_url_https: unknown | string; 366 | profile_background_tile: boolean; 367 | profile_image_url: string; 368 | profile_image_url_https: string; 369 | profile_banner_url?: string; 370 | profile_link_color: string; 371 | profile_sidebar_border_color: string; 372 | profile_sidebar_fill_color: string; 373 | profile_text_color: string; 374 | profile_use_background_image: boolean; 375 | has_extended_profile: boolean; 376 | default_profile: boolean; 377 | default_profile_image: boolean; 378 | pinned_tweet_ids: number[]; 379 | pinned_tweet_ids_str: string[]; 380 | has_custom_timelines: boolean; 381 | can_dm: boolean | null; 382 | following: boolean | null; 383 | follow_request_sent: boolean | null; 384 | notifications: boolean | null; 385 | muting: boolean | null; 386 | blocking: boolean | null; 387 | blocked_by: boolean | null; 388 | want_retweets: boolean | null; 389 | advertiser_account_type: AdvertiserAccountType | string; 390 | advertiser_account_service_levels: any[]; 391 | business_profile_state: AdvertiserAccountType | string; 392 | translator_type: AdvertiserAccountType | string; 393 | withheld_in_countries: any[]; 394 | followed_by: unknown; 395 | ext_is_blue_verified: boolean; 396 | ext_has_nft_avatar: boolean; 397 | ext_profile_image_shape: EXTProfileImageShape | string; 398 | ext: UserEXT; 399 | require_some_consent: boolean; 400 | ext_verified_type?: string; 401 | } 402 | 403 | export interface UserEXT { 404 | hasNftAvatar: HasNftAvatar; 405 | highlightedLabel: UnmentionInfo; 406 | } 407 | 408 | export interface TimelineClass { 409 | id: string; 410 | instructions: Instruction[]; 411 | responseObjects: ResponseObjects; 412 | } 413 | 414 | export interface Instruction { 415 | clearCache?: Broadcasts; 416 | addEntries?: AddEntries; 417 | } 418 | 419 | export interface AddEntries { 420 | entries: Entry[]; 421 | } 422 | 423 | export interface Entry { 424 | entryId: string; 425 | sortIndex: string; 426 | content: EntryContent; 427 | } 428 | 429 | export interface EntryContent { 430 | operation?: Operation; 431 | item?: Item; 432 | } 433 | 434 | export interface Item { 435 | content: ItemContent; 436 | clientEventInfo: ClientEventInfo; 437 | } 438 | 439 | export interface ClientEventInfo { 440 | component: Component; 441 | element: Element; 442 | details: Details; 443 | } 444 | 445 | export enum Component { 446 | ContentRecommenderExploreTweets = "content_recommender_explore_tweets", 447 | TripGeoDomainTweets = "trip_geo_domain_tweets", 448 | } 449 | 450 | export interface Details { 451 | timelinesDetails: TimelinesDetails; 452 | } 453 | 454 | export interface TimelinesDetails { 455 | controllerData: ControllerData; 456 | } 457 | 458 | export enum ControllerData { 459 | DAACDAAODAABCGABvpjoeEdLu5SAAAAA = "DAACDAAODAABCgABvpjoeEdLu5sAAAAA", 460 | } 461 | 462 | export enum Element { 463 | Tweet = "tweet", 464 | } 465 | 466 | export interface ItemContent { 467 | tweet: ContentTweet; 468 | } 469 | 470 | export interface ContentTweet { 471 | id: string; 472 | displayType: DisplayType | string; 473 | } 474 | 475 | export enum DisplayType { 476 | Tweet = "Tweet", 477 | } 478 | 479 | export interface Operation { 480 | cursor: Cursor; 481 | } 482 | 483 | export interface Cursor { 484 | value: string; 485 | cursorType: string; 486 | } 487 | 488 | export interface ResponseObjects { 489 | feedbackActions: { [key: string]: FeedbackAction }; 490 | } 491 | 492 | export interface FeedbackAction { 493 | feedbackType: FeedbackType; 494 | prompt: string; 495 | confirmation: string; 496 | feedbackUrl?: string; 497 | hasUndoAction: boolean; 498 | confirmationDisplayType: ConfirmationDisplayType; 499 | icon?: string; 500 | } 501 | 502 | export enum ConfirmationDisplayType { 503 | BottomSheet = "BottomSheet", 504 | Inline = "Inline", 505 | } 506 | 507 | export enum FeedbackType { 508 | Dismiss = "Dismiss", 509 | SeeFewer = "SeeFewer", 510 | SeeMore = "SeeMore", 511 | } 512 | -------------------------------------------------------------------------------- /src/types/req.d.ts: -------------------------------------------------------------------------------- 1 | import { Request } from "express"; 2 | import { IUser } from "./guide"; 3 | 4 | interface IAuthRequest extends Request { 5 | user: IUser; 6 | } 7 | -------------------------------------------------------------------------------- /src/types/task.ts: -------------------------------------------------------------------------------- 1 | export interface Task { 2 | flow_token: string; 3 | subtask_inputs: SubtaskInput[]; 4 | } 5 | 6 | export interface SubtaskInput { 7 | js_instrumentation?: JSInstrumentation; 8 | subtask_id: 9 | | "LoginJsInstrumentationSubtask" 10 | | "LoginEnterUserIdentifierSSO" 11 | | "EmailVerification" 12 | | "SignupReview" 13 | | "ArkoseEmail" 14 | | "SignupSettingsListEmail" 15 | | "Signup" 16 | | "LoginEnterPassword" 17 | | "SelectAvatar" 18 | | "UsernameEntryBio" 19 | | "NotificationsPermissionPrompt" 20 | | "AccountDuplicationCheck" 21 | | "UsernameEntry"; 22 | settings_list?: SettingsList; 23 | [key: string]: any; // dynamic(tm) 24 | } 25 | 26 | export interface JSInstrumentation { 27 | link: string; 28 | response: string; 29 | } 30 | 31 | export interface SettingsList { 32 | link: string; 33 | setting_responses: SettingResponse[]; 34 | } 35 | 36 | export interface SettingResponse { 37 | key: string; 38 | response_data: ResponseData; 39 | } 40 | 41 | export interface ResponseData { 42 | text_data: TextData; 43 | } 44 | 45 | export interface TextData { 46 | result: string; 47 | } 48 | 49 | export interface TaskRes { 50 | flow_token: string; 51 | status: string; 52 | subtasks: any[]; 53 | } 54 | 55 | export interface Subtask { 56 | enter_password?: EnterPassword; 57 | subtask_back_navigation?: string; 58 | subtask_id: string; 59 | open_link?: OpenLink; 60 | check_logged_in_account?: CheckLoggedInAccount; 61 | } 62 | 63 | export interface CheckLoggedInAccount { 64 | false_link: ELink; 65 | true_link: ELink; 66 | user_id: string; 67 | } 68 | 69 | export interface ELink { 70 | link_id: string; 71 | link_type: string; 72 | } 73 | 74 | export interface EnterPassword { 75 | footer: Footer; 76 | hint: string; 77 | next_link: NextLinkClass; 78 | os_content_type: string; 79 | password_field: PasswordField; 80 | primary_text: Text; 81 | skip_password_validation: boolean; 82 | user_identifier_display_type: string; 83 | username: string; 84 | } 85 | 86 | export interface Footer { 87 | footnote_text: Text; 88 | style: string; 89 | } 90 | 91 | export interface Text { 92 | entities: PrimaryTextEntity[]; 93 | text: string; 94 | } 95 | 96 | export interface PrimaryTextEntity { 97 | from_index: number; 98 | navigation_link: Link; 99 | to_index: number; 100 | } 101 | 102 | export interface Link { 103 | link_id: string; 104 | link_type: string; 105 | url: string; 106 | } 107 | 108 | export interface NextLinkClass { 109 | label: string; 110 | link_id: string; 111 | link_type: string; 112 | subtask_id?: string; 113 | } 114 | 115 | export interface PasswordField { 116 | content_type: string; 117 | detail_text: DetailText; 118 | hint_text: string; 119 | } 120 | 121 | export interface DetailText { 122 | entities: DetailTextEntity[]; 123 | text: string; 124 | } 125 | 126 | export interface DetailTextEntity { 127 | from_index: number; 128 | navigation_link: NextLinkClass; 129 | to_index: number; 130 | } 131 | 132 | export interface OpenLink { 133 | link: Link; 134 | } 135 | -------------------------------------------------------------------------------- /src/util/case.ts: -------------------------------------------------------------------------------- 1 | export function camelToSnakeCase(text: string) { 2 | return text 3 | .replace(/(.)([A-Z][a-z]+)/, "$1_$2") 4 | .replace(/([a-z0-9])([A-Z])/, "$1_$2") 5 | .toLowerCase(); 6 | } 7 | -------------------------------------------------------------------------------- /src/util/dmUtil.ts: -------------------------------------------------------------------------------- 1 | import { randInt } from "./randUtil"; 2 | 3 | const a = { 4 | conversation_id: "1338626424870039552-1483042300943122432", 5 | type: "ONE_TO_ONE", 6 | sort_event_id: "1639434080478699524", 7 | sort_timestamp: "1679706518655", 8 | participants: [ 9 | { 10 | user_id: "3701867534401305", 11 | last_read_event_id: "1639434080478699524", 12 | }, 13 | { 14 | user_id: "484648334416", 15 | last_read_event_id: "1639434080478699524", 16 | }, 17 | ], 18 | nsfw: false, 19 | notifications_disabled: false, 20 | mention_notifications_disabled: false, 21 | last_read_event_id: "1639434080478699524", 22 | read_only: false, 23 | trusted: true, 24 | muted: false, 25 | status: "AT_END", 26 | min_entry_id: "1639433688151732230", 27 | max_entry_id: "1639434080478699524", 28 | }; 29 | 30 | export function generateConversationId() { 31 | return `${randInt(19)}-${randInt(19)}`; 32 | } 33 | -------------------------------------------------------------------------------- /src/util/formatDate.ts: -------------------------------------------------------------------------------- 1 | export function formatDate(date: Date) { 2 | const daysOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; 3 | const monthsOfYear = [ 4 | "Jan", 5 | "Feb", 6 | "Mar", 7 | "Apr", 8 | "May", 9 | "Jun", 10 | "Jul", 11 | "Aug", 12 | "Sep", 13 | "Oct", 14 | "Nov", 15 | "Dec", 16 | ]; 17 | 18 | const dayOfWeek = daysOfWeek[date.getUTCDay()]; 19 | const dayOfMonth = date.getUTCDate().toString().padStart(2, "0"); 20 | const monthOfYear = monthsOfYear[date.getUTCMonth()]; 21 | const year = date.getUTCFullYear(); 22 | const hours = date.getUTCHours().toString().padStart(2, "0"); 23 | const minutes = date.getUTCMinutes().toString().padStart(2, "0"); 24 | const seconds = date.getUTCSeconds().toString().padStart(2, "0"); 25 | 26 | return `${dayOfWeek} ${monthOfYear} ${dayOfMonth} ${hours}:${minutes}:${seconds} +0000 ${year}`; 27 | } 28 | -------------------------------------------------------------------------------- /src/util/logging.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | 3 | export function log(message?: any, ...optionalParams: any[]): void { 4 | optionalParams 5 | ? console.log(`${chalk.green("[LOG]")} ${message} ${optionalParams}`) 6 | : console.log(`${chalk.green("[LOG]")} ${message}`); 7 | } 8 | 9 | export function warn(message?: any, ...optionalParams: any[]): void { 10 | optionalParams 11 | ? console.log(`${chalk.yellow("[WARN]")} ${message} ${optionalParams}`) 12 | : console.log(`${chalk.yellow("[WARN]")} ${message}`); 13 | } 14 | 15 | export function error(message?: any, ...optionalParams: any[]): void { 16 | optionalParams 17 | ? console.log(`${chalk.red("[ERR]")} ${message} ${optionalParams}`) 18 | : console.log(`${chalk.red("[ERR]")} ${message}`); 19 | } 20 | -------------------------------------------------------------------------------- /src/util/notifications.ts: -------------------------------------------------------------------------------- 1 | import Notification from "../models/Notification"; 2 | import User from "../models/User"; 3 | import { randStr } from "./randUtil"; 4 | 5 | /** 6 | * Adds a notification to a given user 7 | * @param {string} text The message. `%1` is automatically replaced with the username of the recipient. 8 | * @param {string} recipient The user ID to add the notification to 9 | * @param {"like" | "reply" | "retweet" | "internal"=} type The type of notification. Internal has the Twitter icon, and disallows `sender`. 10 | */ 11 | export async function addNotification( 12 | text: string, 13 | type: "internal", 14 | recipient: string 15 | ): Promise; 16 | /** 17 | * Adds a notification to a given user 18 | * @param {string} text The message. `%1` is automatically replaced with the username of the sender. 19 | * @param {string} recipient The user ID to add the notification to 20 | * @param {"like" | "reply" | "retweet" | "internal"=} type The type of notification. Internal has the Twitter icon, and disallows `sender`. 21 | * @param {string} sender The user who added the notification 22 | */ 23 | export async function addNotification( 24 | text: string, 25 | type: "like" | "reply" | "retweet", 26 | recipient: string, 27 | sender: string, 28 | tweet: string 29 | ): Promise; 30 | /** 31 | * Adds a notification to a given user 32 | * @param {string=} text The message. `%1` is automatically replaced with the username. 33 | * @param {string=} recipient The user ID to add the notification to 34 | * @param {"like" | "reply" | "retweet" | "internal"=} type The type of notification. Internal has the Twitter icon, and disallows `sender`. 35 | * @param {string=} sender The user who added the notification 36 | */ 37 | export async function addNotification( 38 | text: string, 39 | type: "like" | "reply" | "retweet" | "internal", 40 | recipient: string, 41 | sender?: string, 42 | tweet?: string 43 | ) { 44 | const id = randStr(64); 45 | if (sender) { 46 | // user-based 47 | const icon = (() => { 48 | if (type === "like") return "heart_icon"; 49 | if (type === "reply") return "reply_icon"; 50 | if (type === "retweet") return "reweet_icon"; 51 | else return "bird_icon"; 52 | })(); 53 | const recipientUser = await User.findOne({ id_string: recipient }); 54 | if (!recipientUser) throw new Error(`Recipient not found`); 55 | const senderUser = await User.findOne({ id_string: sender }); 56 | if (!senderUser) throw new Error(`Sender not found`); 57 | const noti = new Notification({ 58 | id, 59 | message: { 60 | text: text.replaceAll("%1", senderUser.name!), 61 | rtl: false, 62 | entities: [ 63 | { 64 | fromIndex: 0, 65 | toIndex: senderUser.name!.length, 66 | ref: { 67 | user: { 68 | id: sender, 69 | }, 70 | }, 71 | }, 72 | ], 73 | }, 74 | icon: { 75 | id: icon, 76 | }, 77 | timestampMs: Date.now(), 78 | template: { 79 | aggregateUserActionsV1: { 80 | targetObjects: [ 81 | { 82 | tweet: { 83 | id: tweet, 84 | }, 85 | }, 86 | ], 87 | fromUsers: [ 88 | { 89 | user: { 90 | id: sender, 91 | }, 92 | }, 93 | ], 94 | }, 95 | }, 96 | }); 97 | recipientUser.notification_ids.push(id); 98 | await noti.save(); 99 | await recipientUser.save(); 100 | return; 101 | } else { 102 | // sent from admin 103 | const recipientUser = await User.findOne({ id_string: recipient }); 104 | if (!recipientUser) throw new Error(`Recipient not found`); 105 | recipientUser.notification_ids.push(id); 106 | 107 | const noti = new Notification({ 108 | id, 109 | message: { 110 | text: text.replaceAll("%1", recipientUser.name!), 111 | rtl: false, 112 | entities: [ 113 | { 114 | fromIndex: 0, 115 | toIndex: recipientUser.name!.length, 116 | ref: { 117 | user: { 118 | id: recipient, 119 | }, 120 | }, 121 | }, 122 | ], 123 | }, 124 | icon: { 125 | id: "bird_icon", 126 | }, 127 | timestampMs: Date.now(), 128 | }); 129 | await noti.save(); 130 | await recipientUser.save(); 131 | return; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/util/randUtil.ts: -------------------------------------------------------------------------------- 1 | export const randInt = (length: number) => { 2 | return Math.floor( 3 | Math.pow(10, length - 1) + 4 | Math.random() * (Math.pow(10, length) - Math.pow(10, length - 1) - 1) 5 | ); 6 | }; 7 | 8 | export const randStr = (len: number) => 9 | [...Array(len)].reduce( 10 | (a) => 11 | a + 12 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"[ 13 | ~~( 14 | Math.random() * 15 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" 16 | .length 17 | ) 18 | ], 19 | "" 20 | ); 21 | -------------------------------------------------------------------------------- /src/util/routeUtil.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | 3 | export function searchDirForTsFiles(dirPath: string): string[] { 4 | const files = fs.readdirSync(dirPath); 5 | 6 | let tsFiles: string[] = []; 7 | 8 | for (const file of files) { 9 | const filePath = `${dirPath}/${file}`; 10 | const stat = fs.statSync(filePath); 11 | 12 | if (stat.isDirectory()) { 13 | // Recursively search subdirectory 14 | const subDirTsFiles = searchDirForTsFiles(filePath); 15 | tsFiles = tsFiles.concat(subDirTsFiles); 16 | } else if (filePath.endsWith(".ts")) { 17 | tsFiles.push(filePath); 18 | } 19 | } 20 | 21 | return tsFiles; 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", 5 | "lib": ["ES2021"], 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "skipLibCheck": true 11 | } 12 | } 13 | --------------------------------------------------------------------------------