├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ └── pack.yml
├── .gitignore
├── LICENSE
├── README.md
├── docs
├── README_JA.md
└── README_KO.md
├── files
├── bundle.css
├── bundle.js
├── decider.json
├── index.html
├── twitter-text.js
├── vendor.js
└── version.json
├── images
├── logo128.png
├── logo144.png
├── logo16.png
├── logo192.png
├── logo32.png
├── logo48.png
├── logo512.png
├── logo96.png
├── tweetdeck.png
└── tweetdeck.svg
├── manifest.json
├── pack.js
├── package-lock.json
├── package.json
├── ruleset.json
└── src
├── background.js
├── challenge.js
├── content.js
├── destroyer.js
├── injection.js
├── interception.js
└── notifications.js
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | Include version of extension. **Make sure you're using last version before submitting a bug report! Check releases page before reporting anything!**
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | Keep in mind that OldTweetDeck is generally not made to enhance TweetDeck, just to return it. All of TweetDeck's code is minified and it's very hard to add something into it. If you propose a new feature, there's a huge chance that it won't be added.
11 | You might be interested in [BetterTweetDeck](https://github.com/dimdenGD/BetterTweetDeck), which is a project that focuses on improving TweetDeck. It also doesn't get new features except bug fixes, but if you haven't checked it yet, there's a chance it already has a feature you want.
12 |
--------------------------------------------------------------------------------
/.github/workflows/pack.yml:
--------------------------------------------------------------------------------
1 | name: Pack Extension
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Checkout Source Tree
16 | uses: actions/checkout@v4
17 |
18 | - name: Setup Node.js environment
19 | uses: actions/setup-node@v4
20 |
21 | - name: Install ZIP
22 | uses: montudor/action-zip@v1
23 |
24 | - name: Setup NPM packages
25 | run: npm install
26 |
27 | - name: Pack extension
28 | run: npm run build
29 |
30 | - name: Unzip extension
31 | run: unzip -qq ../OldTweetDeckChrome.zip -d OldTweetDeckChrome; unzip -qq ../OldTweetDeckFirefox.zip -d OldTweetDeckFirefox
32 | working-directory: ${{ github.workspace }}
33 |
34 | - name: Upload for Firefox
35 | uses: actions/upload-artifact@v4
36 | with:
37 | name: OldTweetDeckFirefox
38 | path: ${{ github.workspace }}/OldTweetDeckFirefox
39 | - name: Upload for Chromium
40 | uses: actions/upload-artifact@v4
41 | with:
42 | name: OldTweetDeckChrome
43 | path: ${{ github.workspace }}/OldTweetDeckChrome
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | _metadata/
2 | node_modules/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2023 dimden (dimden.dev)
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OldTweetDeck
2 |
3 | Returns old TweetDeck, for free!
4 | > [!IMPORTANT]
5 | > Do not use Search columns unless you have X Premium subscription.
6 | > If you do search for something (not recommended), remove the column immediately after you're done.
7 |
8 | > [!NOTE]
9 | > Since Twitter made likes private, the Likes tab aren't loading anymore.
10 | > This can not be fixed since the API to retrieve the likes is gone. Same thing with Activity column.
11 |
12 | > If you're interested in getting 2015-2018 Twitter back, you can also check out [OldTwitter](https://github.com/dimdenGD/OldTwitter) extension.
13 |
14 | 
15 |
16 | ### Other languages
17 |
18 | [한국어 README](docs/README_KO.md)
19 | [日本語 README](docs/README_JA.md)
20 |
21 | ## Installation
22 |
23 | Note: Do not delete the extension files (unzipped archive for Chromium, zip file for Firefox) after installation.
24 |
25 | ### Chromium (Chrome, Edge, Opera, Brave, Etc.)
26 |
27 | 1. Go to [Release page](https://github.com/dimdenGD/OldTweetDeck/releases) and download `OldTweetDeckChrome.zip`
28 | 2. Unzip the archive
29 | 3. Go to extentions page (`chrome://extensions`)
30 | 4. Enable developer mode (there should be switch somewhere on that page)
31 | 5. Press "Load unpacked" button
32 | 6. Select folder with unzipped archive
33 | 7. Go to https://x.com/i/tweetdeck and enjoy old TweetDeck
34 | 8. [Donate to encourage continued support](https://www.patreon.com/dimdendev)
35 |
36 | ### Firefox
37 |
38 | #### Nightly / Developer Edition
39 |
40 | 1. Go to [Release page](https://github.com/dimdenGD/OldTweetDeck/releases) and download `OldTweetDeckFirefox.zip`
41 | 2. Go to Firefox Configuration Editor (`about:config`)
42 | 3. Change the preference `xpinstall.signatures.required` to false
43 | 4. Go to addons page(`about:addons`)
44 | 5. Press "Install Add-on From File..." button
45 | 6. Select zip file you downloaded
46 | 7. Go to https://x.com/i/tweetdeck and enjoy old TweetDeck
47 | 8. [Donate to encourage continued support](https://www.patreon.com/dimdendev)
48 |
49 | #### Stable
50 |
51 | **It's not recommended to use this extension on Stable version.**
52 |
53 | 1. Go to `about:debugging#/runtime/this-firefox`
54 | 2. Press "Load Temporary Add-on" and select zip file you downloaded
55 | 3. **Installing this way on Firefox will remove it after closing browser.**
56 |
57 | ### Safari
58 |
59 | NOT SUPPORTED
60 |
61 | ## Updating
62 |
63 | If TweetDeck's files were updated, you should receive updated files automatically without having to reinstall after refreshing tab (unless you set `localStorage.OTDalwaysUseLocalFiles = '1'`).
64 | If extension files were updated, you have to reinstall extension to get new update.
65 |
66 | ## Better TweetDeck
67 |
68 | I've made a fork of BetterTD that works with this extension, you can find it [here](https://github.com/dimdenGD/BetterTweetDeck/releases).
69 | Install it the same way as this extension.
70 |
71 | ## FAQ
72 |
73 | #### Extension stopped working.
74 |
75 | Before opening an issue, try to reinstall extension.
76 |
77 | #### There is a warning: Manifest version 2 is deprecated, and support will be removed in 2023.
78 |
79 | Update to latest OldTweetDeck version.
80 |
81 | #### 'Link another account you own' doesn't work.
82 |
83 | See https://github.com/dimdenGD/OldTweetDeck/issues/259#issuecomment-2281786253 for a workaround.
84 |
85 | #### User or Search column aren't loading for me.
86 |
87 | You're getting rate limited. They'll comeback after some time.
88 |
89 | #### Likes tab aren't loading for me.
90 |
91 | This can not be fixed. Since Twitter made likes private, the API to retrieve likes is gone.
92 |
--------------------------------------------------------------------------------
/docs/README_JA.md:
--------------------------------------------------------------------------------
1 | # OldTweetDeck
2 | 昔のTweetDeckを取り戻しましょう!無料で!
3 |
4 | > [!IMPORTANT]
5 | > Xプレミアムのサブスクリプションを持っていない限り、検索カラムを使わないでください。
6 | > もし何かしらの理由で検索カラムを使う場合(おすすめしません)は、検索し終わった後すぐにカラムを削除してください。
7 |
8 | > [!NOTE]
9 | > Twitterがいいねを非公開にしたため、いいねタブは読み込まれなくなりました。
10 | > いいねを取得するAPIが存在しなくなったため、これを修正することはできません。アクティビティタブにおいても同様です。
11 |
12 | > また、2015〜2018年のTwitterを取り戻したい場合は [OldTwitter](https://github.com/dimdenGD/OldTwitter) 拡張機能もチェックしてみてください。
13 |
14 | 
15 |
16 | ### 他の言語での説明
17 | [English README](/README.md)
18 | [한국어 README](/docs/README_KO.md)
19 |
20 | ## インストール方法
21 |
22 | 注意: 拡張機能ファイル(Chromiumの場合はZipファイルを展開したもの、Firefoxの場合はZipファイル)をインストール後に削除しないでください。
23 |
24 | ### Chromium (Chrome, Edge, Opera, Brave など)
25 |
26 | 1. [リリースページ](https://github.com/dimdenGD/OldTweetDeck/releases) から `OldTweetDeckChrome.zip` をダウンロードする
27 | 2. Zipファイルを展開する
28 | 3. 拡張機能ページを開く (`chrome://extensions`)
29 | 4. デベロッパーモードを有効にする (拡張機能ページの右上に切り替えスイッチがあります)
30 | 5. "パッケージ化されていない拡張機能を読み込む"をクリックする
31 | 6. Zipファイルを展開したフォルダを選択する
32 | 7. https://x.com/i/tweetdeck にアクセスして昔のTweetDeckを楽しむ
33 | 8. [開発が継続できるように支援する](https://www.patreon.com/dimdendev)
34 |
35 | ### Firefox
36 |
37 | #### Nightly / Developer Edition
38 |
39 | 1. [リリースページ](https://github.com/dimdenGD/OldTweetDeck/releases) から `OldTweetDeckFirefox.zip` をダウンロードする
40 | 2. 高度な設定ページを開く (`about:config`)
41 | 3. `xpinstall.signatures.required` の設定を false に変更する
42 | 4. アドオンページを開く (`about:addons`)
43 | 5. アドオンページ右上の歯車アイコン→"ファイルからアドオンをインストール..."をクリックする
44 | 6. ダウンロードしたZipファイルを選択する
45 | 7. https://x.com/i/tweetdeck にアクセスして昔のTweetDeckを楽しむ
46 | 8. [開発が継続できるように支援する](https://www.patreon.com/dimdendev)
47 |
48 | #### Stable
49 |
50 | **Stableバージョンでこの拡張機能を使うことは推奨されません**
51 |
52 | 1. `about:debugging#/runtime/this-firefox`を開く
53 | 2. "一時的なアドオンを読み込む..."をクリックしてダウンロードしたZipファイルを選択する
54 | 3. **この方法でインストールした場合、Firefoxを閉じた際に拡張機能が削除されます**
55 |
56 | ### Safari
57 |
58 | 対応していません
59 |
60 | ## 更新
61 |
62 | TweetDeckのファイルが更新された場合は、タブを再読み込みするだけで拡張機能を再インストールすることなく自動的に更新を受け取れます。 (`localStorage.OTDalwaysUseLocalFiles = '1'`の設定をしている場合を除く)
63 | 拡張機能のファイルが更新された場合は、拡張機能を再インストールすることで更新を受け取れます。
64 |
65 | ## Better TweetDeck
66 |
67 | この拡張機能と一緒に使うことが出来るBetter TweetDeckの[fork](https://github.com/dimdenGD/BetterTweetDeck/releases)を作りました。
68 | この拡張機能のインストール方法と同じ方法でインストールすることができます。
69 |
70 | ## よくある質問
71 |
72 | #### 拡張機能が動かなくなりました
73 |
74 | 不具合報告をする前に、拡張機能を再度インストールしてみてください。
75 |
76 | #### Manifest version 2 is deprecated, and support will be removed in 2023. という警告が表示されました
77 |
78 | 最新版のOldTweetDeckに更新して下さい。
79 |
80 | #### 「Link another account you own」からアカウントを追加できません
81 |
82 | 一時的な解決策として https://github.com/dimdenGD/OldTweetDeck/issues/259#issuecomment-2281786253 を参考にして下さい。
83 |
84 | #### ユーザーカラムや検索カラムが読み込まれません
85 |
86 | API制限に達しているため読み込めていません。しばらくしてAPI制限が解除されるとまた表示されます。
87 |
88 | #### いいねタブが読み込まれません
89 |
90 | Twitterがいいねを非公開にしたことに伴い、いいねを取得するAPIがなくなりました。そのため、修正することができません。
91 |
92 | ## 更新履歴
93 |
94 | ### 4.0.7
95 | * ツイートが翻訳できない問題を修正
96 | * 検索カラムとユーザーカラムの安定性を少し向上
97 |
98 | ### 4.0.6
99 | * ツイートが拡張出来ない問題をおそらく修正
100 |
101 | ### 4.0.4
102 | * OldTweetDeckが読み込まれない問題を修正
103 |
104 | ### 4.0.3
105 | * プロフィールが読み込まれない問題を修正
106 |
107 | ### 4.0.2
108 | * DMで画像が読み込まれない問題を修正
109 | * 自分のいいねカラムが読み込まれない問題を修正
110 | * Twitterによるブロックを改善
111 | * これによりショートカットキーが機能しない、奇妙なスタイルになってしまうなどの問題が修正されるはずです。
112 | * コンソールで`localStorage.secureRequests = 1`を実行できるようになりました。
113 | * これを実行することにより、リクエストの署名が必須になります。アカウントがより安全になりますが、TweetDeckを正しく読み込むために何度もページをリロードする必要があるかもしれません。
114 |
115 | ### 4.0.1
116 | これはOldTweetDeckの新しいメジャーバージョンです。
117 | ご存知の場合もあるかとは存じますが、Chromeは拡張機能においてManifest V2を廃止し、Manifest V3へ移行しようとしています。
118 | OldTweetDeckはずっとManifest V2を使用していたため、Manifest V3を使用するようにコードを組み直しました。
119 | これにより、Manifest V2の廃止後も引き続き拡張機能が動作するはずです。
120 | この変更は少し試験的なものであるため、バグなどを発見した場合はご報告ください。
121 | また、`OldTweetDeck v3.6.8`も現状まだ使える状態ですので、もし`v4.x`が動かない場合は引き続き`v3.6.8`をご利用いただけます。
122 |
123 | **開発者はウクライナ在住で、かなりひどい状況にあります。可能であれば[寄付](https://dimden.dev/donate/)をお願いいたします。**
124 | **いつもご支援いただきありがとうございます。**
125 |
126 |
127 | 過去の更新履歴
128 |
129 | ### 3.6.8
130 | * リツイートができない問題を修正
131 |
132 | ### 3.6.5
133 | 現在、開発に割ける時間があまりないため、ベータリリースのようなものになります
134 |
135 | * アカウントをチームに追加できない問題をおそらく修正
136 | * リプライがスパム判定される問題をおそらく修正
137 |
138 | ### 3.6.4
139 | * Twitter側のコード変更によるOldTweetDeckが起動できない問題を修正
140 |
141 | ### 3.6.2
142 | * スタイル表示を修正
143 | * チーム機能を修正
144 |
145 | ### 3.6.1
146 | * ツイート作成パネルが正しく表示されない問題を修正
147 | * カラムが読み込まれない問題とツイートが送信されない問題を修正
148 |
149 | ### 3.6
150 | OldTweetDeckが`x.com`上で動作しない問題を修正しました。
151 | (訳者注: TweetDeckのURLが`twitter.com/i/tweetdeck`から`x.com/i/tweetdeck`に変わりました)
152 |
153 | ### 3.5.5
154 | * 読み込まれない問題を修正
155 |
156 | ### 3.5.4
157 | * エラーハンドリングを改善
158 |
159 | ### 3.5.3
160 | * ウィルス対策ソフトに誤検知される問題を修正
161 |
162 | ### 3.5
163 | * ログアウトやロックされる問題をおそらく修正できました。
164 | * このバージョンでは、ウェブ版のTwitterと比較してOldTweetDeckのリクエストに不足していた最後のヘッダーをついに実装しました。
165 | * このジェネレーターは非常に難読化されており、基本的にはリクエストが実際にウェブ版のTwitterから行われているかどうかのセキュリティチェックを行います。
166 | * 通常のリクエストと違うのはこれだけなので、これがログアウトやロックされる原因であることを祈ります。
167 |
168 | ### 3.4.0
169 | * メインアカウント以外で他のユーザーのリプライが表示される問題を修正
170 | * フォロー中のユーザーが自分自身にリプライをしている場合、他のユーザーのリプライが表示される問題を修正
171 | * 複数アカウントでの"いいね"メニューが常にアカウントをプライベートアカウントとして表示し、"いいね"の状態が正しく表示されない問題を修正
172 |
173 | ### 3.3.3
174 | * カラムが消える問題を更に修正
175 |
176 | ### 3.3.2
177 | * ブラウザに保存された状態をインポート(取込)/エクスポート(出力)するためのボタンを追加
178 | 
179 |
180 | ### 3.3.1
181 | * フィルターを変更するとカラムが消える問題を修正
182 |
183 | ### 3.3.0
184 | * OldTweetdeckが読み込まれない問題を修正
185 | * Twitterがカラムの位置と全体の状態を保存するAPIを削除したため、それらのAPIを再現しブラウザ内に状態を保存するようにしました
186 | * **TweetDeckの状態がリセットされるため、更新前でTweetDeckを表示できている場合は検索クエリやカラムの配置をメモしておくことをおすすめします!**
187 |
188 | ### 3.2.3
189 | * 複数アカウントでのリプライのフィルタリングに関する不具合を修正
190 | * 日本語でツイートの展開が動作しない問題を修正
191 |
192 | ### 3.2.2
193 | * フォローしていないアカウントへのリプライがホームタイムラインで表示される問題を修正
194 | * この挙動が気に入った人向けにオプションで切り替え可能にしました
195 | 
196 |
197 | ### 3.2.1
198 | * いいねを読み込めるように修正
199 | * いいねが動作するように修正
200 | * コレクションを読み込めるように修正
201 | * メンションを読み込めるように修正
202 |
203 | ### 3.2.0
204 | * ホームタイムラインを読み込めるように修正
205 | * リストを読み込めるように修正
206 | * 通知が動作するように修正
207 |
208 | ### 3.1.9
209 | tweetdeck.com のリダイレクトを修正
210 |
211 | ### 3.1.8
212 | * ツイートをブックマーク (`Bookmark tweet`) ボタンを追加
213 | * 三点リーダー (`…`) でハッシュタグとリンクが動作しなくなる問題を修正
214 | * 長いツイートを表示するとリンクが失われる問題を修正
215 |
216 | ### 3.1.7
217 | * API制限を緩和する機能を再び追加しました!
218 | 
219 | 
220 |
221 | TweetDeckの設定ボタンから設定画面を開き、`Enable rate limit bypass (OldTweetDeck)`をチェックすることで有効にできます
222 |
223 | API制限の緩和機能を利用する場合、以下の点にご留意ください
224 | * API制限を受けた後に緩和機能を有効にした場合、OldTweetDeckは動作を再開するはずですが、Twitter Webはしばらくの間API制限が続く可能性があります
225 | * API制限の緩和機能の利用はリスクが高くなります、自己責任でご利用ください
226 | * この機能はしばらくテストされていないため、実際長期的にうまく機能するかどうか不明です
227 | * 単なる理論的な考えですが、この機能を有効にしてもAPI制限を受けている場合、この機能を無効にするとAPI制限が解除される可能性があります
228 |
229 | その他の更新内容
230 | * 長いツイートを表示するためのボタンを追加
231 | * 三点リーダー (`…`) が表示されない問題を修正
232 | * 長いリツイートが正しく展開されない問題を修正
233 | * 展開されたツイートの末尾にある`t.co`のリンクを削除するように修正
234 |
235 | #### 3.1.6
236 | バージョン更新
237 |
238 | #### 3.1.5
239 | 長いツイートを表示するためのボタンを追加
240 |
241 | #### 3.1.4
242 | * ツイートを投稿できない問題を修正
243 | * リツイートができない問題を修正
244 | * ツイートが削除できない問題を修正
245 | * リプライが正しく表示されない問題を修正
246 |
247 | 以下の機能はもう動作しません。
248 | * API制限の緩和
249 | * アクティビティタブ
250 |
251 | #### 3.1.3
252 | リプライが表示されない問題を修正
253 |
254 | #### 3.1.2
255 | ツイートが削除できない問題を修正
256 |
257 | #### 3.1.1
258 | ツイートができない問題を修正
259 |
260 | #### 3.1.0
261 | READMEを更新
262 |
263 | #### 3.0.8
264 | バージョン更新
265 |
266 | #### 3.0.7
267 | 日本語READMEを追加
268 |
269 | #### 3.0.6
270 | このバージョンではリクエストに傍受を追加しました。
271 | 通常のTwitterをリバースエンジニアリングして、通常のTwitterで使用される対応するリクエストを見つけました。
272 | TweetDeckがシャットダウンAPIを使用しようとすると、リクエストは新しいエンドポイントにリダイレクトされ、結果は古いフォーマットに変換されます。
273 |
274 | ユーザーカラムと検索カラムがAPI制限の影響を受けるようになったことにより読み込めない問題を修正しました。
275 | 最終的にはさらに多くのAPIが壊れることが予想されますが、その度に新しい動作するAPIに置き換えていく予定です。
276 | 実行されるリクエストは通常のTwitterのリクエストと同じであるため、安全です。
277 |
278 | #### 3.0.5
279 | バージョン更新
280 |
281 | #### 3.0.4
282 | API上限を倍に緩和
283 |
284 | #### 3.0.3
285 | 固定ツイートも表示するように変更 (最近のツイートの場合)
286 |
287 | #### 3.0.2
288 | 複数アカウントのタイムラインに常にメインアカウントが表示される問題を修正
289 |
290 | #### 3.0.1
291 | バージョン更新
292 |
293 | #### 3.0.0
294 | リファラーを削除
295 |
296 | #### 2.0.5
297 | 2.0.4で修正した問題がFirefoxで発生していたのを修正
298 |
299 | #### 2.0.4
300 | 新TweetDeckが表示される場合がある問題を修正
301 |
302 | #### 2.0.3
303 | manifest V2 がFirefoxで動作しない問題を修正
304 |
305 | #### 2.0.2
306 | クリックが反応しない問題を修正
307 |
308 | #### 2.0.1
309 | 新TweetDeckのheadとbodyを削除
310 |
311 | #### 2.0.0
312 | manifest V2 で作り直し外部サーバーを必要としないように変更
313 |
314 | #### 1.0.2
315 | 恐らく動作する
316 |
317 |
318 |
319 | ## 日本語翻訳
320 | [@katabame](https://twitter.com/katabame)
321 | 以下の時点の内容を基に翻訳されています。
322 | * README: commit [fd974f4](https://github.com/dimdenGD/OldTweetDeck/commit/fd974f4716e0d271ca3719ff71e342ca84fb9b98)
323 | * 更新履歴: release/tag [v4.0.4](https://github.com/dimdenGD/OldTweetDeck/releases/tag/v4.0.4)
324 |
--------------------------------------------------------------------------------
/docs/README_KO.md:
--------------------------------------------------------------------------------
1 | # OldTweetDeck
2 | 옛날 트윗덱을 공짜로 되돌립니다!
3 | 현재 쉽게 설치하는 방법은 없지만, 이 리포지토리를 다운하고 크롬/엣지/오페라/웨일 등의 다른 크로미움 브라우저에 이용하능합니다.
4 |
5 | 2015년과 2018년의 트위터를 다시 이용하는 것에 관심있다면, [OldTwitter](https://github.com/dimdenGD/OldTwitter) 확장프로그램도 한번 보세요.
6 |
7 | 
8 |
9 | ## 설치
10 | 메모: 설치 후에 확장프로그램 파일(크로미움의 경우 압축 해제한 폴더, 파이어폭스의 경우 ZIP파일)을 삭제하지 마세요.
11 | ### 크로미움 (크롬, 엣지, 오페라, Brave, 웨일 등)
12 | 1. [배포 페이지](https://github.com/dimdenGD/OldTweetDeck/releases)로 가서 `OldTweetDeckChrome.zip`을 다운로드 합니다.
13 | 2. 파일의 압축을 풉니다.
14 | 3. 확장 프로그램 페이지(``chrome://extensions``)로 접속합니다.
15 | 4. 개발자 모드를 활성화 합니다. (페이지 어딘가에 스위치가 있을 것입니다.)
16 | 5. "압축해제된 확장프로그램을 로드합니다" 버튼을 누릅니다.
17 | 6. 압축해제한 파일의 폴더를 선택합니다.
18 | 7. tweetdeck.twitter.com으로 가서 옛날 트윗덱을 즐깁니다.
19 | 8. [지속적인 지원을 위해 기부해주시면 감사합니다](https://www.patreon.com/dimdendev)
20 |
21 | ### 파이어폭스
22 | #### Nightly / 개발자 버전
23 | 1. [배포 페이지](https://github.com/dimdenGD/OldTweetDeck/releases)로 가서 `OldTweetDeckFirefox.zip`을 다운로드 합니다.
24 | 2. Firefox Configuration Editor (`about:config`)로 접속합니다.
25 | 3. `xpinstall.signatures.required`을 검색하여 false로 바꿔줍니다.
26 | 4. 부가 기능 관리자 페이지(``about:addons``)에 접속합니다
27 | 5. "파일에서 부가 기능 설치..." 버튼을 누릅니다.
28 | 6. 다운로드한 zip 파일을 선택합니다.
29 | 7. tweetdeck.twitter.com으로 가서 옛날 트윗덱을 즐깁니다.
30 | 8. [지속적인 지원을 위해 기부해주시면 감사합니다](https://www.patreon.com/dimdendev)
31 |
32 | #### 안정 버전
33 | **안정 버전에서 확장프로그램을 이용하는 것은 추천하지 않습니다.**
34 | 1. `about:debugging#/runtime/this-firefox`으로 갑니다.
35 | 2. "임시 부가 기능 로드..."를 누르고 다운로드한 zip 파일을 선택합니다.
36 | 3. **이 방법으로 파이어폭스에 설치할 경우, 브라우저가 닫히고 나서 삭제될 것입니다.**
37 |
38 | ### 사파리
39 | 지원안함
40 |
41 | ## 업데이트
42 | 트윗덱 서버쪽의 파일이 업데이트 되면, (설정에서 `localStorage.OTDalwaysUseLocalFiles = 1`을 작성하지 않았을 경우) 재설치할 필요 없이 새로고침만으로 자동으로 새로운 파일이 업데이트 됩니다.
43 | 확장 프로그램 파일 자체가 업데이트 되면, 확장프로그램을 삭제후 재설치 하세요.
44 |
45 |
46 | ## Better TweetDeck
47 | Better TweetDeck이 이 확장프로그램과 장동하도록 만든 포크가 있습니다, [여기](https://github.com/dimdenGD/BetterTweetDeck/)에서 찾을 수 있으며. 이 확장프로그램과 같은 방식으로 설치할 수 있으며, [릴리즈 페이지](https://github.com/dimdenGD/BetterTweetDeck/releases)에서 반드시 다운로드해야 합나다.
48 |
49 | ## FAQ
50 | #### Manifest version 2 is deprecated, and support will be removed in 2023. 이라는 경고가 뜹니다.
51 | 무시하세요
52 |
53 | #### 유저(User) 또는 검색(Search) 열이 로딩되지 않습니다.
54 | API 제한에 도달했습니다. 일정 시간이 지난 후에 원래대로 돌아올 것입니다.
55 |
56 |
57 | ## 업데이트 내역
58 | ### 이 프로젝트를 계속 유지하기 위해 [기부](https://dimden.dev/donate/)를 고려해 주세요.
59 |
60 | 업데이트 내역
61 |
62 | #### 3.1.8
63 | * "북마크 트윗" 버튼 추가!
64 | * 일식 심볼이 해시태그와 링크를 부수는 현상 수정
65 | * 트윗 확장이 링크를 비활성화 하는 문제 해결
66 | #### 3.1.7
67 | * 리밋을 우회하는 설정이 추가되었습니다! 사용하려면:
68 |
69 | 
70 | 
71 |
72 | 리밋 우회와 관련하여 유의해야 할 사항:
73 | * 속도 제한을 받은 후에 우회를 활성화한 경우 OldTweetDeck은 작동하지만 웹의 Twitter는 한동안 속도 제한을 받을 수 있습니다.
74 | * 분명히, 이것을 사용하는 것은 안하는 것보다 더 위험합니다. 자신의 위험을 무릅쓰고 사용해야 합니다.
75 | * 한동안 테스트하지 않았기 때문에 실제로 장기적으로 잘 작동하는지 모릅니다. 한동안 이 설정을 활성화했지만 여전히 속도 제한이 있는 경우 모순적이게도 이 설정을 비활성화하면 속도 제한을 제거하는 데 도움이 될 수 있습니다(이론상 입니다)
76 | 기타 수정사항:
77 | * 긴 리트윗이 올바르게 확장되지 않는 문제 해결
78 | * 확장된 트윗의 끝에 있는 t.co 링크를 제거했습니다.
79 | #### 3.1.6
80 | * 일부 사용자에 대해 답장이 로드되지 않는 문제 해결.
81 | #### 3.1.5
82 | * 긴 텍스트 트윗을 위한 "트윗 확장" 버튼 추가
83 | * "..."(3개의 점 기호)가 트윗의 시작과 끝에서 제거되는 문제 해결
84 | * 트윗 길이 계산이 미스 해결(트윗-텍스트 라이브러리 업데이트)
85 | #### 3.1.4
86 | 대부분 다 고쳤지만:
87 | * 리밋 제한 우회가 더 이상 작동하지 않습니다
88 | * 활동(Activity) 탭이 더 이상 작동하지 않습니다
89 | 알려진 문제:
90 | * ~~트윗을 게시할 수 없음~~ 해결
91 | * ~~리트윗이 작동하지 않음~~ 해결
92 | * ~~삭제가 작동하지 않음~~ 해결
93 | * ~~답장이 제대로 표시되지 않음~~ 해결
94 | * ~~인용 트윗에 인용 트윗이 포함되지 않음~~ 해결
95 | #### 3.1.3
96 | 답장 해결
97 | #### 3.1.2
98 | 트윗 삭제 해결
99 | #### 3.1.1
100 | 트윗 해결
101 | #### 3.1.0
102 | katabame/main과 분기 병합
103 | readme 수정
104 | #### 3.0.8
105 | 이 버전은 요청에 가로채기를 추가했습니다. 일반 트위터의 요청을 리버스앤지니어링 하여서, 일반 트위터에서 사용하는 해당 요청을 찾을 수 있었습니다. 이제 트윗댁이 종료 API를 사용하려고 하면, 요청이 새 엔드포인트로 리다이렉트 되고, 결과가 옛날 포멧으로 변환됩니다.
106 |
107 | 이것은 유저(User)와 검색(Search)열을 고칩니다. 저는 더 많은 API가 고장날 것으로 예상합니다. API가 고장나면 작동하는 API로 교체하는 것을 계속 할 것입니다. 제가 작성하는 요청은 일반 트위터 요청과 동일하므로 안전하다는 것을 명심하십시오. 유저 및 검색 열은 이제 속도제한의 영향을 받습니다. 해당 열이 로드되지 않으면 F12를 누른 뒤, 콘솔(Console)탭으로 가서 `localStorage.abuseAPIkeys = '1'`을 작성한 뒤 엔터를 누르세요. 이렇게 하면 리밋이 두 배로 늘어납니다.
108 |
109 | BetterTweetDeck을 설치하셨다면 OldTweetdeck V3에 작동하도록 수정한 [새로운 업데이트](https://github.com/dimdenGD/BetterTweetDeck/releases/tag/v4.11.1)가 있습니다.
110 | #### 3.0.7
111 | main 분기 병합
112 |
113 | #### 3.0.6
114 | 로그 제거
115 | #### 3.0.5
116 | 버전 업데이트
117 | #### 3.0.4
118 | 리밋을 2배로 늘립니다.
119 | #### 3.0.3
120 | 고정된 트윗을 보여줍니다(만약에 최신에 있을 경우)
121 | #### 3.0.2
122 | 여러 유저의 타임라인이 주계정만 보여주는 문제를 해결했습니다.
123 | #### 3.0.1
124 | 버전 충돌
125 | #### 3.0.0
126 | 리퍼러 제거
127 | #### 2.0.5
128 | 2.0.4의 문제가 파이어폭스에서 여전히 발생하는 문제를 해결했습니다.
129 | #### 2.0.4
130 | 신트윗덱이 종종 구트윗덱과 같이 뜨는 문제를 해결했습니다.
131 | #### 2.0.3
132 | manifest V2가 파이어폭스에서 작동안하는 문제를 해결했습니다.
133 | #### 2.0.2
134 | 문서를 클릭할 수 없는 현상을 수정하였습니다.
135 | #### 2.0.1
136 | 신트윗덱의 head와 body를 제거합니다.
137 | #### 2.0.0
138 | 이 버전부터 manifest V2를 사용하여 별도의 서버가 필요하지 않습니다.
139 | #### 1.0.2
140 | 효과 있을듯?
141 |
142 |
143 | ## ~~투명한 면책조항~~
144 | 옛날 정보는 만료되었습니다.
145 | 확장프로그램을 manifest v2로 다시 만들었고, 외부서버가 필요하지 않습니다.
146 |
--------------------------------------------------------------------------------
/files/decider.json:
--------------------------------------------------------------------------------
1 | {
2 | "decider": {
3 | "tweetdeck_subsequent_follows": true,
4 | "scheduler_write": true,
5 | "in_reply_to_indicator": true,
6 | "enable_cors_firefox": true,
7 | "create_moment": true,
8 | "simplified_edit_collection_flow": true,
9 | "suggest_refresh": true,
10 | "poll_streamed_feed_favorites": true,
11 | "disable_oauth_echo": true,
12 | "scheduler_read_visible": true,
13 | "upload_big_gifs": true,
14 | "cookie_force_migrate": true,
15 | "action_retweeted_retweet": true,
16 | "native_animated_gifs": true,
17 | "touchdeck_sidebar_v2": true,
18 | "account_settings_join_team_flow": true,
19 | "enable_rewrite_columns": true,
20 | "touchdeck_font_size_v2": true,
21 | "touchdeck_search_v2": true,
22 | "disable_typeahead_search_with_feather_v2": true,
23 | "dataminr_proxied_auth_flow": true,
24 | "disable_streaming": true,
25 | "abuse_emergency_filter_info": true,
26 | "poll_streamed_feed_home": true,
27 | "compose_quoted_tweet_as_attachment": true,
28 | "send_twitter_auth_type_header": true,
29 | "dataminr": true,
30 | "heartfave_animation": true,
31 | "touchdeck_column_options_v2": true,
32 | "tweets_emoji": true,
33 | "column_unread_bar": true,
34 | "with_video_upload": true,
35 | "continuous_pipeline_staging": true,
36 | "universal_search_timelines": true,
37 | "machine_translated_tweets": true,
38 | "hashflags": true,
39 | "scheduler_read_background": true,
40 | "cookie_streaming": true,
41 | "poll_streamed_feed_usertweets": true,
42 | "faster_notifications": true,
43 | "disable_scheduled_messages": true,
44 | "streamed_chirp_lookup_metrics": true,
45 | "tweet_up_to_four_images": true,
46 | "sample_failed_requests": true,
47 | "iq_tweets": true,
48 | "add_column_by_url_query_param": true,
49 | "use_twitter_api_sync": true,
50 | "track_search_engagement": true,
51 | "quote_tweet_read": true,
52 | "cookie_access_tweetdeck": true,
53 | "account_settings_redesign": true,
54 | "windows_migration_logged_in_2": true,
55 | "tweetstorms": true,
56 | "action_favorited_retweet": true,
57 | "tweetdeck_subsequent_likes": true,
58 | "touchdeck_tweet_controls_v3": true,
59 | "trends_tailored": true,
60 | "live_video_timelines": true,
61 | "slow_collection_refresh": true,
62 | "fetch_entire_blocklist": true,
63 | "report_flow_iframe": true,
64 | "tweet_hide_suffix": true,
65 | "windows_migration_warning_2": true,
66 | "version_poll_force_upgrade": true,
67 | "quote_tweet_write": true,
68 | "poll_cards_enabled": true,
69 | "version_poll": true,
70 | "migrate_chrome_app_session_to_web": true,
71 | "add_account": true,
72 | "disable_quote_tweet_unavailable_msg": true,
73 | "convert_new_oauth_account_to_contributor": true,
74 | "iq_rts": true,
75 | "migrate_mac_app_session_to_web_gt_3_9_482": true,
76 | "enable_cors_2": true,
77 | "windows_migration_logged_out_2": true,
78 | "simplified_replies": true,
79 | "scheduler_write_media": true,
80 | "multi_photo_media_grid": true,
81 | "touchdeck_modals_v2": true,
82 | "non_destructive_chirp_rerender": true,
83 | "touchdeck_dropdowns_v2": true,
84 | "umf_prompts": true,
85 | "cramming": true,
86 | "trends_regional": true,
87 | "cookie_td_cookie_migration": true,
88 | "universal_search_timelines_by_id": true,
89 | "add_account_via_xauth_2": true,
90 | "native_video": true,
91 | "chirp_lateness_metric": true,
92 | "upload_use_sru": true,
93 | "mute_conversation": true,
94 | "action_quoted_tweet": true,
95 | "dm_rounded_avatars": true,
96 | "compose_character_limit_do_not_count_attachments": true,
97 | "touchdeck_compose_v2": true,
98 | "autocomplete_remote_sources": true,
99 | "cards_enabled_detail_view": true
100 | }
101 | }
--------------------------------------------------------------------------------
/files/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | TweetDeck
8 |
9 |
11 |
12 |
13 |
14 |
16 |
18 |
20 |
22 |
23 |
24 |
25 |
26 |
30 |
31 |
32 |
33 |

35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/files/version.json:
--------------------------------------------------------------------------------
1 | {"version":"4.0.220811153004","minimum":"4.0.190610153508"}
--------------------------------------------------------------------------------
/images/logo128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimdenGD/OldTweetDeck/071ad328ffbe3cfc0df247211041ec0de6767d2d/images/logo128.png
--------------------------------------------------------------------------------
/images/logo144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimdenGD/OldTweetDeck/071ad328ffbe3cfc0df247211041ec0de6767d2d/images/logo144.png
--------------------------------------------------------------------------------
/images/logo16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimdenGD/OldTweetDeck/071ad328ffbe3cfc0df247211041ec0de6767d2d/images/logo16.png
--------------------------------------------------------------------------------
/images/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimdenGD/OldTweetDeck/071ad328ffbe3cfc0df247211041ec0de6767d2d/images/logo192.png
--------------------------------------------------------------------------------
/images/logo32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimdenGD/OldTweetDeck/071ad328ffbe3cfc0df247211041ec0de6767d2d/images/logo32.png
--------------------------------------------------------------------------------
/images/logo48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimdenGD/OldTweetDeck/071ad328ffbe3cfc0df247211041ec0de6767d2d/images/logo48.png
--------------------------------------------------------------------------------
/images/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimdenGD/OldTweetDeck/071ad328ffbe3cfc0df247211041ec0de6767d2d/images/logo512.png
--------------------------------------------------------------------------------
/images/logo96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimdenGD/OldTweetDeck/071ad328ffbe3cfc0df247211041ec0de6767d2d/images/logo96.png
--------------------------------------------------------------------------------
/images/tweetdeck.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimdenGD/OldTweetDeck/071ad328ffbe3cfc0df247211041ec0de6767d2d/images/tweetdeck.png
--------------------------------------------------------------------------------
/images/tweetdeck.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "OldTweetDeck",
3 | "description": "Returns old TweetDeck, for free!",
4 | "version": "4.0.7",
5 | "manifest_version": 3,
6 | "homepage_url": "https://github.com/dimdenGD/OldTweetDeck",
7 | "permissions": [
8 | "webNavigation",
9 | "cookies",
10 | "declarativeNetRequest"
11 | ],
12 | "host_permissions": [
13 | "https://twitter.com/*",
14 | "https://*.twitter.com/*",
15 | "https://x.com/*",
16 | "https://*.x.com/*",
17 | "https://abs.twimg.com/*",
18 | "https://api.twitter.com/*",
19 | "https://tweetdeck.com/"
20 | ],
21 | "web_accessible_resources": [
22 | {
23 | "resources": ["images/*", "files/*", "src/*", "solver.html", "manifest.json"],
24 | "matches": [""]
25 | }
26 | ],
27 | "declarative_net_request": {
28 | "rule_resources" : [{
29 | "id": "ruleset",
30 | "enabled": true,
31 | "path": "ruleset.json"
32 | }]
33 | },
34 | "icons": {
35 | "16": "/images/logo16.png",
36 | "32": "/images/logo32.png",
37 | "48": "/images/logo48.png",
38 | "128": "/images/logo128.png"
39 | },
40 | "content_scripts": [
41 | {
42 | "matches": ["https://twitter.com/i/tweetdeck", "https://x.com/i/tweetdeck", "https://x.com/i/tweetdeck?*"],
43 | "js": ["src/content.js"],
44 | "all_frames": true,
45 | "run_at": "document_start"
46 | },
47 | {
48 | "matches": ["https://twitter.com/i/tweetdeck", "https://x.com/i/tweetdeck", "https://x.com/i/tweetdeck?*"],
49 | "js": ["src/destroyer.js", "src/notifications.js", "src/injection.js"],
50 | "all_frames": true,
51 | "run_at": "document_start",
52 | "world": "MAIN"
53 | }
54 | ]
55 | }
56 |
--------------------------------------------------------------------------------
/pack.js:
--------------------------------------------------------------------------------
1 | // This script generates Firefox version of the extension and packs Chrome and Firefox versions to zip files.
2 |
3 | const fsp = require('fs').promises;
4 | const fs = require('fs');
5 | const path = require('path');
6 | const AdmZip = require('adm-zip');
7 |
8 | async function copyDir(src, dest) {
9 | const entries = await fsp.readdir(src, { withFileTypes: true });
10 | await fsp.mkdir(dest);
11 | for (let entry of entries) {
12 | if(entry.name === '.git' || entry.name === '.github' || entry.name === '_metadata' || entry.name === 'node_modules') continue;
13 | const srcPath = path.join(src, entry.name);
14 | const destPath = path.join(dest, entry.name);
15 | if (entry.isDirectory()) {
16 | await copyDir(srcPath, destPath);
17 | } else {
18 | await fsp.copyFile(srcPath, destPath);
19 | }
20 | }
21 | }
22 |
23 | if(fs.existsSync('../OldTweetDeckTempChrome')) {
24 | fs.rmSync('../OldTweetDeckTempChrome', { recursive: true });
25 | }
26 | if(fs.existsSync('../OldTweetDeckFirefox')) {
27 | fs.rmSync('../OldTweetDeckFirefox', { recursive: true });
28 | }
29 |
30 | console.log("Copying...");
31 | copyDir('./', '../OldTweetDeckFirefox').then(async () => {
32 | await copyDir('./', '../OldTweetDeckTempChrome');
33 | console.log("Copied!");
34 | console.log("Patching...");
35 |
36 | let manifest = JSON.parse(await fsp.readFile('../OldTweetDeckTempChrome/manifest.json', 'utf8'));
37 | manifest.browser_specific_settings = {
38 | gecko: {
39 | id: "oldtweetdeck@dimden.dev",
40 | strict_min_version: "90.0"
41 | }
42 | };
43 | manifest.manifest_version = 2;
44 | manifest.host_permissions.push("https://tweetdeck.dimden.dev/*", "https://raw.githubusercontent.com/*");
45 | delete manifest.declarative_net_request;
46 | manifest.permissions.push("webRequest", "webRequestBlocking", ...manifest.host_permissions);
47 | delete manifest.host_permissions;
48 | for(let content_script of manifest.content_scripts) {
49 | if(content_script.world === "MAIN") {
50 | delete content_script.world;
51 | }
52 | content_script.js = content_script.js.filter(js => js !== "src/destroyer.js");
53 | }
54 | manifest.background = {
55 | scripts: ["src/background.js"],
56 | }
57 | manifest.web_accessible_resources = manifest.web_accessible_resources[0].resources;
58 |
59 | fs.unlinkSync('../OldTweetDeckFirefox/pack.js');
60 | fs.unlinkSync('../OldTweetDeckTempChrome/pack.js');
61 | fs.unlinkSync('../OldTweetDeckFirefox/README.md');
62 | fs.unlinkSync('../OldTweetDeckTempChrome/README.md');
63 | fs.unlinkSync('../OldTweetDeckFirefox/package.json');
64 | fs.unlinkSync('../OldTweetDeckTempChrome/package.json');
65 | fs.unlinkSync('../OldTweetDeckFirefox/package-lock.json');
66 | fs.unlinkSync('../OldTweetDeckTempChrome/package-lock.json');
67 | fs.unlinkSync('../OldTweetDeckFirefox/.gitignore');
68 | fs.unlinkSync('../OldTweetDeckTempChrome/.gitignore');
69 | fs.writeFileSync('../OldTweetDeckFirefox/manifest.json', JSON.stringify(manifest, null, 2));
70 |
71 | console.log("Patched!");
72 |
73 | console.log("Zipping Firefox version...");
74 | try {
75 | const zip = new AdmZip();
76 | const outputDir = "../OldTweetDeckFirefox.zip";
77 | zip.addLocalFolder("../OldTweetDeckFirefox");
78 | zip.writeZip(outputDir);
79 | } catch (e) {
80 | console.log(`Something went wrong ${e}`);
81 | }
82 | console.log("Zipping Chrome version...");
83 | try {
84 | const zip = new AdmZip();
85 | const outputDir = "../OldTweetDeckChrome.zip";
86 | zip.addLocalFolder("../OldTweetDeckTempChrome");
87 | zip.writeZip(outputDir);
88 | } catch (e) {
89 | console.log(`Something went wrong ${e}`);
90 | }
91 | console.log("Zipped!");
92 | console.log("Deleting temporary folders...");
93 | fs.rmSync('../OldTweetDeckTempChrome', { recursive: true });
94 | fs.rmSync('../OldTweetDeckFirefox', { recursive: true });
95 | console.log("Deleted!");
96 | });
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "oldtweetdeck",
3 | "version": "0.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "oldtweetdeck",
9 | "version": "0.0.0",
10 | "license": "MIT",
11 | "dependencies": {
12 | "adm-zip": "^0.5.10"
13 | }
14 | },
15 | "node_modules/adm-zip": {
16 | "version": "0.5.10",
17 | "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz",
18 | "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==",
19 | "engines": {
20 | "node": ">=6.0"
21 | }
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "oldtweetdeck",
3 | "version": "0.0.0",
4 | "description": "Returns old TweetDeck, for free!",
5 | "main": "pack.js",
6 | "scripts": {
7 | "build": "node pack.js"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/dimdenGD/OldTweetDeck.git"
12 | },
13 | "keywords": [
14 | "twitter",
15 | "x",
16 | "tweetdeck",
17 | "extension",
18 | "addon"
19 | ],
20 | "author": "dimden",
21 | "license": "MIT",
22 | "bugs": {
23 | "url": "https://github.com/dimdenGD/OldTweetDeck/issues"
24 | },
25 | "homepage": "https://github.com/dimdenGD/OldTweetDeck#readme",
26 | "dependencies": {
27 | "adm-zip": "^0.5.10"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/ruleset.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "priority": 1,
5 | "action": {
6 | "type": "modifyHeaders",
7 | "responseHeaders": [
8 | {
9 | "header": "content-security-policy",
10 | "operation": "remove"
11 | },
12 | {
13 | "header": "x-frame-options",
14 | "operation": "remove"
15 | }
16 | ]
17 | },
18 | "condition": {
19 | "urlFilter": "https://twitter.com/i/tweetdeck",
20 | "resourceTypes": ["main_frame", "sub_frame", "stylesheet", "script", "image", "font", "xmlhttprequest", "other"]
21 | }
22 | },
23 | {
24 | "id": 2,
25 | "priority": 1,
26 | "action": {
27 | "type": "modifyHeaders",
28 | "responseHeaders": [
29 | {
30 | "header": "content-security-policy",
31 | "operation": "remove"
32 | },
33 | {
34 | "header": "x-frame-options",
35 | "operation": "remove"
36 | }
37 | ]
38 | },
39 | "condition": {
40 | "urlFilter": "https://x.com/i/tweetdeck",
41 | "resourceTypes": ["main_frame", "sub_frame", "stylesheet", "script", "image", "font", "xmlhttprequest", "other"]
42 | }
43 | },
44 | {
45 | "id": 3,
46 | "priority": 1,
47 | "action": {
48 | "type": "redirect",
49 | "redirect": { "url": "https://x.com/i/tweetdeck" }
50 | },
51 | "condition": {
52 | "urlFilter": "*://tweetdeck.com/*",
53 | "resourceTypes": ["main_frame", "sub_frame", "stylesheet", "script", "image", "font", "xmlhttprequest", "other"]
54 | }
55 | },
56 | {
57 | "id": 4,
58 | "priority": 1,
59 | "action": {
60 | "type": "redirect",
61 | "redirect": { "url": "https://x.com/i/tweetdeck" }
62 | },
63 | "condition": {
64 | "urlFilter": "*://tweetdeck.x.com/*",
65 | "resourceTypes": ["main_frame", "sub_frame", "stylesheet", "script", "image", "font", "xmlhttprequest", "other"]
66 | }
67 | },
68 | {
69 | "id": 5,
70 | "priority": 1,
71 | "action": {
72 | "type": "redirect",
73 | "redirect": { "url": "https://x.com/i/tweetdeck" }
74 | },
75 | "condition": {
76 | "urlFilter": "*://tweetdeck.twitter.com/*",
77 | "resourceTypes": ["main_frame", "sub_frame", "stylesheet", "script", "image", "font", "xmlhttprequest", "other"]
78 | }
79 | }
80 | ]
--------------------------------------------------------------------------------
/src/background.js:
--------------------------------------------------------------------------------
1 | const extraInfoSpec = ["blocking", "responseHeaders"];
2 | if (chrome.webRequest.OnHeadersReceivedOptions.hasOwnProperty("EXTRA_HEADERS")) {
3 | extraInfoSpec.push("extraHeaders");
4 | }
5 |
6 | chrome.webRequest.onHeadersReceived.addListener(
7 | function(details) {
8 | let headers = details.responseHeaders.filter(header => header.name.toLowerCase() !== 'content-security-policy' && header.name.toLowerCase() !== 'location');
9 | return {
10 | responseHeaders: headers
11 | }
12 | },
13 | {urls: ["https://twitter.com/i/tweetdeck", "https://x.com/i/tweetdeck", "https://x.com/i/tweetdeck?*"]},
14 | extraInfoSpec
15 | );
16 |
17 | chrome.webRequest.onBeforeSendHeaders.addListener(
18 | function(details) {
19 | let headers = details.requestHeaders.filter(header => header.name.toLowerCase() !== 'referer');
20 | return {
21 | requestHeaders: headers
22 | }
23 | },
24 | {urls: ["https://twitter.com/i/api/graphql/*", "https://x.com/i/api/graphql/*"]},
25 | extraInfoSpec.map(s => s.replace('response', 'request'))
26 | )
27 |
28 | chrome.webRequest.onBeforeRequest.addListener(
29 | function(details) {
30 | try {
31 | let parsedUrl = new URL(details.url);
32 | let path = parsedUrl.pathname;
33 | if(path === '/decider') {
34 | return {
35 | redirectUrl: chrome.runtime.getURL('/files/decider.json')
36 | }
37 | } else if(path === '/web/dist/version.json') {
38 | return {
39 | redirectUrl: chrome.runtime.getURL('/files/version.json')
40 | }
41 | };
42 | } catch(e) {}
43 | },
44 | {urls: ["https://*.twitter.com/*", "https://*.x.com/*"]},
45 | ["blocking"]
46 | );
47 |
48 | chrome.webRequest.onBeforeRequest.addListener(
49 | function() {
50 | return {
51 | redirectUrl: 'https://twitter.com/i/tweetdeck'
52 | }
53 | },
54 | {urls: ["https://tweetdeck.twitter.com/*", "https://tweetdeck.x.com/*"]},
55 | ["blocking"]
56 | );
57 |
58 | chrome.webRequest.onBeforeRequest.addListener(
59 | function() {
60 | return {
61 | redirectUrl: 'https://twitter.com/i/tweetdeck'
62 | }
63 | },
64 | {urls: ["https://tweetdeck.com/*"]},
65 | ["blocking"]
66 | );
67 |
68 | const isFirefox = typeof browser !== "undefined";
69 |
70 | // Store the URL of the tab that initiated the request.
71 | let urls = {};
72 |
73 | const flushCache = chrome.webRequest.handlerBehaviorChanged;
74 |
75 | chrome.webNavigation.onCommitted.addListener(
76 | function (details) {
77 | // Flushes in-memory cache when moving from other twitter.com sites to TweetDeck,
78 | // because if cache hits, `onBeforeRequest` event won't be called (and thus we can't block unwanted requests below).
79 | // Only needed in Chrome. See: https://developer.chrome.com/docs/extensions/reference/webRequest/#caching
80 | if (
81 | !isFirefox &&
82 | (urls[details.tabId]?.[details.frameId].startsWith("https://twitter.com/") || urls[details.tabId]?.[details.frameId].startsWith("https://x.com/")) &&
83 | details.transitionType !== "reload" &&
84 | (details.url === "https://twitter.com/i/tweetdeck" || details.url === "https://x.com/i/tweetdeck")
85 | ) {
86 | flushCache();
87 | // Update stored URL
88 | }
89 | if (details.tabId === -1 || details.frameId !== 0) {
90 | return;
91 | }
92 | if (!urls.hasOwnProperty(details.tabId)) {
93 | urls[details.tabId] = {};
94 | }
95 | urls[details.tabId][details.frameId] = details.url;
96 | },
97 | { url: [{ hostSuffix: "twitter.com" }, { hostSuffix: "x.com" }] },
98 | );
99 |
100 | // Block requests for files related to Web App, except for main.{random}.js (which may be needed for API connection)
101 | chrome.webRequest.onBeforeRequest.addListener(
102 | function (details) {
103 | try {
104 | let parsedUrl = new URL(details.url);
105 | let path = parsedUrl.pathname;
106 | // want to use details.originUrl but it's not available in Chrome
107 | let requestFrom = urls[details.tabId][details.frameId];
108 | if (
109 | (
110 | path.startsWith("/responsive-web/client-web-legacy/") ||
111 | path.startsWith("/responsive-web/client-web/")
112 | ) &&
113 | (requestFrom === "https://twitter.com/i/tweetdeck" || requestFrom === "https://x.com/i/tweetdeck") &&
114 | !path.includes('ondemand.s.')
115 | ) {
116 | return {
117 | cancel: true,
118 | };
119 | }
120 | } catch (e) {}
121 | },
122 | { urls: ["https://abs.twimg.com/*"] },
123 | ["blocking"],
124 | );
125 |
126 | chrome.runtime.onMessage.addListener(async (request, sender) => {
127 | if(request.action === 'setcookie') {
128 | chrome.cookies.getAll({url: "https://x.com"}, async cookies => {
129 | console.log('setcookie', cookies);
130 | chrome.tabs.query({ active: true, lastFocusedWindow: true }, tab => {
131 | tab = tab[0];
132 | chrome.cookies.getAllCookieStores(async cookieStores => {
133 | console.log('cookieStores', cookieStores, tab);
134 | const storeId = cookieStores?.find( cookieStore => cookieStore?.tabIds?.indexOf(tab?.id) !== -1)?.id;
135 |
136 | for(let cookie of cookies) {
137 | chrome.cookies.set({
138 | url: "https://twitter.com",
139 | name: cookie.name,
140 | value: cookie.value,
141 | expirationDate: cookie.expirationDate,
142 | domain: ".twitter.com",
143 | sameSite: cookie.sameSite,
144 | secure: cookie.secure,
145 | httpOnly: cookie.httpOnly,
146 | storeId
147 | }, () => {
148 | console.log('set cookie', cookie, storeId);
149 | });
150 | }
151 | });
152 | });
153 | });
154 | }
155 | });
--------------------------------------------------------------------------------
/src/challenge.js:
--------------------------------------------------------------------------------
1 | let solveId = 0;
2 | let solveCallbacks = {};
3 | let solverErrored = false;
4 |
5 | let solverIframe = document.createElement('iframe');
6 | solverIframe.style.position = 'absolute';
7 | solverIframe.width = '0px';
8 | solverIframe.height = '0px';
9 | solverIframe.style.border = 'none';
10 | solverIframe.style.opacity = 0;
11 | solverIframe.style.pointerEvents = 'none';
12 | solverIframe.tabIndex = -1;
13 | solverIframe.src = "https://tweetdeck.dimden.dev/solver.html?1"; // check source code of that page to make sure its safe if u dont trust it
14 | fetch(solverIframe.src).catch(() => {
15 | console.error("Cannot load solver iframe");
16 | solverErrored = true;
17 | for(let id in solveCallbacks) {
18 | solveCallbacks[id].reject('Solver errored');
19 | delete solveCallbacks[id];
20 | }
21 | });
22 | let injectedBody = document.getElementById('injected-body');
23 | if(injectedBody) injectedBody.appendChild(solverIframe);
24 | else {
25 | let int = setInterval(() => {
26 | injectedBody = document.getElementById('injected-body');
27 | if(injectedBody) {
28 | clearInterval(int);
29 | injectedBody.appendChild(solverIframe);
30 | }
31 | }, 50);
32 | }
33 |
34 | function uuidV4() {
35 | const uuid = new Array(36);
36 | for (let i = 0; i < 36; i++) {
37 | uuid[i] = Math.floor(Math.random() * 16);
38 | }
39 | uuid[14] = 4; // set bits 12-15 of time-high-and-version to 0100
40 | uuid[19] = uuid[19] &= ~(1 << 2); // set bit 6 of clock-seq-and-reserved to zero
41 | uuid[19] = uuid[19] |= (1 << 3); // set bit 7 of clock-seq-and-reserved to one
42 | uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
43 | return uuid.map((x) => x.toString(16)).join('');
44 | }
45 |
46 | async function readCryptoKey() {
47 | return new Promise((resolve, reject) => {
48 | let request = indexedDB.open("localforage");
49 |
50 | request.onerror = function(event) {
51 | reject(event);
52 | };
53 |
54 | request.onsuccess = function(event) {
55 | const db = event.target.result;
56 |
57 | // Open a transaction to access the keyvaluepairs object store
58 | if (db.objectStoreNames.contains('keyvaluepairs')) {
59 | const transaction = db.transaction(['keyvaluepairs'], 'readonly');
60 | const objectStore = transaction.objectStore('keyvaluepairs');
61 |
62 | objectStore.openCursor().onsuccess = function(event) {
63 | const cursor = event.target.result;
64 | if (cursor) {
65 | // Check if the key matches the pattern
66 | const key = cursor.key;
67 | if (key.startsWith('device:rweb.dmCryptoKeys')) {
68 | resolve(cursor.value);
69 | }
70 |
71 | // Move to the next entry
72 | cursor.continue();
73 | } else {
74 | // No more entries, reject the promise
75 | reject("No key found");
76 | }
77 | };
78 | } else {
79 | reject("No key found");
80 | }
81 | };
82 | });
83 | }
84 |
85 | function solveChallenge(path, method) {
86 | return new Promise((resolve, reject) => {
87 | if(solverErrored) {
88 | reject('Solver errored');
89 | return;
90 | }
91 | let id = solveId++;
92 | solveCallbacks[id] = { resolve, reject, time: Date.now() };
93 | if(!solverIframe.contentWindow) {
94 | solverIframe.addEventListener('load', () => {
95 | solverIframe.contentWindow.postMessage({ action: 'solve', id, path, method }, '*');
96 | setTimeout(() => {
97 | if(solveCallbacks[id]) {
98 | solveCallbacks[id].reject('Solver timed out');
99 | delete solveCallbacks[id];
100 | }
101 | }, 300);
102 | });
103 | } else {
104 | solverIframe.contentWindow.postMessage({ action: 'solve', id, path, method }, '*');
105 | setTimeout(() => {
106 | if(solveCallbacks[id]) {
107 | solveCallbacks[id].reject('Solver timed out');
108 | delete solveCallbacks[id];
109 | }
110 | }, 500);
111 | }
112 | });
113 | }
114 |
115 | window.addEventListener('message', e => {
116 | if(e.source !== solverIframe.contentWindow) return;
117 | let data = e.data;
118 | if(data.action === 'solved') {
119 | let { id, result } = data;
120 | if(solveCallbacks[id]) {
121 | solveCallbacks[id].resolve(result);
122 | delete solveCallbacks[id];
123 | }
124 | } else if(data.action === 'error') {
125 | let { id, error } = data;
126 | if(solveCallbacks[id]) {
127 | solveCallbacks[id].reject(error);
128 | delete solveCallbacks[id];
129 | }
130 | } else if(data.action === 'initError') {
131 | console.error('Solver init error:', data.error);
132 | solverErrored = true;
133 | for(let id in solveCallbacks) {
134 | solveCallbacks[id].reject('Solver errored');
135 | delete solveCallbacks[id];
136 | }
137 | }
138 | });
139 |
140 | (async () => {
141 | try {
142 | try {
143 | let cryptoKey = await readCryptoKey();
144 | if(cryptoKey) {
145 | localStorage.device_id = cryptoKey.deviceId;
146 | } else if(!localStorage.device_id) {
147 | localStorage.device_id = uuidV4();
148 | }
149 | } catch(e) {
150 | console.error(`Error during device id generation:`, e);
151 | if(!localStorage.device_id) {
152 | localStorage.device_id = uuidV4();
153 | }
154 | }
155 |
156 | let homepageData = await fetch(`https://${location.hostname}/`).then(res => res.text());
157 | let dom = new DOMParser().parseFromString(homepageData, 'text/html');
158 | let anims = Array.from(dom.querySelectorAll('svg[id^="loading-x"]')).map(svg => svg.outerHTML);
159 |
160 | let challengeCode = homepageData.match(/"ondemand.s":"(\w+)"/)[1];
161 | let challengeData = await fetch(`https://abs.twimg.com/responsive-web/client-web/ondemand.s.${challengeCode}a.js`).then(res => res.text());
162 |
163 | function sendInit() {
164 | solverIframe.contentWindow.postMessage({
165 | action: 'init',
166 | challenge: challengeData,
167 | anims,
168 | verificationCode: dom.querySelector('meta[name="twitter-site-verification"]').content,
169 | }, '*');
170 | }
171 | if(solverIframe.contentWindow) {
172 | sendInit();
173 | } else {
174 | solverIframe.addEventListener('load', () => sendInit());
175 | }
176 | } catch (e) {
177 | console.error(`Error during challenge:`);
178 | console.error(e);
179 | }
180 | })()
--------------------------------------------------------------------------------
/src/content.js:
--------------------------------------------------------------------------------
1 | window.addEventListener('message', e => {
2 | if(e.data === 'extensionId') {
3 | let extId = chrome.runtime.getURL('/injection.js').split("/")[2];
4 | window.postMessage({ extensionId: extId }, '*');
5 | }
6 | });
--------------------------------------------------------------------------------
/src/destroyer.js:
--------------------------------------------------------------------------------
1 | // Step 1: fool twitter into thinking scripts loaded
2 | window.__SCRIPTS_LOADED__ = Object.freeze({
3 | main: true,
4 | vendor: true,
5 | runtime: false
6 | });
7 |
8 | // Step 2: continously wreck havoc
9 | let _destroyerInt = setInterval(() => {
10 | delete window.webpackChunk_twitter_responsive_web;
11 | window.__SCRIPTS_LOADED__ = Object.freeze({
12 | main: true,
13 | vendor: true,
14 | runtime: false
15 | });
16 | if(document.getElementById('ScriptLoadFailure')) {
17 | document.getElementById('ScriptLoadFailure').remove();
18 | }
19 | });
20 |
21 | // Step 3: destroy twitter critical modules
22 | let _originalPush = Array.prototype.push;
23 | Array.prototype.push = function() {
24 | try {
25 | if(arguments[0]?.[0]?.[0] === "vendor" || arguments[0]?.[0]?.[0] === "main") {
26 | throw "Twitter killing magic killed Twitter https://lune.dimden.dev/f016efffcd3d.png (thats fine)";
27 | }
28 | } catch(e) {
29 | Array.prototype.push = _originalPush;
30 | } finally {
31 | return _originalPush.apply(this, arguments);
32 | }
33 | }
34 |
35 | // Step 4: prevent twitter from reporting it
36 | let _originalTest = RegExp.prototype.test;
37 | RegExp.prototype.test = function() {
38 | try {
39 | if(this.toString() === '/[?&]failedScript=/') {
40 | RegExp.prototype.test = _originalTest;
41 | throw "hehe";
42 | };
43 | } catch(e) {
44 | RegExp.prototype.test = _originalTest;
45 | } finally {
46 | return _originalTest.apply(this, arguments);
47 | }
48 | }
49 |
50 | // Step 5: self destruct
51 | setTimeout(() => {
52 | clearInterval(_destroyerInt);
53 | Array.prototype.push = _originalPush;
54 | RegExp.prototype.test = _originalTest;
55 | }, 5000);
56 |
57 | // Step 6: Live OTD reaction: https://lune.dimden.dev/6743b45eb1de.png
--------------------------------------------------------------------------------
/src/injection.js:
--------------------------------------------------------------------------------
1 | let extId;
2 | let isFirefox = navigator.userAgent.indexOf('Firefox') > -1;
3 | if(!window.chrome) window.chrome = {};
4 | if(!window.chrome.runtime) window.chrome.runtime = {};
5 | window.chrome.runtime.getURL = url => {
6 | if(!url.startsWith('/')) url = `/${url}`;
7 | return `${isFirefox ? 'moz-extension://' : 'chrome-extension://'}${extId}${url}`;
8 | }
9 | window.addEventListener('message', e => {
10 | if(e.data.extensionId) {
11 | console.log("got extensionId", e.data.extensionId);
12 | extId = e.data.extensionId;
13 | main();
14 | }
15 | });
16 | window.postMessage('extensionId', '*');
17 |
18 | async function main() {
19 | let html = await fetch(chrome.runtime.getURL('/files/index.html')).then(r => r.text());
20 | document.documentElement.innerHTML = html;
21 |
22 | let [challenge_js, interception_js, vendor_js, bundle_js, bundle_css, twitter_text] =
23 | await Promise.allSettled([
24 | fetch(chrome.runtime.getURL("/src/challenge.js")).then(r => r.text()),
25 | fetch(chrome.runtime.getURL("/src/interception.js")).then(r => r.text()),
26 | fetch(chrome.runtime.getURL("/files/vendor.js")).then(r => r.text()),
27 | fetch(chrome.runtime.getURL("/files/bundle.js")).then(r => r.text()),
28 | fetch(chrome.runtime.getURL("/files/bundle.css")).then(r => r.text()),
29 | fetch(chrome.runtime.getURL("/files/twitter-text.js")).then(r => r.text()),
30 | ]);
31 | if (!localStorage.getItem("OTDalwaysUseLocalFiles")) {
32 | const [
33 | remote_challenge_js_req,
34 | remote_interception_js_req,
35 | remote_vendor_js_req,
36 | remote_bundle_js_req,
37 | remote_bundle_css_req,
38 | remote_twitter_text_req,
39 | ] = await Promise.allSettled([
40 | fetch("https://raw.githubusercontent.com/dimdenGD/OldTweetDeck/main/src/challenge.js"),
41 | fetch("https://raw.githubusercontent.com/dimdenGD/OldTweetDeck/main/src/interception.js"),
42 | fetch("https://raw.githubusercontent.com/dimdenGD/OldTweetDeck/main/files/vendor.js"),
43 | fetch("https://raw.githubusercontent.com/dimdenGD/OldTweetDeck/main/files/bundle.js"),
44 | fetch("https://raw.githubusercontent.com/dimdenGD/OldTweetDeck/main/files/bundle.css"),
45 | fetch("https://raw.githubusercontent.com/dimdenGD/OldTweetDeck/main/files/twitter-text.js"),
46 | ]);
47 |
48 | if(
49 | (remote_challenge_js_req.value && remote_challenge_js_req.value.ok) ||
50 | (remote_interception_js_req.value && remote_interception_js_req.value.ok) ||
51 | (remote_vendor_js_req.value && remote_vendor_js_req.value.ok) ||
52 | (remote_bundle_js_req.value && remote_bundle_js_req.value.ok) ||
53 | (remote_bundle_css_req.value && remote_bundle_css_req.value.ok) ||
54 | (remote_twitter_text_req.value && remote_twitter_text_req.value.ok)
55 | ) {
56 | const [
57 | remote_challenge_js,
58 | remote_interception_js,
59 | remote_vendor_js,
60 | remote_bundle_js,
61 | remote_bundle_css,
62 | remote_twitter_text,
63 | ] = await Promise.allSettled([
64 | remote_challenge_js_req.value.text(),
65 | remote_interception_js_req.value.text(),
66 | remote_vendor_js_req.value.text(),
67 | remote_bundle_js_req.value.text(),
68 | remote_bundle_css_req.value.text(),
69 | remote_twitter_text_req.value.text(),
70 | ]);
71 |
72 | if (
73 | remote_challenge_js_req.value &&
74 | remote_challenge_js_req.value.ok &&
75 | remote_challenge_js.status === "fulfilled" &&
76 | remote_challenge_js.value.length > 30
77 | ) {
78 | challenge_js = remote_challenge_js;
79 | console.log("Using remote challenge.js");
80 | }
81 |
82 | if (
83 | remote_interception_js_req.value &&
84 | remote_interception_js_req.value.ok &&
85 | remote_interception_js.status === "fulfilled" &&
86 | remote_interception_js.value.length > 30
87 | ) {
88 | interception_js = remote_interception_js;
89 | console.log("Using remote interception.js");
90 | }
91 | if (
92 | remote_vendor_js_req.value &&
93 | remote_vendor_js_req.value.ok &&
94 | remote_vendor_js.status === "fulfilled" &&
95 | remote_vendor_js.value.length > 30
96 | ) {
97 | vendor_js = remote_vendor_js;
98 | console.log("Using remote vendor.js");
99 | }
100 | if (
101 | remote_bundle_js_req.value &&
102 | remote_bundle_js_req.value.ok &&
103 | remote_bundle_js.status === "fulfilled" &&
104 | remote_bundle_js.value.length > 30
105 | ) {
106 | bundle_js = remote_bundle_js;
107 | console.log("Using remote bundle.js");
108 | }
109 | if (
110 | remote_bundle_css_req.value &&
111 | remote_bundle_css_req.value.ok &&
112 | remote_bundle_css.status === "fulfilled" &&
113 | remote_bundle_css.value.length > 30
114 | ) {
115 | bundle_css = remote_bundle_css;
116 | console.log("Using remote bundle.css");
117 | }
118 | if (
119 | remote_twitter_text_req.value &&
120 | remote_twitter_text_req.value.ok &&
121 | remote_twitter_text.status === "fulfilled" &&
122 | remote_twitter_text.value.length > 30
123 | ) {
124 | twitter_text = remote_twitter_text;
125 | console.log("Using remote twitter-text.js");
126 | }
127 | }
128 | }
129 |
130 | let challenge_js_script = document.createElement("script");
131 | challenge_js_script.innerHTML = challenge_js.value.replaceAll('SOLVER_URL', chrome.runtime.getURL("solver.html"));
132 | document.head.appendChild(challenge_js_script);
133 |
134 | let interception_js_script = document.createElement("script");
135 | interception_js_script.innerHTML = interception_js.value;
136 | document.head.appendChild(interception_js_script);
137 |
138 | let bundle_css_style = document.createElement("style");
139 | bundle_css_style.innerHTML = bundle_css.value;
140 | document.head.appendChild(bundle_css_style);
141 |
142 | let vendor_js_script = document.createElement("script");
143 | vendor_js_script.innerHTML = vendor_js.value;
144 | document.head.appendChild(vendor_js_script);
145 |
146 | let bundle_js_script = document.createElement("script");
147 | bundle_js_script.innerHTML = bundle_js.value;
148 | document.head.appendChild(bundle_js_script);
149 |
150 | let twitter_text_script = document.createElement("script");
151 | twitter_text_script.innerHTML = twitter_text.value;
152 | document.head.appendChild(twitter_text_script);
153 |
154 | let int = setTimeout(function() {
155 | let badBody = document.querySelector('body:not(#injected-body)');
156 | if (badBody) {
157 | let badHead = document.querySelector('head:not(#injected-head)');
158 | clearInterval(int);
159 | if(badHead) badHead.remove();
160 | badBody.remove();
161 | }
162 | }, 200);
163 | setTimeout(() => clearInterval(int), 10000);
164 |
165 | let injInt;
166 | function injectAccount() {
167 | if(!document.querySelector('a[data-title="Accounts"]')) return;
168 | clearInterval(injInt);
169 |
170 | let accountsBtn = document.querySelector('a[data-title="Accounts"]');
171 | accountsBtn.addEventListener("click", function() {
172 | console.log("setting account cookie");
173 | chrome.runtime.sendMessage({ action: "setcookie" });
174 | });
175 | }
176 | setInterval(injectAccount, 1000);
177 | };
--------------------------------------------------------------------------------
/src/interception.js:
--------------------------------------------------------------------------------
1 | const PUBLIC_TOKENS = [
2 | "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA",
3 | "Bearer AAAAAAAAAAAAAAAAAAAAAFQODgEAAAAAVHTp76lzh3rFzcHbmHVvQxYYpTw%3DckAlMINMjmCwxUcaXbAN4XqJVdgMJaHqNOFgPMK0zN1qLqLQCF",
4 | ];
5 | const NEW_API = `https://${location.hostname}/i/api/graphql`;
6 | const cursors = {};
7 | const OTD_INIT_TIME = Date.now();
8 |
9 | const generateID = () => {
10 | return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
11 | }
12 |
13 | let verifiedUser = localStorage.OTDverifiedUser ? JSON.parse(localStorage.OTDverifiedUser) : null;
14 | let feeds = localStorage.OTDfeeds ? JSON.parse(localStorage.OTDfeeds) : {};
15 | let columns = localStorage.OTDcolumns ? JSON.parse(localStorage.OTDcolumns) : {};
16 | let settings = localStorage.OTDsettings ? JSON.parse(localStorage.OTDsettings) : null;
17 |
18 | function exportState() {
19 | const a = document.createElement('a');
20 | a.href = URL.createObjectURL(new Blob([JSON.stringify({
21 | feeds,
22 | columns,
23 | settings,
24 | columnIds: localStorage.OTDcolumnIds ? JSON.parse(localStorage.OTDcolumnIds) : []
25 | })], {type: 'application/json'}));
26 | a.download = 'OTDState.json';
27 | a.click();
28 | }
29 |
30 | function importState() {
31 | const input = document.createElement('input');
32 | input.type = 'file';
33 | input.accept = '.json';
34 | input.onchange = async () => {
35 | const file = input.files[0];
36 | if(!file) return;
37 | const reader = new FileReader();
38 | reader.onload = async (e) => {
39 | const text = e.target.result;
40 | try {
41 | const data = JSON.parse(text);
42 | if(!data.feeds || !data.columns || !data.settings || !data.columnIds) {
43 | throw new Error("Invalid file");
44 | }
45 | localStorage.OTDfeeds = JSON.stringify(data.feeds);
46 | localStorage.OTDcolumns = JSON.stringify(data.columns);
47 | localStorage.OTDsettings = JSON.stringify(data.settings);
48 | localStorage.OTDcolumnIds = JSON.stringify(data.columnIds);
49 | location.reload();
50 | } catch(e) {
51 | alert("Error parsing file");
52 | }
53 | };
54 | reader.readAsText(file);
55 | };
56 | input.click();
57 | }
58 |
59 | function cleanUp() {
60 | let ids = localStorage.OTDcolumnIds ? JSON.parse(localStorage.OTDcolumnIds) : [];
61 | for(let columnId in columns) {
62 | if(!ids.includes(columnId)) {
63 | delete columns[columnId];
64 | }
65 | }
66 | localStorage.OTDcolumns = JSON.stringify(columns);
67 | for(let id in feeds) {
68 | if(!localStorage.OTDcolumns.includes(id)) {
69 | delete feeds[id];
70 | }
71 | }
72 | localStorage.OTDfeeds = JSON.stringify(feeds);
73 | }
74 |
75 | function getFollows(id = getCurrentUserId(), cursor = -1, count = 5000) {
76 | return new Promise(function (resolve, reject) {
77 | var xhr = new XMLHttpRequest();
78 | xhr.open("GET", `https://api.${location.hostname}/1.1/friends/ids.json?user_id=${id}&cursor=${cursor}&stringify_ids=true&count=${count}`, true);
79 | xhr.setRequestHeader("X-Twitter-Active-User", "yes");
80 | xhr.setRequestHeader("X-Twitter-Auth-Type", "OAuth2Session");
81 | xhr.setRequestHeader("X-Twitter-Client-Language", "en");
82 | xhr.setRequestHeader("Authorization", "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA");
83 | xhr.setRequestHeader("X-Csrf-Token", (function () {
84 | var csrf = document.cookie.match(/(?:^|;\s*)ct0=([0-9a-f]+)\s*(?:;|$)/);
85 | return csrf ? csrf[1] : "";
86 | })());
87 | xhr.withCredentials = true;
88 |
89 | xhr.onreadystatechange = function () {
90 | if (xhr.readyState === 4 && xhr.status === 200) {
91 | resolve(JSON.parse(xhr.responseText));
92 | } else if (xhr.readyState === 4 && xhr.status !== 200) {
93 | reject(xhr);
94 | }
95 | };
96 |
97 | xhr.send();
98 | });
99 | }
100 |
101 | let followsData = JSON.parse(localStorage.OTDfollowsData || "{}");
102 |
103 | let updatingFollows = false;
104 | function updateFollows(id = getCurrentUserId()) {
105 | if(followsData[id] && followsData[id].lastUpdate && Date.now() - +followsData[id].lastUpdate < 1000 * 60 * 60 * 6) return;
106 | if(updatingFollows) return;
107 | updatingFollows = true;
108 |
109 | if(!followsData[id]) followsData[id] = {};
110 | let newfollows = [];
111 | let cursor = -1;
112 | let count = 5000;
113 | let i = 0;
114 | let get = async () => {
115 | let res = await getFollows(id, cursor, count);
116 | newfollows = newfollows.concat(res.ids);
117 | if(res.next_cursor_str === "0" || i++ > 10) {
118 | followsData[id].lastUpdate = Date.now();
119 | followsData[id].data = newfollows;
120 | localStorage.OTDfollowsData = JSON.stringify(followsData);
121 | updatingFollows = false;
122 | return;
123 | }
124 | cursor = res.next_cursor_str;
125 | get();
126 | };
127 |
128 | get();
129 | }
130 |
131 | setTimeout(updateFollows, 1000);
132 | setInterval(updateFollows, 1000 * 60);
133 |
134 | function parseNoteTweet(result) {
135 | let text, entities;
136 | if (result.note_tweet.note_tweet_results.result) {
137 | text = result.note_tweet.note_tweet_results.result.text;
138 | entities = result.note_tweet.note_tweet_results.result.entity_set;
139 | if (result.note_tweet.note_tweet_results.result.richtext?.richtext_tags.length) {
140 | entities.richtext = result.note_tweet.note_tweet_results.result.richtext.richtext_tags; // logically, richtext is an entity, right?
141 | }
142 | } else {
143 | text = result.note_tweet.note_tweet_results.text;
144 | entities = result.note_tweet.note_tweet_results.entity_set;
145 | }
146 | return { text, entities };
147 | }
148 |
149 | function parseTweet(res) {
150 | if (typeof res !== "object") return;
151 | if (res.limitedActionResults) {
152 | let limitation = res.limitedActionResults.limited_actions.find((l) => l.action === "Reply");
153 | if (limitation) {
154 | res.tweet.legacy.limited_actions_text = limitation.prompt
155 | ? limitation.prompt.subtext.text
156 | : "This tweet has limitations to who can reply.";
157 | }
158 | res = res.tweet;
159 | }
160 | if (!res.legacy && res.tweet) res = res.tweet;
161 | let tweet = res.legacy;
162 | if (!res.core) return;
163 | tweet.user = res.core.user_results.result.legacy;
164 | tweet.user.id_str = tweet.user_id_str;
165 | if (res.core.user_results.result.is_blue_verified) {
166 | tweet.user.verified = true;
167 | tweet.user.verified_type = "Blue";
168 | }
169 | if (tweet.retweeted_status_result) {
170 | let result = tweet.retweeted_status_result.result;
171 | if (result.limitedActionResults) {
172 | let limitation = result.limitedActionResults.limited_actions.find(
173 | (l) => l.action === "Reply"
174 | );
175 | if (limitation) {
176 | result.tweet.legacy.limited_actions_text = limitation.prompt
177 | ? limitation.prompt.subtext.text
178 | : "This tweet has limitations to who can reply.";
179 | }
180 | result = result.tweet;
181 | }
182 | if (
183 | result.quoted_status_result &&
184 | result.quoted_status_result.result &&
185 | result.quoted_status_result.result.legacy &&
186 | result.quoted_status_result.result.core &&
187 | result.quoted_status_result.result.core.user_results.result.legacy
188 | ) {
189 | result.legacy.quoted_status = result.quoted_status_result.result.legacy;
190 | if (result.legacy.quoted_status) {
191 | result.legacy.quoted_status.user =
192 | result.quoted_status_result.result.core.user_results.result.legacy;
193 | result.legacy.quoted_status.user.id_str = result.legacy.quoted_status.user_id_str;
194 | if (result.quoted_status_result.result.core.user_results.result.is_blue_verified) {
195 | result.legacy.quoted_status.user.verified = true;
196 | result.legacy.quoted_status.user.verified_type = "Blue";
197 | }
198 | } else {
199 | console.warn("No retweeted quoted status", result);
200 | }
201 | }
202 | tweet.retweeted_status = result.legacy;
203 | if (tweet.retweeted_status && result.core.user_results.result.legacy) {
204 | tweet.retweeted_status.user = result.core.user_results.result.legacy;
205 | tweet.retweeted_status.user.id_str = tweet.retweeted_status.user_id_str;
206 | if (result.core.user_results.result.is_blue_verified) {
207 | tweet.retweeted_status.user.verified = true;
208 | tweet.retweeted_status.user.verified_type = "Blue";
209 | }
210 | tweet.retweeted_status.ext = {};
211 | if (result.views) {
212 | tweet.retweeted_status.ext.views = { r: { ok: { count: +result.views.count } } };
213 | }
214 | if (res.card && res.card.legacy && res.card.legacy.binding_values) {
215 | tweet.retweeted_status.card = res.card.legacy;
216 | }
217 | } else {
218 | console.warn("No retweeted status", result);
219 | }
220 | if (result.note_tweet && result.note_tweet.note_tweet_results) {
221 | let note = parseNoteTweet(result);
222 | tweet.retweeted_status.full_text = note.text;
223 | tweet.retweeted_status.entities = note.entities;
224 | tweet.retweeted_status.display_text_range = undefined; // no text range for long tweets
225 | }
226 | }
227 |
228 | if (res.quoted_status_result) {
229 | tweet.quoted_status_result = res.quoted_status_result;
230 | }
231 | if (res.note_tweet && res.note_tweet.note_tweet_results) {
232 | let note = parseNoteTweet(res);
233 | tweet.full_text = note.text;
234 | tweet.entities = note.entities;
235 | tweet.display_text_range = undefined; // no text range for long tweets
236 | }
237 | if (tweet.quoted_status_result && tweet.quoted_status_result.result) {
238 | let result = tweet.quoted_status_result.result;
239 | if (!result.core && result.tweet) result = result.tweet;
240 | if (result.limitedActionResults) {
241 | let limitation = result.limitedActionResults.limited_actions.find(
242 | (l) => l.action === "Reply"
243 | );
244 | if (limitation) {
245 | result.tweet.legacy.limited_actions_text = limitation.prompt
246 | ? limitation.prompt.subtext.text
247 | : "This tweet has limitations to who can reply.";
248 | }
249 | result = result.tweet;
250 | }
251 | tweet.quoted_status = result.legacy;
252 | if (tweet.quoted_status) {
253 | tweet.quoted_status.user = result.core.user_results.result.legacy;
254 | if (!tweet.quoted_status.user) {
255 | delete tweet.quoted_status;
256 | } else {
257 | tweet.quoted_status.user.id_str = tweet.quoted_status.user_id_str;
258 | if (result.core.user_results.result.is_blue_verified) {
259 | tweet.quoted_status.user.verified = true;
260 | tweet.quoted_status.user.verified_type = "Blue";
261 | }
262 | tweet.quoted_status.ext = {};
263 | if (result.views) {
264 | tweet.quoted_status.ext.views = { r: { ok: { count: +result.views.count } } };
265 | }
266 | }
267 | } else {
268 | console.warn("No quoted status", result);
269 | }
270 | }
271 | if (res.card && res.card.legacy) {
272 | tweet.card = res.card.legacy;
273 | let bvo = {};
274 | for (let i = 0; i < tweet.card.binding_values.length; i++) {
275 | let bv = tweet.card.binding_values[i];
276 | bvo[bv.key] = bv.value;
277 | }
278 | tweet.card.binding_values = bvo;
279 | }
280 | if (res.views) {
281 | if (!tweet.ext) tweet.ext = {};
282 | tweet.ext.views = { r: { ok: { count: +res.views.count } } };
283 | }
284 | if (res.source) {
285 | tweet.source = res.source;
286 | }
287 | if (res.birdwatch_pivot) {
288 | // community notes
289 | tweet.birdwatch = res.birdwatch_pivot;
290 | }
291 |
292 | if (tweet.favorited && tweet.favorite_count === 0) {
293 | tweet.favorite_count = 1;
294 | }
295 | if (tweet.retweeted && tweet.retweet_count === 0) {
296 | tweet.retweet_count = 1;
297 | }
298 |
299 | return tweet;
300 | }
301 |
302 | function getCurrentUserId() {
303 | let accounts = TD.storage.accountController.getAll();
304 | let screen_name = TD.storage.accountController.getUserIdentifier();
305 | let account = accounts.find((account) => account.state.username === screen_name);
306 | return account?.state?.userId ?? verifiedUser?.id_str ?? localStorage.twitterAccountID;
307 | }
308 |
309 | function generateParams(features, variables, fieldToggles) {
310 | let params = new URLSearchParams();
311 | params.append("variables", JSON.stringify(variables));
312 | params.append("features", JSON.stringify(features));
313 | if (fieldToggles) params.append("fieldToggles", JSON.stringify(fieldToggles));
314 |
315 | return params.toString();
316 | }
317 |
318 | let counter = 0;
319 | const OriginalXHR = XMLHttpRequest;
320 | const proxyRoutes = [
321 | // Home timeline
322 | {
323 | path: "/1.1/statuses/home_timeline.json",
324 | method: "GET",
325 | // beforeRequest: (xhr) => {
326 | // try {
327 | // let url = new URL(xhr.modUrl);
328 | // let params = new URLSearchParams(url.search);
329 | // let variables = {
330 | // includePromotedContent: false,
331 | // latestControlAvailable: true,
332 | // count: 40,
333 | // requestContext: "launch",
334 | // };
335 | // let features = {
336 | // responsive_web_graphql_exclude_directive_enabled: true,
337 | // verified_phone_label_enabled: false,
338 | // responsive_web_home_pinned_timelines_enabled: true,
339 | // creator_subscriptions_tweet_preview_api_enabled: true,
340 | // responsive_web_graphql_timeline_navigation_enabled: true,
341 | // responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
342 | // c9s_tweet_anatomy_moderator_badge_enabled: true,
343 | // tweetypie_unmention_optimization_enabled: true,
344 | // responsive_web_edit_tweet_api_enabled: true,
345 | // graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
346 | // view_counts_everywhere_api_enabled: true,
347 | // longform_notetweets_consumption_enabled: true,
348 | // responsive_web_twitter_article_tweet_consumption_enabled: false,
349 | // tweet_awards_web_tipping_enabled: false,
350 | // freedom_of_speech_not_reach_fetch_enabled: true,
351 | // standardized_nudges_misinfo: true,
352 | // tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
353 | // longform_notetweets_rich_text_read_enabled: true,
354 | // longform_notetweets_inline_media_enabled: true,
355 | // responsive_web_media_download_video_enabled: false,
356 | // responsive_web_enhance_cards_enabled: false,
357 | // };
358 |
359 | // let max_id = params.get("max_id");
360 | // if (max_id) {
361 | // let bn = BigInt(params.get("max_id"));
362 | // bn += BigInt(1);
363 | // if (cursors[`home-${bn}`]) {
364 | // variables.cursor = cursors[`home-${bn}`];
365 | // }
366 | // }
367 | // xhr.modUrl = `${NEW_API}/Qe2CCi4SE0Dvsb1TYrDfKQ/HomeLatestTimeline?${generateParams(
368 | // features,
369 | // variables
370 | // )}`;
371 | // } catch (e) {
372 | // console.error(e);
373 | // }
374 | // },
375 | beforeSendHeaders: (xhr) => {
376 | xhr.storage.user_id = xhr.modReqHeaders["x-act-as-user-id"] ?? getCurrentUserId();
377 | xhr.modReqHeaders["Content-Type"] = "application/json";
378 | xhr.modReqHeaders["X-Twitter-Active-User"] = "yes";
379 | xhr.modReqHeaders["X-Twitter-Client-Language"] = "en";
380 | xhr.modReqHeaders["Authorization"] = PUBLIC_TOKENS[1];
381 | delete xhr.modReqHeaders["X-Twitter-Client-Version"];
382 | updateFollows(xhr.storage.user_id);
383 | },
384 | afterRequest: (xhr) => {
385 | let data;
386 | try {
387 | data = JSON.parse(xhr.responseText);
388 | } catch (e) {
389 | console.error(e);
390 | return [];
391 | }
392 | if (data.errors && data.errors[0]) {
393 | return [];
394 | }
395 |
396 | if(localStorage.OTDshowAllRepliesInHome === '1') {
397 | return data;
398 | }
399 |
400 | let userId = xhr.storage.user_id;
401 | let follows = followsData[userId];
402 | if(follows && follows.data) follows = follows.data;
403 | else follows = [];
404 |
405 | let filtered = data.filter(t =>
406 | !t.in_reply_to_user_id_str || // not a reply
407 | t.user.id_str === userId || // my tweet
408 | (
409 | // reply to someone i follow from someone i follow
410 | follows.includes(t.in_reply_to_user_id_str) &&
411 | t.user.following && t.entities.user_mentions.every(user => follows.includes(user.id_str))
412 | ) ||
413 | (
414 | // reply to me from someone i follow
415 | t.in_reply_to_user_id_str === userId &&
416 | t.user.following
417 | )
418 | );
419 |
420 | return filtered;
421 | }
422 | // responseHeaderOverride: {
423 | // // slow it down a bit
424 | // "x-rate-limit-limit": (value) => {
425 | // if (value == "500") {
426 | // return "100";
427 | // }
428 | // return value;
429 | // },
430 | // "x-rate-limit-remaining": (value, headers) => {
431 | // if (headers["x-rate-limit-limit"] == "500" && value > 250) {
432 | // return (+value - 400).toString();
433 | // } else {
434 | // return value;
435 | // }
436 | // },
437 | // },
438 | // afterRequest: (xhr) => {
439 | // let data;
440 | // try {
441 | // data = JSON.parse(xhr.responseText);
442 | // } catch (e) {
443 | // console.error(e);
444 | // return [];
445 | // }
446 | // if (data.errors && data.errors[0]) {
447 | // return [];
448 | // }
449 | // let instructions = data.data.home.home_timeline_urt.instructions;
450 | // let entries = instructions.find((i) => i.type === "TimelineAddEntries");
451 | // if (!entries) {
452 | // return [];
453 | // }
454 | // entries = entries.entries;
455 | // let tweets = [];
456 | // for (let e of entries) {
457 | // // thats a lot of trash https://lune.dimden.dev/0bf524e52eb.png
458 | // if (e.entryId.startsWith("tweet-")) {
459 | // let res = e.content.itemContent.tweet_results.result;
460 | // let tweet = parseTweet(res);
461 | // if (!tweet) continue;
462 | // if (
463 | // tweet.source &&
464 | // (tweet.source.includes("Twitter for Advertisers") ||
465 | // tweet.source.includes("advertiser-interface"))
466 | // )
467 | // continue;
468 | // if (tweet.user.blocking || tweet.user.muting) continue;
469 |
470 | // tweets.push(tweet);
471 | // } else if (e.entryId.startsWith("home-conversation-")) {
472 | // let items = e.content.items;
473 |
474 | // let pushedTweets = [];
475 | // for (let i = 0; i < items.length; i++) {
476 | // let item = items[i];
477 | // if (
478 | // item.entryId.includes("-tweet-") &&
479 | // !item.entryId.includes("promoted")
480 | // ) {
481 | // let res = item.item.itemContent.tweet_results.result;
482 | // let tweet = parseTweet(res);
483 | // if (!tweet) continue;
484 | // if (
485 | // tweet.source &&
486 | // (tweet.source.includes("Twitter for Advertisers") ||
487 | // tweet.source.includes("advertiser-interface"))
488 | // )
489 | // continue;
490 | // if (tweet.user.blocking || tweet.user.muting) break;
491 | // if (item.item.feedbackInfo) {
492 | // tweet.feedback = item.item.feedbackInfo.feedbackKeys
493 | // .map(
494 | // (f) =>
495 | // data.data.home.home_timeline_urt.responseObjects.feedbackActions.find(
496 | // (a) => a.key === f
497 | // ).value
498 | // )
499 | // .filter((f) => f);
500 | // if (tweet.feedback) {
501 | // tweet.feedbackMetadata =
502 | // item.item.feedbackInfo.feedbackMetadata;
503 | // }
504 | // }
505 | // tweets.push(tweet);
506 | // pushedTweets.push(tweet);
507 | // }
508 | // }
509 | // }
510 | // }
511 |
512 | // if (tweets.length === 0) return tweets;
513 |
514 | // // i didn't know they return tweets unsorted???
515 | // tweets.sort(
516 | // (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
517 | // );
518 |
519 | // let cursor = entries.find(
520 | // (e) =>
521 | // e.entryId.startsWith("sq-cursor-bottom-") ||
522 | // e.entryId.startsWith("cursor-bottom-")
523 | // );
524 | // if (cursor) {
525 | // cursors[`${xhr.storage.user_id}-${tweets[tweets.length - 1].id_str}`] =
526 | // cursor.content.value;
527 | // }
528 |
529 | // return tweets;
530 | // },
531 | },
532 | // List timeline
533 | {
534 | path: "/1.1/lists/statuses.json",
535 | method: "GET",
536 | // beforeRequest: (xhr) => {
537 | // try {
538 | // let url = new URL(xhr.modUrl);
539 | // let params = new URLSearchParams(url.search);
540 | // let variables = { count: 40 };
541 | // let features = {
542 | // rweb_lists_timeline_redesign_enabled: false,
543 | // responsive_web_graphql_exclude_directive_enabled: true,
544 | // verified_phone_label_enabled: false,
545 | // creator_subscriptions_tweet_preview_api_enabled: true,
546 | // responsive_web_graphql_timeline_navigation_enabled: true,
547 | // responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
548 | // tweetypie_unmention_optimization_enabled: true,
549 | // responsive_web_edit_tweet_api_enabled: true,
550 | // graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
551 | // view_counts_everywhere_api_enabled: true,
552 | // longform_notetweets_consumption_enabled: true,
553 | // responsive_web_twitter_article_tweet_consumption_enabled: false,
554 | // tweet_awards_web_tipping_enabled: false,
555 | // freedom_of_speech_not_reach_fetch_enabled: true,
556 | // standardized_nudges_misinfo: true,
557 | // tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
558 | // longform_notetweets_rich_text_read_enabled: true,
559 | // longform_notetweets_inline_media_enabled: true,
560 | // responsive_web_media_download_video_enabled: false,
561 | // responsive_web_enhance_cards_enabled: false,
562 | // };
563 |
564 | // let list_id = params.get("list_id");
565 | // let max_id = params.get("max_id");
566 | // if (max_id) {
567 | // let bn = BigInt(params.get("max_id"));
568 | // bn += BigInt(1);
569 | // if (cursors[`list-${list_id}-${bn}`]) {
570 | // variables.cursor = cursors[`list-${list_id}-${bn}`];
571 | // }
572 | // }
573 | // variables.listId = list_id;
574 | // xhr.storage.list_id = list_id;
575 | // xhr.modUrl = `${NEW_API}/2Vjeyo_L0nizAUhHe3fKyA/ListLatestTweetsTimeline?${generateParams(
576 | // features,
577 | // variables
578 | // )}`;
579 | // } catch (e) {
580 | // console.error(e);
581 | // }
582 | // },
583 | beforeSendHeaders: (xhr) => {
584 | xhr.modReqHeaders["Content-Type"] = "application/json";
585 | xhr.modReqHeaders["X-Twitter-Active-User"] = "yes";
586 | xhr.modReqHeaders["X-Twitter-Client-Language"] = "en";
587 | xhr.modReqHeaders["Authorization"] = PUBLIC_TOKENS[1];
588 | delete xhr.modReqHeaders["X-Twitter-Client-Version"];
589 | },
590 | // afterRequest: (xhr) => {
591 | // let data;
592 | // try {
593 | // data = JSON.parse(xhr.responseText);
594 | // } catch (e) {
595 | // console.error(e);
596 | // return [];
597 | // }
598 | // if (data.errors && data.errors[0]) {
599 | // return [];
600 | // }
601 | // let list = data.data.list.tweets_timeline.timeline.instructions.find(
602 | // (i) => i.type === "TimelineAddEntries"
603 | // );
604 | // if (!list) return [];
605 | // list = list.entries;
606 | // let tweets = [];
607 | // for (let e of list) {
608 | // if (e.entryId.startsWith("tweet-")) {
609 | // let res = e.content.itemContent.tweet_results.result;
610 | // let tweet = parseTweet(res);
611 | // if (tweet) {
612 | // tweets.push(tweet);
613 | // }
614 | // } else if (e.entryId.startsWith("list-conversation-")) {
615 | // let lt = e.content.items;
616 | // for (let i = 0; i < lt.length; i++) {
617 | // let t = lt[i];
618 | // if (t.entryId.includes("-tweet-")) {
619 | // let res = t.item.itemContent.tweet_results.result;
620 | // let tweet = parseTweet(res);
621 | // if (!tweet) continue;
622 | // tweets.push(tweet);
623 | // }
624 | // }
625 | // }
626 | // }
627 |
628 | // if (tweets.length === 0) return tweets;
629 |
630 | // // i didn't know they return tweets unsorted???
631 | // tweets.sort(
632 | // (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
633 | // );
634 |
635 | // let cursor = list.find(
636 | // (e) =>
637 | // e.entryId.startsWith("sq-cursor-bottom-") ||
638 | // e.entryId.startsWith("cursor-bottom-")
639 | // );
640 | // if (cursor) {
641 | // cursors[`list-${xhr.storage.list_id}-${tweets[tweets.length - 1].id_str}`] =
642 | // cursor.content.value;
643 | // }
644 |
645 | // return tweets;
646 | // },
647 | },
648 | // User timeline
649 | {
650 | path: "/1.1/statuses/user_timeline.json",
651 | method: "GET",
652 | beforeRequest: (xhr) => {
653 | try {
654 | let url = new URL(xhr.modUrl);
655 | let params = new URLSearchParams(url.search);
656 | let user_id = params.get("user_id");
657 | let variables = {
658 | count: 20,
659 | includePromotedContent: false,
660 | withQuickPromoteEligibilityTweetFields: false,
661 | withVoice: true,
662 | withV2Timeline: true,
663 | };
664 | let features = {
665 | rweb_lists_timeline_redesign_enabled: false,
666 | responsive_web_graphql_exclude_directive_enabled: true,
667 | verified_phone_label_enabled: false,
668 | creator_subscriptions_tweet_preview_api_enabled: true,
669 | responsive_web_graphql_timeline_navigation_enabled: true,
670 | responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
671 | tweetypie_unmention_optimization_enabled: true,
672 | responsive_web_edit_tweet_api_enabled: true,
673 | graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
674 | view_counts_everywhere_api_enabled: true,
675 | longform_notetweets_consumption_enabled: true,
676 | responsive_web_twitter_article_tweet_consumption_enabled: false,
677 | tweet_awards_web_tipping_enabled: false,
678 | freedom_of_speech_not_reach_fetch_enabled: true,
679 | standardized_nudges_misinfo: true,
680 | tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
681 | longform_notetweets_rich_text_read_enabled: true,
682 | longform_notetweets_inline_media_enabled: true,
683 | responsive_web_media_download_video_enabled: false,
684 | responsive_web_enhance_cards_enabled: false,
685 | };
686 |
687 | if (!user_id) {
688 | variables.userId = getCurrentUserId();
689 | } else {
690 | variables.userId = user_id;
691 | }
692 | let max_id = params.get("max_id");
693 | if (max_id) {
694 | let bn = BigInt(params.get("max_id"));
695 | bn += BigInt(1);
696 | if (cursors[`${variables.userId}-${bn}`]) {
697 | variables.cursor = cursors[`${variables.userId}-${bn}`];
698 | }
699 | }
700 | xhr.storage.user_id = variables.userId;
701 |
702 | xhr.modUrl = `${NEW_API}/wxoVeDnl0mP7VLhe6mTOdg/UserTweetsAndReplies?${generateParams(
703 | features,
704 | variables
705 | )}`;
706 | } catch (e) {
707 | console.error(e);
708 | }
709 | },
710 | beforeSendHeaders: (xhr) => {
711 | xhr.modReqHeaders["Content-Type"] = "application/json";
712 | xhr.modReqHeaders["X-Twitter-Active-User"] = "yes";
713 | xhr.modReqHeaders["X-Twitter-Client-Language"] = "en";
714 | xhr.modReqHeaders["Authorization"] =
715 | PUBLIC_TOKENS[1];
716 | delete xhr.modReqHeaders["X-Twitter-Client-Version"];
717 | // delete xhr.modReqHeaders["x-act-as-user-id"];
718 | },
719 | afterRequest: (xhr) => {
720 | let data;
721 | try {
722 | data = JSON.parse(xhr.responseText);
723 | } catch (e) {
724 | console.error(e);
725 | return [];
726 | }
727 | if (data.errors && data.errors[0]) {
728 | return [];
729 | }
730 | let instructions = data.data.user.result.timeline_v2.timeline.instructions;
731 | let entries = instructions.find((e) => e.type === "TimelineAddEntries");
732 | if (!entries) {
733 | return [];
734 | }
735 | entries = entries.entries;
736 | let tweets = [];
737 | for (let entry of entries) {
738 | if (entry.entryId.startsWith("tweet-")) {
739 | let result = entry.content.itemContent.tweet_results.result;
740 | let tweet = parseTweet(result);
741 | if (tweet) {
742 | tweets.push(tweet);
743 | }
744 | } else if (entry.entryId.startsWith("profile-conversation-")) {
745 | let items = entry.content.items;
746 | for (let i = 0; i < items.length; i++) {
747 | let item = items[i];
748 | let result = item.item.itemContent.tweet_results.result;
749 | if (item.entryId.includes("-tweet-")) {
750 | let tweet = parseTweet(result);
751 | if (tweet && tweet.user.id_str === xhr.storage.user_id) {
752 | tweets.push(tweet);
753 | }
754 | }
755 | }
756 | }
757 | }
758 |
759 | if (tweets.length === 0) return tweets;
760 |
761 | // i didn't know they return tweets unsorted???
762 | tweets.sort(
763 | (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
764 | );
765 |
766 | let cursor = entries.find(
767 | (e) =>
768 | e.entryId.startsWith("sq-cursor-bottom-") ||
769 | e.entryId.startsWith("cursor-bottom-")
770 | ).content.value;
771 | if (cursor) {
772 | cursors[`${xhr.storage.user_id}-${tweets[tweets.length - 1].id_str}`] = cursor;
773 | }
774 |
775 | let pinEntry = instructions.find((e) => e.type === "TimelinePinEntry");
776 | if (
777 | pinEntry &&
778 | pinEntry.entry &&
779 | pinEntry.entry.content &&
780 | pinEntry.entry.content.itemContent
781 | ) {
782 | let result = pinEntry.entry.content.itemContent.tweet_results.result;
783 | let pinnedTweet = parseTweet(result);
784 | if (pinnedTweet) {
785 | let tweetTimes = tweets.map((t) => [
786 | t.id_str,
787 | new Date(t.created_at).getTime(),
788 | ]);
789 | tweetTimes.push([
790 | pinnedTweet.id_str,
791 | new Date(pinnedTweet.created_at).getTime(),
792 | ]);
793 | tweetTimes.sort((a, b) => b[1] - a[1]);
794 | let index = tweetTimes.findIndex((t) => t[0] === pinnedTweet.id_str);
795 | if (index !== tweets.length) {
796 | tweets.splice(index, 0, pinnedTweet);
797 | }
798 | }
799 | }
800 |
801 | return tweets;
802 | },
803 | },
804 | // Notifications
805 | {
806 | path: "/1.1/activity/about_me.json",
807 | method: "GET",
808 | // beforeRequest: (xhr) => {
809 | // try {
810 | // let url = new URL(xhr.modUrl);
811 | // let params = new URLSearchParams(url.search);
812 | // let max_id = params.get("max_id");
813 |
814 | // let cursor;
815 | // if(max_id) {
816 | // let bn = BigInt(params.get("max_id"));
817 | // bn += BigInt(1);
818 | // if (cursors[`notifs-${bn}`]) {
819 | // cursor = cursors[`notifs-${bn}`];
820 | // }
821 | // }
822 |
823 | // xhr.modUrl = `https://${location.hostname}/i/api/2/notifications/all.json?include_profile_interstitial_type=1&include_blocking=1&include_blocked_by=1&include_followed_by=1&include_want_retweets=1&include_mute_edge=1&include_can_dm=1&include_can_media_tag=1&include_ext_has_nft_avatar=1&include_ext_is_blue_verified=1&include_ext_verified_type=1&include_ext_profile_image_shape=1&skip_status=1&cards_platform=Web-12&include_cards=1&include_ext_alt_text=true&include_ext_limited_action_results=true&include_quote_count=true&include_reply_count=1&tweet_mode=extended&include_ext_views=true&include_entities=true&include_user_entities=true&include_ext_media_color=true&include_ext_media_availability=true&include_ext_sensitive_media_warning=true&include_ext_trusted_friends_metadata=true&send_error_codes=true&simple_quoted_tweet=true&count=20&requestContext=launch&ext=mediaStats%2ChighlightedLabel%2ChasNftAvatar%2CvoiceInfo%2CbirdwatchPivot%2CsuperFollowMetadata%2CunmentionInfo%2CeditControl${cursor ? `&cursor=${cursor}` : ''}`;
824 | // } catch (e) {
825 | // console.error(e);
826 | // }
827 | // },
828 | beforeSendHeaders: (xhr) => {
829 | xhr.modReqHeaders["Content-Type"] = "application/json";
830 | xhr.modReqHeaders["X-Twitter-Active-User"] = "yes";
831 | xhr.modReqHeaders["X-Twitter-Client-Language"] = "en";
832 | xhr.modReqHeaders["Authorization"] = PUBLIC_TOKENS[1];
833 | delete xhr.modReqHeaders["X-Twitter-Client-Version"];
834 | },
835 | // afterRequest: (xhr) => {
836 | // let data;
837 | // try {
838 | // data = JSON.parse(xhr.responseText);
839 | // } catch (e) {
840 | // console.error(e);
841 | // return [];
842 | // }
843 | // if (data.errors && data.errors[0]) {
844 | // return [];
845 | // }
846 | // },
847 | },
848 | // Mentions timeline
849 | {
850 | path: "/1.1/statuses/mentions_timeline.json",
851 | method: "GET",
852 | beforeSendHeaders: (xhr) => {
853 | xhr.modReqHeaders["Content-Type"] = "application/json";
854 | xhr.modReqHeaders["X-Twitter-Active-User"] = "yes";
855 | xhr.modReqHeaders["X-Twitter-Client-Language"] = "en";
856 | xhr.modReqHeaders["Authorization"] = PUBLIC_TOKENS[1];
857 | delete xhr.modReqHeaders["X-Twitter-Client-Version"];
858 | },
859 | },
860 | // User likes timeline
861 | {
862 | path: "/1.1/favorites/list.json",
863 | method: "GET",
864 | beforeRequest: (xhr) => {
865 | try {
866 | let url = new URL(xhr.modUrl);
867 | let params = new URLSearchParams(url.search);
868 | let user_id = params.get("user_id") ?? getCurrentUserId();
869 | let variables = {
870 | "userId": user_id,
871 | "count": 50,
872 | "includePromotedContent": false,
873 | "withSuperFollowsUserFields": true,
874 | "withDownvotePerspective": false,
875 | "withReactionsMetadata": false,
876 | "withReactionsPerspective": false,
877 | "withSuperFollowsTweetFields": true,
878 | "withClientEventToken": false,
879 | "withBirdwatchNotes": false,
880 | "withVoice": true,
881 | "withV2Timeline": true
882 | };
883 | let features = {
884 | "dont_mention_me_view_api_enabled": true,
885 | "interactive_text_enabled": true,
886 | "responsive_web_uc_gql_enabled": false,
887 | "vibe_tweet_context_enabled": false,
888 | "responsive_web_edit_tweet_api_enabled": false,
889 | "standardized_nudges_misinfo": false,
890 | "responsive_web_enhance_cards_enabled": false
891 | };
892 |
893 | let max_id = params.get("max_id");
894 | if (max_id) {
895 | let bn = BigInt(params.get("max_id"));
896 | bn += BigInt(1);
897 | if (cursors[`${variables.userId}-${bn}-likes`]) {
898 | variables.cursor = cursors[`${variables.userId}-${bn}-likes`];
899 | }
900 | }
901 | xhr.storage.user_id = variables.userId;
902 |
903 | xhr.modUrl = `${NEW_API}/vni8vUvtZvJoIsl49VPudg/Likes?${generateParams(
904 | features,
905 | variables
906 | )}`;
907 | } catch (e) {
908 | console.error(e);
909 | }
910 | },
911 | beforeSendHeaders: (xhr) => {
912 | xhr.modReqHeaders["Content-Type"] = "application/json";
913 | xhr.modReqHeaders["X-Twitter-Active-User"] = "yes";
914 | xhr.modReqHeaders["X-Twitter-Client-Language"] = "en";
915 | xhr.modReqHeaders["Authorization"] =
916 | PUBLIC_TOKENS[localStorage.OTDuseDifferentToken === "1" ? (Math.random() > 0.5 ? 1 : 0) : 0];
917 | delete xhr.modReqHeaders["X-Twitter-Client-Version"];
918 | // delete xhr.modReqHeaders["x-act-as-user-id"];
919 | },
920 | afterRequest: (xhr) => {
921 | let data;
922 | try {
923 | data = JSON.parse(xhr.responseText);
924 | } catch (e) {
925 | console.error(e);
926 | return [];
927 | }
928 | if (data.errors && data.errors[0]) {
929 | return [];
930 | }
931 | let instructions = data.data.user.result.timeline_v2.timeline.instructions;
932 | let entries = instructions.find((e) => e.type === "TimelineAddEntries");
933 | if (!entries) {
934 | return [];
935 | }
936 | entries = entries.entries;
937 |
938 | let tweets = entries
939 | .filter(e => e.entryId.startsWith('tweet-') && e.content.itemContent.tweet_results.result)
940 | .map(e => parseTweet(e.content.itemContent.tweet_results.result))
941 | .filter(e => e);
942 |
943 | if (tweets.length === 0) return tweets;
944 |
945 | let cursor = entries.find(
946 | (e) =>
947 | e.entryId.startsWith("sq-cursor-bottom-") ||
948 | e.entryId.startsWith("cursor-bottom-")
949 | ).content.value;
950 | if (cursor) {
951 | cursors[`${xhr.storage.user_id}-${tweets[tweets.length - 1].id_str}-likes`] = cursor;
952 | }
953 |
954 | // i didn't know they return tweets unsorted???
955 | tweets.sort(
956 | (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
957 | );
958 |
959 | return tweets;
960 | },
961 | },
962 | // Liking / unliking
963 | {
964 | path: /\/1\.1\/favorites\/.*\.json/,
965 | method: "POST",
966 | beforeSendHeaders: (xhr) => {
967 | xhr.modReqHeaders["X-Twitter-Active-User"] = "yes";
968 | xhr.modReqHeaders["X-Twitter-Client-Language"] = "en";
969 | xhr.modReqHeaders["Authorization"] = PUBLIC_TOKENS[1];
970 | delete xhr.modReqHeaders["X-Twitter-Client-Version"];
971 | },
972 | },
973 | // Collections
974 | {
975 | path: /\/1\.1\/collections\/.*\.json/,
976 | method: "GET",
977 | beforeSendHeaders: (xhr) => {
978 | xhr.modReqHeaders["X-Twitter-Active-User"] = "yes";
979 | xhr.modReqHeaders["X-Twitter-Client-Language"] = "en";
980 | xhr.modReqHeaders["Authorization"] = PUBLIC_TOKENS[1];
981 | delete xhr.modReqHeaders["X-Twitter-Client-Version"];
982 | },
983 | },
984 | {
985 | path: /\/1\.1\/collections\/.*\.json/,
986 | method: "POST",
987 | beforeSendHeaders: (xhr) => {
988 | xhr.modReqHeaders["X-Twitter-Active-User"] = "yes";
989 | xhr.modReqHeaders["X-Twitter-Client-Language"] = "en";
990 | xhr.modReqHeaders["Authorization"] = PUBLIC_TOKENS[1];
991 | delete xhr.modReqHeaders["X-Twitter-Client-Version"];
992 | },
993 | },
994 | // User profile
995 | {
996 | path: "/1.1/users/show.json",
997 | method: "GET",
998 | beforeSendHeaders: (xhr) => {
999 | xhr.modReqHeaders["X-Twitter-Active-User"] = "yes";
1000 | xhr.modReqHeaders["X-Twitter-Client-Language"] = "en";
1001 | xhr.modReqHeaders["Authorization"] = PUBLIC_TOKENS[1];
1002 | delete xhr.modReqHeaders["X-Twitter-Client-Version"];
1003 | },
1004 | },
1005 | // Search
1006 | {
1007 | path: "/1.1/search/universal.json",
1008 | method: "GET",
1009 | beforeRequest: (xhr) => {
1010 | try {
1011 | let url = new URL(xhr.modUrl);
1012 | let params = new URLSearchParams(url.search);
1013 | let variables = {
1014 | rawQuery: params.get("q"),
1015 | count: 20,
1016 | querySource: "typed_query",
1017 | product: "Latest",
1018 | };
1019 | let features = {"responsive_web_graphql_exclude_directive_enabled":true,"verified_phone_label_enabled":false,"creator_subscriptions_tweet_preview_api_enabled":true,"responsive_web_graphql_timeline_navigation_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"communities_web_enable_tweet_community_results_fetch":true,"c9s_tweet_anatomy_moderator_badge_enabled":true,"tweetypie_unmention_optimization_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"responsive_web_twitter_article_tweet_consumption_enabled":true,"tweet_awards_web_tipping_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"rweb_video_timestamps_enabled":true,"longform_notetweets_rich_text_read_enabled":true,"longform_notetweets_inline_media_enabled":true,"responsive_web_enhance_cards_enabled":false};
1020 |
1021 | xhr.modUrl = `${NEW_API}/l0dLMlz_fHji3FT8AfrvxA/SearchTimeline?${generateParams(
1022 | features,
1023 | variables
1024 | )}`;
1025 | } catch (e) {
1026 | console.error(e);
1027 | }
1028 | },
1029 | beforeSendHeaders: (xhr) => {
1030 | xhr.modReqHeaders["Content-Type"] = "application/json";
1031 | xhr.modReqHeaders["X-Twitter-Active-User"] = "yes";
1032 | xhr.modReqHeaders["X-Twitter-Client-Language"] = "en";
1033 | xhr.modReqHeaders["Authorization"] =
1034 | PUBLIC_TOKENS[1];
1035 | delete xhr.modReqHeaders["X-Twitter-Client-Version"];
1036 | },
1037 | afterRequest: (xhr) => {
1038 | let data;
1039 | try {
1040 | data = JSON.parse(xhr.responseText);
1041 | } catch (e) {
1042 | console.error(e);
1043 | return [];
1044 | }
1045 | if (data.errors && data.errors[0]) {
1046 | return [];
1047 | }
1048 | let instructions = data.data.search_by_raw_query.search_timeline.timeline.instructions;
1049 | let entries = instructions.find((i) => i.entries);
1050 | if (!entries) {
1051 | return [];
1052 | }
1053 | entries = entries.entries;
1054 | let res = [];
1055 | for (let entry of entries) {
1056 | if (entry.entryId.startsWith("sq-I-t-") || entry.entryId.startsWith("tweet-")) {
1057 | let result = entry.content.itemContent.tweet_results.result;
1058 |
1059 | if (entry.content.itemContent.promotedMetadata) {
1060 | continue;
1061 | }
1062 | let tweet = parseTweet(result);
1063 | if (!tweet) {
1064 | continue;
1065 | }
1066 | res.push(tweet);
1067 | }
1068 | }
1069 | let cursor = entries.find(
1070 | (e) =>
1071 | e.entryId.startsWith("sq-cursor-bottom-") ||
1072 | e.entryId.startsWith("cursor-bottom-")
1073 | );
1074 | if (cursor) {
1075 | cursor = cursor.content.value;
1076 | } else {
1077 | cursor = instructions.find(
1078 | (e) =>
1079 | e.entry_id_to_replace &&
1080 | (e.entry_id_to_replace.startsWith("sq-cursor-bottom-") ||
1081 | e.entry_id_to_replace.startsWith("cursor-bottom-"))
1082 | );
1083 | if (cursor) {
1084 | cursor = cursor.entry.content.value;
1085 | } else {
1086 | cursor = null;
1087 | }
1088 | }
1089 |
1090 | return {
1091 | metadata: {
1092 | cursor,
1093 | refresh_interval_in_sec: 30,
1094 | },
1095 | modules: res.map((t) => ({ status: { data: t } })),
1096 | };
1097 | },
1098 | },
1099 | // User search
1100 | {
1101 | path: "/1.1/users/search.json",
1102 | method: "GET",
1103 | beforeRequest: (xhr) => {
1104 | try {
1105 | let url = new URL(xhr.modUrl);
1106 | let params = new URLSearchParams(url.search);
1107 | let variables = {
1108 | rawQuery: params.get("q"),
1109 | count: 20,
1110 | querySource: "typed_query",
1111 | product: "People",
1112 | };
1113 | let features = {
1114 | rweb_lists_timeline_redesign_enabled: false,
1115 | responsive_web_graphql_exclude_directive_enabled: true,
1116 | verified_phone_label_enabled: false,
1117 | creator_subscriptions_tweet_preview_api_enabled: true,
1118 | responsive_web_graphql_timeline_navigation_enabled: true,
1119 | responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
1120 | tweetypie_unmention_optimization_enabled: true,
1121 | responsive_web_edit_tweet_api_enabled: true,
1122 | graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
1123 | view_counts_everywhere_api_enabled: true,
1124 | longform_notetweets_consumption_enabled: true,
1125 | responsive_web_twitter_article_tweet_consumption_enabled: false,
1126 | tweet_awards_web_tipping_enabled: false,
1127 | freedom_of_speech_not_reach_fetch_enabled: true,
1128 | standardized_nudges_misinfo: true,
1129 | tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
1130 | longform_notetweets_rich_text_read_enabled: true,
1131 | longform_notetweets_inline_media_enabled: true,
1132 | responsive_web_media_download_video_enabled: false,
1133 | responsive_web_enhance_cards_enabled: false,
1134 | };
1135 |
1136 | xhr.modUrl = `${NEW_API}/nK1dw4oV3k4w5TdtcAdSww/SearchTimeline?${generateParams(
1137 | features,
1138 | variables
1139 | )}`;
1140 | } catch (e) {
1141 | console.error(e);
1142 | }
1143 | },
1144 | beforeSendHeaders: (xhr) => {
1145 | xhr.modReqHeaders["Content-Type"] = "application/json";
1146 | xhr.modReqHeaders["X-Twitter-Active-User"] = "yes";
1147 | xhr.modReqHeaders["X-Twitter-Client-Language"] = "en";
1148 | xhr.modReqHeaders["Authorization"] =
1149 | PUBLIC_TOKENS[1];
1150 | delete xhr.modReqHeaders["X-Twitter-Client-Version"];
1151 | },
1152 | afterRequest: (xhr) => {
1153 | let data;
1154 | try {
1155 | data = JSON.parse(xhr.responseText);
1156 | } catch (e) {
1157 | console.error(e);
1158 | return [];
1159 | }
1160 | if (data.errors && data.errors[0]) {
1161 | return [];
1162 | }
1163 | let instructions = data.data.search_by_raw_query.search_timeline.timeline.instructions;
1164 | let entries = instructions.find((i) => i.entries);
1165 | if (!entries) {
1166 | return [];
1167 | }
1168 | entries = entries.entries;
1169 | let res = [];
1170 | for (let entry of entries) {
1171 | if (entry.entryId.startsWith("sq-I-u-") || entry.entryId.startsWith("user-")) {
1172 | let result = entry.content.itemContent.user_results.result;
1173 | if (!result || !result.legacy) {
1174 | console.log("Bug: no user", entry);
1175 | continue;
1176 | }
1177 | let user = result.legacy;
1178 | user.id_str = result.rest_id;
1179 | res.push(user);
1180 | }
1181 | }
1182 | let cursor = entries.find(
1183 | (e) =>
1184 | e.entryId.startsWith("sq-cursor-bottom-") ||
1185 | e.entryId.startsWith("cursor-bottom-")
1186 | );
1187 | if (cursor) {
1188 | cursor = cursor.content.value;
1189 | } else {
1190 | cursor = instructions.find(
1191 | (e) =>
1192 | e.entry_id_to_replace &&
1193 | (e.entry_id_to_replace.startsWith("sq-cursor-bottom-") ||
1194 | e.entry_id_to_replace.startsWith("cursor-bottom-"))
1195 | );
1196 | if (cursor) {
1197 | cursor = cursor.entry.content.value;
1198 | } else {
1199 | cursor = null;
1200 | }
1201 | }
1202 |
1203 | return res;
1204 | },
1205 | },
1206 | // Tweet creation
1207 | {
1208 | path: "/1.1/statuses/update.json",
1209 | method: "POST",
1210 | beforeRequest: (xhr) => {
1211 | xhr.modUrl = `https://${location.hostname}/i/api/graphql/oB-5XsHNAbjvARJEc8CZFw/CreateTweet`;
1212 | },
1213 | beforeSendHeaders: (xhr) => {
1214 | xhr.modReqHeaders["Content-Type"] = "application/json";
1215 | xhr.modReqHeaders["X-Twitter-Active-User"] = "yes";
1216 | xhr.modReqHeaders["X-Twitter-Client-Language"] = "en";
1217 | xhr.modReqHeaders["Authorization"] = PUBLIC_TOKENS[1];
1218 | delete xhr.modReqHeaders["X-Twitter-Client-Version"];
1219 | },
1220 | beforeSendBody: (xhr, body) => {
1221 | let params = Object.fromEntries(new URLSearchParams(body));
1222 | let variables = {
1223 | tweet_text: params.status,
1224 | media: {
1225 | media_entities: [],
1226 | possibly_sensitive: false,
1227 | },
1228 | semantic_annotation_ids: [],
1229 | dark_request: false,
1230 | };
1231 | if (params.in_reply_to_status_id) {
1232 | variables.reply = {
1233 | in_reply_to_tweet_id: params.in_reply_to_status_id,
1234 | exclude_reply_user_ids: [],
1235 | };
1236 | if (params.exclude_reply_user_ids) {
1237 | variables.reply.exclude_reply_user_ids =
1238 | params.exclude_reply_user_ids.split(",");
1239 | }
1240 | }
1241 | if (params.media_ids) {
1242 | variables.media.media_entities = params.media_ids
1243 | .split(",")
1244 | .map((id) => ({ media_id: id, tagged_users: [] }));
1245 | }
1246 | if (params.attachment_url) {
1247 | variables.attachment_url = params.attachment_url;
1248 | }
1249 |
1250 | return JSON.stringify({
1251 | variables,
1252 | features: {"communities_web_enable_tweet_community_results_fetch":true,"c9s_tweet_anatomy_moderator_badge_enabled":true,"tweetypie_unmention_optimization_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"responsive_web_twitter_article_tweet_consumption_enabled":true,"tweet_awards_web_tipping_enabled":false,"creator_subscriptions_quote_tweet_preview_enabled":false,"longform_notetweets_rich_text_read_enabled":true,"longform_notetweets_inline_media_enabled":true,"articles_preview_enabled":true,"rweb_video_timestamps_enabled":true,"rweb_tipjar_consumption_enabled":true,"responsive_web_graphql_exclude_directive_enabled":true,"verified_phone_label_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":true,"responsive_web_enhance_cards_enabled":false},
1253 | queryId: "oB-5XsHNAbjvARJEc8CZFw",
1254 | });
1255 | },
1256 | afterRequest: (xhr) => {
1257 | let data;
1258 | try {
1259 | data = JSON.parse(xhr.responseText);
1260 | } catch (e) {
1261 | console.error(e);
1262 | return {};
1263 | }
1264 | if (data.errors && data.errors[0]) {
1265 | return {};
1266 | }
1267 | let tweet = parseTweet(data.data.create_tweet.tweet_results.result);
1268 | return tweet;
1269 | },
1270 | },
1271 | // Retweeting
1272 | {
1273 | path: /\/1.1\/statuses\/retweet\/(\d+).json/,
1274 | method: "POST",
1275 | beforeRequest: (xhr) => {
1276 | let originalUrl = new URL(xhr.originalUrl);
1277 | xhr.storage.tweet_id = originalUrl.pathname.match(
1278 | /\/1.1\/statuses\/retweet\/(\d+).json/
1279 | )[1];
1280 | xhr.storage.retweeter = getCurrentUserId();
1281 | xhr.modUrl = `https://${location.hostname}/i/api/graphql/ojPdsZsimiJrUGLR1sjUtA/CreateRetweet`;
1282 | },
1283 | beforeSendHeaders: (xhr) => {
1284 | xhr.modReqHeaders["Content-Type"] = "application/json";
1285 | xhr.modReqHeaders["X-Twitter-Active-User"] = "yes";
1286 | xhr.modReqHeaders["X-Twitter-Client-Language"] = "en";
1287 | xhr.modReqHeaders["Authorization"] = PUBLIC_TOKENS[1];
1288 | delete xhr.modReqHeaders["X-Twitter-Client-Version"];
1289 | if (xhr.modReqHeaders["x-act-as-user-id"]) {
1290 | xhr.storage.retweeter = xhr.modReqHeaders["x-act-as-user-id"];
1291 | }
1292 | },
1293 | beforeSendBody: (xhr, body) => {
1294 | return JSON.stringify({
1295 | variables: {
1296 | tweet_id: xhr.storage.tweet_id,
1297 | dark_request: false,
1298 | },
1299 | queryId: "ojPdsZsimiJrUGLR1sjUtA",
1300 | });
1301 | },
1302 | afterRequest: (xhr) => {
1303 | let data;
1304 | try {
1305 | data = JSON.parse(xhr.responseText);
1306 | } catch (e) {
1307 | console.error(e);
1308 | return {};
1309 | }
1310 | if (data.errors && data.errors[0]) {
1311 | return {};
1312 | }
1313 | let res = data.data.create_retweet.retweet_results.result;
1314 | let tweet = res.legacy;
1315 | tweet.id_str = res.rest_id;
1316 | if (!tweet.user) {
1317 | tweet.user = {
1318 | id_str: xhr.storage.retweeter,
1319 | };
1320 | }
1321 | return tweet;
1322 | },
1323 | },
1324 | // Unretweeting
1325 | {
1326 | path: /\/1.1\/statuses\/unretweet\/(\d+).json/,
1327 | method: "POST",
1328 | beforeRequest: (xhr) => {
1329 | let originalUrl = new URL(xhr.originalUrl);
1330 | xhr.storage.tweet_id = originalUrl.pathname.match(
1331 | /\/1.1\/statuses\/unretweet\/(\d+).json/
1332 | )[1];
1333 | xhr.storage.retweeter = getCurrentUserId();
1334 | xhr.modUrl = `https://${location.hostname}/i/api/graphql/iQtK4dl5hBmXewYZuEOKVw/DeleteRetweet`;
1335 | },
1336 | beforeSendHeaders: (xhr) => {
1337 | xhr.modReqHeaders["Content-Type"] = "application/json";
1338 | xhr.modReqHeaders["X-Twitter-Active-User"] = "yes";
1339 | xhr.modReqHeaders["X-Twitter-Client-Language"] = "en";
1340 | xhr.modReqHeaders["Authorization"] =
1341 | PUBLIC_TOKENS[localStorage.OTDuseDifferentToken === "1" ? 1 : 0];
1342 | delete xhr.modReqHeaders["X-Twitter-Client-Version"];
1343 | if (xhr.modReqHeaders["x-act-as-user-id"]) {
1344 | xhr.storage.retweeter = xhr.modReqHeaders["x-act-as-user-id"];
1345 | }
1346 | },
1347 | beforeSendBody: (xhr, body) => {
1348 | return JSON.stringify({
1349 | variables: { source_tweet_id: xhr.storage.tweet_id, dark_request: false },
1350 | queryId: "iQtK4dl5hBmXewYZuEOKVw",
1351 | });
1352 | },
1353 | afterRequest: (xhr) => {
1354 | let data;
1355 | try {
1356 | data = JSON.parse(xhr.responseText);
1357 | } catch (e) {
1358 | console.error(e);
1359 | return {};
1360 | }
1361 | if (data.errors && data.errors[0]) {
1362 | return {};
1363 | }
1364 | let res = data.data.unretweet.source_tweet_results.result;
1365 | let tweet = res.legacy;
1366 | tweet.id_str = res.rest_id;
1367 | if (!tweet.user) {
1368 | tweet.user = {
1369 | id_str: xhr.storage.retweeter,
1370 | };
1371 | }
1372 | return tweet;
1373 | },
1374 | },
1375 | // Getting tweet details
1376 | {
1377 | path: /\/1.1\/statuses\/show\/(\d+).json/,
1378 | method: "GET",
1379 | beforeRequest: (xhr) => {
1380 | let originalUrl = new URL(xhr.originalUrl);
1381 | xhr.storage.tweet_id = originalUrl.pathname.match(
1382 | /\/1.1\/statuses\/show\/(\d+).json/
1383 | )[1];
1384 | xhr.modUrl = `https://${location.hostname}/i/api/graphql/KwGBbJZc6DBx8EKmyQSP7g/TweetDetail?variables=${encodeURIComponent(
1385 | JSON.stringify({
1386 | focalTweetId: xhr.storage.tweet_id,
1387 | with_rux_injections: false,
1388 | includePromotedContent: false,
1389 | withCommunity: true,
1390 | withQuickPromoteEligibilityTweetFields: true,
1391 | withBirdwatchNotes: true,
1392 | withVoice: true,
1393 | withV2Timeline: true,
1394 | })
1395 | )}&features=${encodeURIComponent(
1396 | JSON.stringify({
1397 | rweb_lists_timeline_redesign_enabled: false,
1398 | blue_business_profile_image_shape_enabled: true,
1399 | responsive_web_graphql_exclude_directive_enabled: true,
1400 | verified_phone_label_enabled: false,
1401 | creator_subscriptions_tweet_preview_api_enabled: false,
1402 | responsive_web_graphql_timeline_navigation_enabled: true,
1403 | responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
1404 | tweetypie_unmention_optimization_enabled: true,
1405 | vibe_api_enabled: true,
1406 | responsive_web_edit_tweet_api_enabled: true,
1407 | graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
1408 | view_counts_everywhere_api_enabled: true,
1409 | longform_notetweets_consumption_enabled: true,
1410 | tweet_awards_web_tipping_enabled: false,
1411 | freedom_of_speech_not_reach_fetch_enabled: true,
1412 | standardized_nudges_misinfo: true,
1413 | tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: false,
1414 | interactive_text_enabled: true,
1415 | responsive_web_text_conversations_enabled: false,
1416 | longform_notetweets_rich_text_read_enabled: true,
1417 | longform_notetweets_inline_media_enabled: false,
1418 | responsive_web_enhance_cards_enabled: false,
1419 | })
1420 | )}`;
1421 | },
1422 | beforeSendHeaders: (xhr) => {
1423 | xhr.modReqHeaders["Content-Type"] = "application/json";
1424 | xhr.modReqHeaders["X-Twitter-Active-User"] = "yes";
1425 | xhr.modReqHeaders["X-Twitter-Client-Language"] = "en";
1426 | xhr.modReqHeaders["Authorization"] =
1427 | PUBLIC_TOKENS[1];
1428 | delete xhr.modReqHeaders["X-Twitter-Client-Version"];
1429 | },
1430 | afterRequest: (xhr) => {
1431 | let data;
1432 | try {
1433 | data = JSON.parse(xhr.responseText);
1434 | } catch (e) {
1435 | console.error(e);
1436 | return {};
1437 | }
1438 | if (data.errors && data.errors[0]) {
1439 | return {};
1440 | }
1441 | let ic = data.data.threaded_conversation_with_injections_v2.instructions
1442 | .find((i) => i.type === "TimelineAddEntries")
1443 | .entries.find((e) => e.entryId === `tweet-${xhr.storage.tweet_id}`)
1444 | .content.itemContent;
1445 | let res = ic.tweet_results.result;
1446 | let tweet = parseTweet(res);
1447 | return tweet;
1448 | },
1449 | },
1450 | {
1451 | path: "/1.1/statuses/show.json",
1452 | method: "GET",
1453 | beforeRequest: (xhr) => {
1454 | let originalUrl = new URL(xhr.originalUrl);
1455 | xhr.storage.tweet_id = originalUrl.searchParams.get("id");
1456 | xhr.modUrl = `https://${location.hostname}/i/api/graphql/KwGBbJZc6DBx8EKmyQSP7g/TweetDetail?variables=${encodeURIComponent(
1457 | JSON.stringify({
1458 | focalTweetId: xhr.storage.tweet_id,
1459 | with_rux_injections: false,
1460 | includePromotedContent: false,
1461 | withCommunity: true,
1462 | withQuickPromoteEligibilityTweetFields: true,
1463 | withBirdwatchNotes: true,
1464 | withVoice: true,
1465 | withV2Timeline: true,
1466 | })
1467 | )}&features=${encodeURIComponent(
1468 | JSON.stringify({
1469 | rweb_lists_timeline_redesign_enabled: false,
1470 | blue_business_profile_image_shape_enabled: true,
1471 | responsive_web_graphql_exclude_directive_enabled: true,
1472 | verified_phone_label_enabled: false,
1473 | creator_subscriptions_tweet_preview_api_enabled: false,
1474 | responsive_web_graphql_timeline_navigation_enabled: true,
1475 | responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
1476 | tweetypie_unmention_optimization_enabled: true,
1477 | vibe_api_enabled: true,
1478 | responsive_web_edit_tweet_api_enabled: true,
1479 | graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
1480 | view_counts_everywhere_api_enabled: true,
1481 | longform_notetweets_consumption_enabled: true,
1482 | tweet_awards_web_tipping_enabled: false,
1483 | freedom_of_speech_not_reach_fetch_enabled: true,
1484 | standardized_nudges_misinfo: true,
1485 | tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: false,
1486 | interactive_text_enabled: true,
1487 | responsive_web_text_conversations_enabled: false,
1488 | longform_notetweets_rich_text_read_enabled: true,
1489 | longform_notetweets_inline_media_enabled: false,
1490 | responsive_web_enhance_cards_enabled: false,
1491 | })
1492 | )}`;
1493 | },
1494 | beforeSendHeaders: (xhr) => {
1495 | xhr.modReqHeaders["Content-Type"] = "application/json";
1496 | xhr.modReqHeaders["X-Twitter-Active-User"] = "yes";
1497 | xhr.modReqHeaders["X-Twitter-Client-Language"] = "en";
1498 | xhr.modReqHeaders["Authorization"] =
1499 | PUBLIC_TOKENS[1];
1500 | delete xhr.modReqHeaders["X-Twitter-Client-Version"];
1501 | },
1502 | afterRequest: (xhr) => {
1503 | let data;
1504 | try {
1505 | data = JSON.parse(xhr.responseText);
1506 | } catch (e) {
1507 | console.error(e);
1508 | return {};
1509 | }
1510 | if (data.errors && data.errors[0]) {
1511 | return {};
1512 | }
1513 | let ic = data.data.threaded_conversation_with_injections_v2.instructions
1514 | .find((i) => i.type === "TimelineAddEntries")
1515 | .entries.find((e) => e.entryId === `tweet-${xhr.storage.tweet_id}`)
1516 | .content.itemContent;
1517 | let res = ic.tweet_results.result;
1518 | let tweet = parseTweet(res);
1519 | return tweet;
1520 | },
1521 | },
1522 | // Tweet deletion
1523 | {
1524 | path: /\/1.1\/statuses\/destroy\/(\d+).json/,
1525 | method: "POST",
1526 | beforeRequest: (xhr) => {
1527 | let originalUrl = new URL(xhr.originalUrl);
1528 | xhr.storage.tweet_id = originalUrl.pathname.match(
1529 | /\/1.1\/statuses\/destroy\/(\d+).json/
1530 | )[1];
1531 | xhr.modUrl = `https://${location.hostname}/i/api/graphql/VaenaVgh5q5ih7kvyVjgtg/DeleteTweet`;
1532 | },
1533 | beforeSendHeaders: (xhr) => {
1534 | xhr.modReqHeaders["Content-Type"] = "application/json";
1535 | xhr.modReqHeaders["X-Twitter-Active-User"] = "yes";
1536 | xhr.modReqHeaders["X-Twitter-Client-Language"] = "en";
1537 | xhr.modReqHeaders["Authorization"] =
1538 | PUBLIC_TOKENS[localStorage.OTDuseDifferentToken === "1" ? 1 : 0];
1539 | delete xhr.modReqHeaders["X-Twitter-Client-Version"];
1540 | },
1541 | beforeSendBody: (xhr, body) => {
1542 | return JSON.stringify({
1543 | variables: { tweet_id: xhr.storage.tweet_id, dark_request: false },
1544 | queryId: "VaenaVgh5q5ih7kvyVjgtg",
1545 | });
1546 | },
1547 | },
1548 | // Tweet replies
1549 | {
1550 | path: /\/2\/timeline\/conversation\/(\d+).json/,
1551 | method: "GET",
1552 | beforeRequest: (xhr) => {
1553 | let originalUrl = new URL(xhr.originalUrl);
1554 | let params = new URLSearchParams(originalUrl.search);
1555 |
1556 | params.delete("ext");
1557 | params.delete("include_ext_has_nft_avatar");
1558 | params.delete("include_ext_is_blue_verified");
1559 | params.delete("include_ext_verified_type");
1560 | params.delete("include_ext_sensitive_media_warning");
1561 | params.delete("include_ext_media_color");
1562 |
1563 | originalUrl.search = params.toString();
1564 |
1565 | xhr.modUrl = originalUrl.toString();
1566 | },
1567 | afterRequest: (xhr) => {
1568 | let data;
1569 | try {
1570 | data = JSON.parse(xhr.responseText);
1571 | } catch (e) {
1572 | console.error(e);
1573 | return data;
1574 | }
1575 | if (data.errors && data.errors[0]) {
1576 | return data;
1577 | }
1578 | for (let id in data.globalObjects.tweets) {
1579 | let tweet = data.globalObjects.tweets[id];
1580 |
1581 | if (!tweet.contributors) tweet.contributors = null;
1582 | if (tweet.conversation_id_str)
1583 | tweet.conversation_id = parseInt(tweet.conversation_id_str);
1584 | if (!tweet.coordinates) tweet.coordinates = null;
1585 | if (!tweet.conversation_muted) tweet.conversation_muted = false;
1586 | if (!tweet.favorited) tweet.favorited = false;
1587 | if (!tweet.geo) tweet.geo = null;
1588 | if (!tweet.id) tweet.id = parseInt(id);
1589 | if (!tweet.in_reply_to_screen_name) tweet.in_reply_to_screen_name = null;
1590 | if (!tweet.in_reply_to_status_id) tweet.in_reply_to_status_id = null;
1591 | if (!tweet.in_reply_to_status_id_str) tweet.in_reply_to_status_id_str = null;
1592 | if (!tweet.in_reply_to_user_id) tweet.in_reply_to_user_id = null;
1593 | if (!tweet.in_reply_to_user_id_str) tweet.in_reply_to_user_id_str = null;
1594 | if (!tweet.is_quote_status) tweet.is_quote_status = false;
1595 | if (!tweet.place) tweet.place = null;
1596 | if (!tweet.supplemental_language) tweet.supplemental_language = null;
1597 | if (!tweet.retweeted) tweet.retweeted = false;
1598 | if (!tweet.truncated) tweet.truncated = false;
1599 | if (!tweet.user_id) tweet.user_id = parseInt(tweet.user_id_str);
1600 | }
1601 |
1602 | for (let id in data.globalObjects.users) {
1603 | let user = data.globalObjects.users[id];
1604 |
1605 | if (!user.default_profile) user.default_profile = false;
1606 | if (!user.default_profile_image) user.default_profile_image = false;
1607 | if (!user.entities.description) user.entities.description = { urls: [] };
1608 | if (!user.entities.description.urls) user.entities.description.urls = [];
1609 | if (!user.entities.url) user.entities.url = { urls: [] };
1610 | if (!user.entities.url.urls) user.entities.url.urls = [];
1611 | if (!user.follow_request_sent) user.follow_request_sent = false;
1612 | if (!user.following) user.following = false;
1613 | if (!user.has_extended_profile) user.has_extended_profile = false;
1614 | if (!user.is_translation_enabled) user.is_translation_enabled = false;
1615 | if (!user.is_translator) user.is_translator = false;
1616 | if (!user.followed_by) user.followed_by = false;
1617 | if (!user.id) user.id = parseInt(id);
1618 | if (!user.lang) user.lang = null;
1619 | if (!user.notifications) user.notifications = false;
1620 | if (!user.profile_background_color) user.profile_background_color = "C0DEED";
1621 | if (!user.profile_background_image_url)
1622 | user.profile_background_image_url =
1623 | "http://abs.twimg.com/images/themes/theme1/bg.png";
1624 | if (!user.profile_background_image_url_https)
1625 | user.profile_background_image_url_https =
1626 | "https://abs.twimg.com/images/themes/theme1/bg.png";
1627 | if (!user.profile_background_tile) user.profile_background_tile = false;
1628 | if (!user.profile_link_color) user.profile_link_color = "1DA1F2";
1629 | if (!user.profile_image_url && user.profile_image_url_https)
1630 | user.profile_image_url = user.profile_image_url_https.replace(
1631 | "https://",
1632 | "http://"
1633 | );
1634 | if (!user.profile_sidebar_border_color)
1635 | user.profile_sidebar_border_color = "000000";
1636 | if (!user.profile_sidebar_fill_color) user.profile_sidebar_fill_color = "DDEEF6";
1637 | if (!user.profile_text_color) user.profile_text_color = "333333";
1638 | if (!user.profile_use_background_image) user.profile_use_background_image = true;
1639 | if (!user.protected) user.protected = false;
1640 | if (!user.require_some_consent) user.require_some_consent = false;
1641 | if (!user.time_zone) user.time_zone = null;
1642 | if (!user.utc_offset) user.utc_offset = null;
1643 | if (!user.verified) user.verified = false;
1644 | }
1645 |
1646 | let entries = data.timeline.instructions.find((i) => i.addEntries);
1647 | if (entries) {
1648 | entries.addEntries.entries = entries.addEntries.entries.filter(
1649 | (e) => !e.entryId.startsWith("tweetComposer-")
1650 | );
1651 | for (let entry of entries.addEntries.entries) {
1652 | if (entry.entryId.startsWith("conversationThread-")) {
1653 | let newContent = {
1654 | item: {
1655 | content: {
1656 | conversationThread: {
1657 | conversationComponents: [],
1658 | },
1659 | },
1660 | },
1661 | };
1662 | if (entry.content.timelineModule.items)
1663 | for (let item of entry.content.timelineModule.items) {
1664 | if (item.item && item.item.content && item.item.content.tweet) {
1665 | newContent.item.content.conversationThread.conversationComponents.push(
1666 | {
1667 | conversationTweetComponent: {
1668 | tweet: item.item.content.tweet,
1669 | },
1670 | }
1671 | );
1672 | }
1673 | }
1674 | entry.content = newContent;
1675 | }
1676 | }
1677 | }
1678 |
1679 | return data;
1680 | },
1681 | },
1682 | // TweetDeck state
1683 | {
1684 | path: "/1.1/tweetdeck/clients/blackbird/all",
1685 | method: "GET",
1686 | beforeRequest: (xhr) => {
1687 | xhr.modUrl = `https://api.${location.hostname}/1.1/help/settings.json?meow`;
1688 | },
1689 | afterRequest: (xhr) => {
1690 | const state = {
1691 | client: {
1692 | columns: localStorage.OTDcolumnIds ? JSON.parse(localStorage.OTDcolumnIds) : [],
1693 | mtime: new Date().toISOString(),
1694 | name: "blackbird",
1695 | settings: settings ?? {
1696 | account_whitelist: [`twitter:${verifiedUser.id_str}`],
1697 | default_account: `twitter:${verifiedUser.id_str}`,
1698 | recent_searches: [],
1699 | display_sensitive_media: false,
1700 | name_cache: {
1701 | customTimelines: {},
1702 | lists: {},
1703 | users: {}
1704 | },
1705 | navbar_width: "full-size",
1706 | previous_splash_version: "4.0.220811153004",
1707 | show_search_filter_callout: false,
1708 | show_trends_filter_callout: false,
1709 | theme: "light",
1710 | use_narrow_columns: null,
1711 | version: 2
1712 | },
1713 | },
1714 | columns: columns ?? {},
1715 | decider: {},
1716 | feeds: feeds ?? {},
1717 | messages: [],
1718 | new: true
1719 | };
1720 | if(!settings) {
1721 | settings = state.client.settings;
1722 | localStorage.OTDsettings = JSON.stringify(settings);
1723 | }
1724 | cleanUp();
1725 | console.log('state', state);
1726 |
1727 | return state;
1728 | },
1729 | },
1730 | // emulate sending state data
1731 | {
1732 | path: "/1.1/tweetdeck/clients/blackbird",
1733 | method: "POST",
1734 | responseHeaderOverride: {
1735 | "x-td-mtime": () => {
1736 | return new Date().toISOString();
1737 | },
1738 | },
1739 | beforeRequest: (xhr) => {
1740 | xhr.modUrl = `https://api.${location.hostname}/1.1/help/settings.json?meow_push`;
1741 | xhr.modMethod = "GET";
1742 | },
1743 | beforeSendBody: (xhr, body) => {
1744 | let json = JSON.parse(body);
1745 | console.log('state push', json);
1746 | if(json.columns) {
1747 | localStorage.OTDcolumnIds = JSON.stringify(json.columns);
1748 | }
1749 | if(json.settings && settings) {
1750 | for(let key in json.settings) {
1751 | settings[key] = json.settings[key];
1752 | }
1753 | localStorage.OTDsettings = JSON.stringify(settings);
1754 | }
1755 | cleanUp();
1756 | return body;
1757 | },
1758 | afterRequest: (xhr) => {
1759 | return "";
1760 | }
1761 | },
1762 | // emulate sending feeds
1763 | {
1764 | path: "/1.1/tweetdeck/feeds",
1765 | method: "POST",
1766 | responseHeaderOverride: {
1767 | "X-Td-Mtime": () => {
1768 | return new Date().toISOString();
1769 | },
1770 | },
1771 | beforeRequest: (xhr) => {
1772 | xhr.modUrl = `https://api.${location.hostname}/1.1/help/settings.json?meow_feeds_push`;
1773 | xhr.modMethod = "GET";
1774 | },
1775 | beforeSendBody: (xhr, body) => {
1776 | let json = JSON.parse(body);
1777 | let ids = [];
1778 | for(let i = 0; i < json.length; i++) {
1779 | const id = json[i].id ?? generateID();
1780 | ids.push(id);
1781 | feeds[id] = json[i];
1782 | }
1783 | xhr.storage.ids = ids;
1784 | localStorage.OTDfeeds = JSON.stringify(feeds);
1785 | console.log('feeds push', json, ids);
1786 | return body;
1787 | },
1788 | afterRequest: (xhr) => {
1789 | return xhr.storage.ids;
1790 | }
1791 | },
1792 | // emulate sending columns
1793 | {
1794 | path: "/1.1/tweetdeck/columns",
1795 | method: "POST",
1796 | responseHeaderOverride: {
1797 | "X-Td-Mtime": () => {
1798 | return new Date().toISOString();
1799 | },
1800 | },
1801 | beforeRequest: (xhr) => {
1802 | xhr.modUrl = `https://api.${location.hostname}/1.1/help/settings.json?meow_columns_push`;
1803 | xhr.modMethod = "GET";
1804 | },
1805 | beforeSendBody: (xhr, body) => {
1806 | let json = JSON.parse(body);
1807 | let ids = [];
1808 | for(let i = 0; i < json.length; i++) {
1809 | const id = json[i].id ?? generateID();
1810 | ids.push(id);
1811 | columns[id] = json[i];
1812 | }
1813 | xhr.storage.ids = ids;
1814 | localStorage.OTDcolumns = JSON.stringify(columns);
1815 | console.log('columns push', json, ids);
1816 | return body;
1817 | },
1818 | afterRequest: (xhr) => {
1819 | return xhr.storage.ids;
1820 | }
1821 | },
1822 | // getting user
1823 | {
1824 | path: "/1.1/account/verify_credentials.json",
1825 | method: "GET",
1826 | beforeSendHeaders: (xhr) => {
1827 | xhr.modReqHeaders["Content-Type"] = "application/json";
1828 | xhr.modReqHeaders["X-Twitter-Active-User"] = "yes";
1829 | xhr.modReqHeaders["X-Twitter-Client-Language"] = "en";
1830 | xhr.modReqHeaders["Authorization"] = PUBLIC_TOKENS[1];
1831 | delete xhr.modReqHeaders["X-Twitter-Client-Version"];
1832 | },
1833 | afterRequest: (xhr) => {
1834 | try {
1835 | let data = JSON.parse(xhr.responseText);
1836 | verifiedUser = data;
1837 | localStorage.OTDverifiedUser = JSON.stringify(data);
1838 | } catch (e) {
1839 | console.error(e);
1840 | }
1841 | return xhr.responseText;
1842 | }
1843 | },
1844 | // DM messages
1845 | {
1846 | path: /\/1.1\/dm\/conversation\/(\d+)-(\d+).json/,
1847 | method: "GET",
1848 | afterRequest: (xhr) => {
1849 | return xhr.responseText.replaceAll("\\/\\/ton.twitter.com\\/1.1", "\\/\\/ton.x.com\\/i");
1850 | }
1851 | },
1852 | // Inbox
1853 | {
1854 | path: "/1.1/dm/user_updates.json",
1855 | method: "GET",
1856 | afterRequest: (xhr) => {
1857 | return xhr.responseText.replaceAll("\\/\\/ton.twitter.com\\/1.1", "\\/\\/ton.x.com\\/i");
1858 | }
1859 | },
1860 | // Translating tweets
1861 | {
1862 | path: "/1.1/translations/show.json",
1863 | method: "GET",
1864 | beforeRequest: (xhr) => {
1865 | let url = new URL(xhr.modUrl);
1866 | let params = new URLSearchParams(url.search);
1867 | let tweet_id = params.get("id");
1868 | let dest = params.get("dest");
1869 | xhr.modUrl = `https://${location.hostname}/i/api/1.1/strato/column/None/tweetId=${tweet_id},destinationLanguage=None,translationSource=Some(Google),feature=None,timeout=None,onlyCached=None/translation/service/translateTweet`;
1870 | },
1871 | beforeSendHeaders: (xhr) => {
1872 | xhr.modReqHeaders["Content-Type"] = "application/json";
1873 | xhr.modReqHeaders["X-Twitter-Active-User"] = "yes";
1874 | xhr.modReqHeaders["X-Twitter-Client-Language"] = navigator.language.split("-")[0];
1875 | xhr.modReqHeaders["Authorization"] =
1876 | PUBLIC_TOKENS[1];
1877 | delete xhr.modReqHeaders["X-Twitter-Client-Version"];
1878 | },
1879 | afterRequest: (xhr) => {
1880 | const response = JSON.parse(xhr.responseText);
1881 |
1882 | return JSON.stringify({
1883 | text: response.translation,
1884 | entities: response.entities,
1885 | translated_lang: response.sourceLanguage,
1886 | });
1887 | }
1888 | },
1889 | ];
1890 |
1891 | // wrap the XMLHttpRequest
1892 | XMLHttpRequest = function () {
1893 | return new Proxy(new OriginalXHR(), {
1894 | open(method, url, async, username = null, password = null) {
1895 | this.modMethod = method;
1896 | this.modUrl = url;
1897 | this.originalUrl = url;
1898 | this.modReqHeaders = {};
1899 | this.storage = {};
1900 |
1901 | try {
1902 | let parsedUrl = new URL(url);
1903 | this.proxyRoute = proxyRoutes.find((route) => {
1904 | if (route.method.toUpperCase() !== method.toUpperCase()) return false;
1905 | if (typeof route.path === "string") {
1906 | return route.path === parsedUrl.pathname;
1907 | } else if (route.path instanceof RegExp) {
1908 | return route.path.test(parsedUrl.pathname);
1909 | }
1910 | });
1911 | } catch (e) {
1912 | console.error(e);
1913 | }
1914 | if (this.proxyRoute && this.proxyRoute.beforeRequest) {
1915 | this.proxyRoute.beforeRequest(this);
1916 | }
1917 |
1918 | // both handlers must be set, because if openHandler never opens the request 'send' will always error
1919 | if(this.proxyRoute && this.proxyRoute.openHandler && this.proxyRoute.sendHandler) {
1920 | this.proxyRoute.openHandler(this, this.modMethod, this.modUrl, async, username, password);
1921 | } else {
1922 | this.open(this.modMethod, this.modUrl, async, username, password);
1923 | }
1924 | },
1925 | setRequestHeader(name, value) {
1926 | this.modReqHeaders[name] = value;
1927 | },
1928 | async send(body = null) {
1929 | let parsedUrl = new URL(this.modUrl);
1930 | let method = this.modMethod;
1931 | if(!method) {
1932 | method = "GET";
1933 | } else {
1934 | method = method.toUpperCase();
1935 | }
1936 | if(
1937 | this.readyState === 1 &&
1938 | (
1939 | this.modUrl.includes("api.twitter.com") ||
1940 | this.modUrl.includes("api.x.com") ||
1941 | this.modUrl.includes("twitter.com/i/api") ||
1942 | this.modUrl.includes("x.com/i/api")
1943 | )
1944 | ) {
1945 | if(localStorage.device_id) this.setRequestHeader('X-Client-UUID', localStorage.device_id);
1946 | if(window.solveChallenge) {
1947 | try {
1948 | this.setRequestHeader('x-client-transaction-id', await solveChallenge(parsedUrl.pathname, method));
1949 | } catch (e) {
1950 | if(localStorage.secureRequests && Date.now() - OTD_INIT_TIME > 3000) {
1951 | throw e;
1952 | }
1953 | }
1954 | }
1955 | }
1956 | if (this.proxyRoute && this.proxyRoute.beforeSendHeaders) {
1957 | this.proxyRoute.beforeSendHeaders(this);
1958 | }
1959 | try {
1960 | for (const [name, value] of Object.entries(this.modReqHeaders)) {
1961 | this.setRequestHeader(name, value);
1962 | }
1963 | } catch(e) {
1964 | if(!String(e).includes('OPENED')) {
1965 | console.error(e);
1966 | }
1967 | }
1968 | if (this.proxyRoute && this.proxyRoute.beforeSendBody) {
1969 | body = this.proxyRoute.beforeSendBody(this, body);
1970 | }
1971 | if(this.proxyRoute && this.proxyRoute.sendHandler) {
1972 | this.proxyRoute.sendHandler(this, body);
1973 | } else {
1974 | this.send(body);
1975 | }
1976 | },
1977 | get(xhr, key) {
1978 | if (!key in xhr) return undefined;
1979 | if (key === "responseText" && xhr._responseText) return xhr._responseText;
1980 | if (key === "responseText") return this.interceptResponseText(xhr);
1981 | if (key === "readyState" && xhr._readyState) return xhr._readyState;
1982 | if (key === "status" && xhr._status) return xhr._status;
1983 | if (key === "statusText" && xhr._statusText) return xhr._statusText;
1984 |
1985 | let value = xhr[key];
1986 | if (typeof value === "function") {
1987 | value = this[key] || value;
1988 | return (...args) => value.apply(xhr, args);
1989 | } else {
1990 | return value;
1991 | }
1992 | },
1993 | set(xhr, key, value) {
1994 | if (key in xhr) {
1995 | xhr[key] = value;
1996 | }
1997 | return value;
1998 | },
1999 | interceptResponseText(xhr) {
2000 | if (xhr.proxyRoute && xhr.proxyRoute.afterRequest) {
2001 | let out = xhr.proxyRoute.afterRequest(xhr);
2002 | if (typeof out === "object") {
2003 | return JSON.stringify(out);
2004 | } else {
2005 | return out;
2006 | }
2007 | }
2008 | return xhr.responseText;
2009 | },
2010 | getAllResponseHeaders() {
2011 | let headers = this.getAllResponseHeaders();
2012 |
2013 | let override = this.responseHeaderOverride ? this.responseHeaderOverride : this.proxyRoute ? this.proxyRoute.responseHeaderOverride : undefined;
2014 | if (this.proxyRoute && override) {
2015 | let splitHeaders = headers.split("\r\n");
2016 | let objHeaders = {};
2017 | for (let header of splitHeaders) {
2018 | let splitHeader = header.split(": ");
2019 | let headerName = splitHeader[0];
2020 | let headerValue = splitHeader[1];
2021 | objHeaders[headerName.toLowerCase()] = headerValue;
2022 | }
2023 | for(let header in override) {
2024 | objHeaders[header.toLowerCase()] = override[header]();
2025 | }
2026 | headers = Object.entries(objHeaders).filter(([_, value]) => value).map(([name, value]) => `${name}: ${value}`).join("\r\n");
2027 | }
2028 |
2029 | return headers;
2030 | },
2031 | });
2032 | };
2033 |
--------------------------------------------------------------------------------
/src/notifications.js:
--------------------------------------------------------------------------------
1 | function createModal(html, className, onclose, canclose) {
2 | let modal = document.createElement('div');
3 | modal.classList.add('otd-modal');
4 | let modal_content = document.createElement('div');
5 | modal_content.classList.add('otd-modal-content');
6 | if(className) modal_content.classList.add(className);
7 | modal_content.innerHTML = html;
8 | modal.appendChild(modal_content);
9 | let close = document.createElement('span');
10 | close.classList.add('otd-modal-close');
11 | close.title = "ESC";
12 | close.innerHTML = '×';
13 | document.body.style.overflowY = 'hidden';
14 | function removeModal() {
15 | modal.remove();
16 | let event = new Event('findActiveTweet');
17 | document.dispatchEvent(event);
18 | document.removeEventListener('keydown', escapeEvent);
19 | if(onclose) onclose();
20 | let modals = document.getElementsByClassName('modal');
21 | if(modals.length === 0) {
22 | document.body.style.overflowY = 'auto';
23 | }
24 | }
25 | modal.removeModal = removeModal;
26 | function escapeEvent(e) {
27 | if(e.key === 'Escape' || (e.altKey && e.keyCode === 78)) {
28 | if(!canclose || canclose()) removeModal();
29 | }
30 | }
31 | close.addEventListener('click', removeModal);
32 | modal.addEventListener('click', e => {
33 | if(e.target === modal) {
34 | if(!canclose || canclose()) removeModal();
35 | }
36 | });
37 | document.addEventListener('keydown', escapeEvent);
38 | modal_content.appendChild(close);
39 | document.body.appendChild(modal);
40 | return modal;
41 | }
42 |
43 | async function getNotifications() {
44 | let notifs = await fetch('https://tweetdeck.dimden.dev/notifications.json?t='+Date.now()).then(r => r.json());
45 | let readNotifs = localStorage.getItem('readNotifications') ? JSON.parse(localStorage.getItem('readNotifications')) : [];
46 | let notifsToDisplay = notifs.filter(notif => !readNotifs.includes(notif.id));
47 |
48 | return notifsToDisplay;
49 | }
50 | function maxVersionCheck(ver, maxVer) {
51 | let verArr = ver.split('.');
52 | let maxVerArr = maxVer.split('.');
53 | for(let i = 0; i < verArr.length; i++) {
54 | if(parseInt(verArr[i]) > parseInt(maxVerArr[i])) return false;
55 | if(parseInt(verArr[i]) < parseInt(maxVerArr[i])) return true;
56 | }
57 | return true;
58 | }
59 | function minVersionCheck(ver, minVer) {
60 | let verArr = ver.split('.');
61 | let minVerArr = minVer.split('.');
62 | for(let i = 0; i < verArr.length; i++) {
63 | if(parseInt(verArr[i]) < parseInt(minVerArr[i])) return false;
64 | if(parseInt(verArr[i]) > parseInt(minVerArr[i])) return true;
65 | }
66 | return true;
67 | }
68 |
69 | async function showNotifications() {
70 | let notifsToDisplay = await getNotifications();
71 | if(notifsToDisplay.length === 0) return;
72 | let manifest = await fetch(chrome.runtime.getURL('/manifest.json')).then(r => r.json());
73 | let currentVersion = manifest.version;
74 |
75 | for(let notif of notifsToDisplay) {
76 | if(!localStorage.OTDnotifsReadOnce && notif.ignoreOnInstall) {
77 | let readNotifs = localStorage.getItem('readNotifications') ? JSON.parse(localStorage.getItem('readNotifications')) : [];
78 | if(readNotifs.includes(notif.id)) continue;
79 | readNotifs.push(notif.id);
80 | localStorage.setItem('readNotifications', JSON.stringify(readNotifs));
81 | continue;
82 | }
83 | if(notif.maxVersion && !maxVersionCheck(currentVersion, notif.maxVersion)) continue;
84 | if(notif.minVersion && !minVersionCheck(currentVersion, notif.minVersion)) continue;
85 | if(document.querySelector('.otd-notification-modal')) continue;
86 | let notifHTML = ``;
87 | let shown = Date.now();
88 | createModal(notifHTML, 'otd-notification-modal', () => {
89 | if(!notif.dismissable) return;
90 | let readNotifs = localStorage.getItem('readNotifications') ? JSON.parse(localStorage.getItem('readNotifications')) : [];
91 | if(readNotifs.includes(notif.id)) return;
92 | readNotifs.push(notif.id);
93 | localStorage.setItem('readNotifications', JSON.stringify(readNotifs));
94 | }, () => Date.now() - shown > 3000);
95 | }
96 | localStorage.OTDnotifsReadOnce = '1';
97 | }
98 |
99 | let style = document.createElement('style');
100 | style.innerHTML = /*css*/`
101 | .otd-modal {
102 | position: fixed;
103 | z-index: 200;
104 | left: 0;
105 | top: 0;
106 | width: 100%;
107 | height: 100%;
108 | overflow: auto;
109 | background-color: rgb(0, 0, 0);
110 | background-color: rgba(0, 0, 0, 0.4);
111 | }
112 |
113 | .otd-modal-content {
114 | width: fit-content;
115 | min-width: 500px;
116 | margin: auto;
117 | border-radius: 5px;
118 | padding: 20px;
119 | background-color: white;
120 | color: black;
121 | top: 20%;
122 | position: relative;
123 | max-height: 60%;
124 | overflow-y: inherit;
125 | animation: opac 0.2s ease-in-out;
126 | }
127 | html.dark .otd-modal-content {
128 | background-color: #15202b;
129 | color: white;
130 | }
131 | .otd-notification-warning > .otd-notification-content::before {
132 | content: "⚠️";
133 | margin-right: 5px;
134 | }
135 | .otd-notification-error > .otd-notification-content::before {
136 | content: "❌";
137 | margin-right: 5px;
138 | }
139 | .otd-notification-info > .otd-notification-content::before {
140 | content: "ℹ️";
141 | margin-right: 5px;
142 | }
143 | @keyframes opac {
144 | 0% {
145 | opacity: 0
146 | }
147 | 100% {
148 | opacity: 1
149 | }
150 | }
151 |
152 | .otd-modal-close {
153 | color: #aaaaaa;
154 | float: right;
155 | font-size: 20px;
156 | font-weight: bold;
157 | top: 0;
158 | right: 5px;
159 | position: absolute;
160 | }
161 |
162 | .otd-modal-close:hover,
163 | .otd-modal-close:focus {
164 | color: black;
165 | text-decoration: none;
166 | cursor: pointer;
167 | }
168 | `;
169 |
170 | setTimeout(() => {
171 | document.head.appendChild(style);
172 | }, 1000);
173 | setTimeout(showNotifications, 2000);
174 | setInterval(showNotifications, 60000 * 60);
--------------------------------------------------------------------------------