Showing preview only (2,176K chars total). Download the full file or copy to clipboard to get everything.
Repository: BANKA2017/twitter-monitor
Branch: node
Commit: 4ddb93122c60
Files: 87
Total size: 2.1 MB
Directory structure:
gitextract_drmlutwn/
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── .yarnrc.yml
├── LICENSE
├── README.MD
├── apps/
│ ├── backend/
│ │ ├── CoreFunctions/
│ │ │ ├── album/
│ │ │ │ └── Album.mjs
│ │ │ ├── online/
│ │ │ │ ├── OnlineLogin.mjs
│ │ │ │ ├── OnlineMisc.mjs
│ │ │ │ ├── OnlineTrends.mjs
│ │ │ │ ├── OnlineTweet.mjs
│ │ │ │ └── OnlineUserInfo.mjs
│ │ │ └── translate/
│ │ │ ├── OnlineTranslate.mjs
│ │ │ └── Translate.mjs
│ │ ├── app.mjs
│ │ ├── service/
│ │ │ ├── album.mjs
│ │ │ ├── online.mjs
│ │ │ └── translate.mjs
│ │ ├── share.mjs
│ │ └── static/
│ │ ├── .gitkeep
│ │ └── xml/
│ │ └── rss.xsl
│ ├── online_tools/
│ │ ├── config.html
│ │ ├── oauth_signature_builder.html
│ │ ├── snowflake.html
│ │ ├── webpush.html
│ │ └── x_client_transaction_id.html
│ ├── open_account/
│ │ ├── readme.md
│ │ └── scripts/
│ │ ├── get_guest_token.js
│ │ ├── get_open_account_info.mjs
│ │ ├── login.mjs
│ │ └── proxy.txt
│ ├── rate_limit_checker/
│ │ ├── data/
│ │ │ └── .gitkeep
│ │ ├── readme.md
│ │ └── run.mjs
│ ├── scripts/
│ │ ├── apiPathGenerator.mjs
│ │ ├── loginflow.js
│ │ ├── updateAndroidQueryIdList.mjs
│ │ └── updateQueryIdList.mjs
│ └── web_push/
│ ├── callback.mjs
│ ├── config.mjs
│ ├── config_example.json
│ ├── decrypt.mjs
│ ├── package.json
│ ├── readme.md
│ ├── twitter.mjs
│ ├── utils.mjs
│ ├── web_push.mjs
│ └── websocket.mjs
├── libs/
│ ├── README.md
│ ├── assets/
│ │ ├── config_sample.json
│ │ ├── graphql/
│ │ │ ├── androidQueryIdList.js
│ │ │ ├── featuresValueList.js
│ │ │ ├── featuresValueList.json
│ │ │ ├── graphqlQueryIdList.js
│ │ │ └── graphqlQueryIdList.json
│ │ └── setting_sample.mjs
│ ├── core/
│ │ ├── Core.Rss.mjs
│ │ ├── Core.android.mjs
│ │ ├── Core.apiPath.mjs
│ │ ├── Core.blurhash.mjs
│ │ ├── Core.fetch.mjs
│ │ ├── Core.function.mjs
│ │ ├── Core.info.mjs
│ │ ├── Core.push.mjs
│ │ ├── Core.translate.mjs
│ │ ├── Core.tweet.mjs
│ │ └── Core.xClientTransactionID.mjs
│ └── share/
│ ├── Constant.mjs
│ ├── Mime.mjs
│ ├── MockFuntions.mjs
│ └── NodeConstant.mjs
├── package.json
├── packages/
│ ├── axios-helper/
│ │ ├── README.md
│ │ ├── index.js
│ │ ├── index.node.js
│ │ └── package.json
│ ├── crypto-helper/
│ │ ├── index.js
│ │ ├── index.node.js
│ │ └── package.json
│ └── get-mime/
│ ├── index.js
│ └── package.json
├── tests/
│ ├── backend.online.test.js
│ ├── core.fetch.android.test.js
│ ├── core.fetch.anonymous.test.js
│ ├── mock/
│ │ └── express.js
│ └── mock.express.test.js
└── vitest.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
apps/crawler/savetweets/*
apps/scripts/t.mjs
apps/scripts/save_spaces/
apps/scripts/astParser/
apps/backend/cache/*
apps/backend/static/aac/
apps/backend/static/bangdream_trends/
apps/backend/static/lovelive_trends/
apps/backend/static/trends/
apps/backend/static/trends2/
apps/open_account/scripts/openAccount.json
!apps/backend/cache/.gitkeep
apps/archiver/*
!apps/archiver/archive.mjs
!apps/archiver/archive_lite.mjs
!apps/archiver/init.ps1
!apps/archiver/init.sh
!apps/archiver/README.md
!apps/archiver/retryMedia.mjs
apps/web_push/config.json
apps/web_push/tweets.json
libs/assets/setting.mjs
libs/assets/config.json
libs/assets/sagm_config.cjs
libs/assets/analytics/account.json
apps/cfworkers/dist/
!apps/crawler/savetweets/.gitkeep
packages/get-mime/*
!packages/get-mime/index.js
!packages/get-mime/package.json
.DS_Store
node_modules
.yarn
/dist
public/test/
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
stats.html
================================================
FILE: .prettierignore
================================================
apps/crawler/savetweets/*
apps/scripts/t.mjs
apps/scripts/save_spaces/
apps/scripts/astParser/
apps/backend/cache/*
apps/backend/static/
!apps/backend/cache/.gitkeep
apps/archiver/*
!apps/archiver/archive.mjs
!apps/archiver/archive_lite.mjs
!apps/archiver/init.ps1
!apps/archiver/init.sh
!apps/archiver/README.md
!apps/archiver/retryMedia.mjs
libs/assets/setting.mjs
libs/assets/config.json
libs/assets/sagm_config.cjs
libs/assets/analytics/account.json
libs/assets/graphql/
libs/assets/sqlite/
libs/core/Core.apiPath.mjs
apps/cfworkers/dist/
!apps/crawler/savetweets/.gitkeep
libs/model/
.DS_Store
node_modules
.yarn
yarn.lock
/dist
public/test/
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.md
*.html
================================================
FILE: .prettierrc.json
================================================
{
"useTabs": false,
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 250,
"semi": false
}
================================================
FILE: .yarnrc.yml
================================================
compressionLevel: mixed
enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.5.0.cjs
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2022-present BANKA2017
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.
================================================
FILE: README.MD
================================================
# Twitter Monitor v3 monorepo (RE?)
---
## ⚠ WARNING / 警告
March 2026 Update: A large amount of content was removed. This part of the solution was already outdated and irreparable, making further maintenance unnecessary. The following are the affected projects, which may be missing:
- Removed all early crawlers and statistics (data from LoveLive!/BanGDream!, etc.)
- Removed or hid **tmv1/tmv3/media proxy/static file and image caching** related APIs
- The existing API's media proxy uses the currently unopened TM@GO. Future code will be available at [twitter-monitor/go](https://github.com/BANKA2017/twitter-monitor/tree/go)
- If you need previous data, please contact me directly.
- Removed all cloudflare@workers related content.
- Removed some globally installable CLI tools such as prettier/vitest/nodemon...
- Removed all other unnecessary content.
If you need unaffected repositories, please check [Commit 0a47432](https://github.com/BANKA2017/twitter-monitor/commit/0a4743247f5bb5e923cb217c4c8d8e62c7f233b7) and previous commits.
2026-03 更新移除了大量内容,这部分方案早已失效,并且无法修复,已经失去继续维护的必要,下面是受影响的项目,可能会有遗漏:
- 移除所有早期爬虫和统计数据(来自 LoveLive!/BanGDream! 等成员的数据)
- 移除或隐藏 **tmv1/tmv3/媒体代理/静态文件和图片缓存** 相关 API
- 现有 API 的媒体代理使用暂未开源的 TM@GO,未来相关代码会放在 [twitter-monitor/go](https://github.com/BANKA2017/twitter-monitor/tree/go)
- 如果需要以前的数据,请直接联系我
- 移除所有 cloudflare@workers 相关的内容
- 移除了一些可以全局安装的 cli 工具 prettier/vitest/nodemon...
- 移除所有其他不必要的内容
如果需要未受影响的仓库,请检查 [Commit 0a47432](https://github.com/BANKA2017/twitter-monitor/commit/0a4743247f5bb5e923cb217c4c8d8e62c7f233b7) 及以前的 commit
---
This repository included `core/api/scripts`, frontend repository is [here](https://github.com/BANKA2017/twitter-monitor-frontend/).
这个仓库包含 `核心/api/脚本`,前端仓库位于[这里](https://github.com/BANKA2017/twitter-monitor-frontend/)
## How to
### Settings
* leave it as is if not necessary
* service **tmv1** and **analytics** in `SQL_CONFIG` are not necessary, execpt you used twitter monitor before 2020-03
### Core
* **NO TYPESCRIPT**, core code are not yet supported typescript
* copy and rename the setting file from `libs/assets/settings_sample.mjs` to `libs/assets/settings.mjs`, and edit it
* enjoy it!
### Graphql
**IF POSSIBLE, DO NOT EXECUTE THOSE SCRIPTS**
* execute `node apps/scripts/updateQueryIdList.mjs` to update `queryId`
* execute `node apps/scripts/checkGraphqlFeaturesStatus.mjs` for check after updating `queryId`
### Crawler
* edit `SQL_CONFIG` to enable service **twitter_monitor**
* execute `yarn run init` or `npm run init`
* open `config.html` by browser to edit and save config file `config.json` as `libs/assets/config.json`
[Online editor](https://banka2017.github.io/twitter-monitor/apps/online_tools/config.html)
* [PM2](https://pm2.keymetrics.io/) is a good choice for you, you also can use screen or nohup
```shell
pm2 start apps/crawler/get_tweets.mjs
```
* set crontab,e.g.
```shell
#those are example path, use your path
* * * * * node apps/crawler/blurhash.mjs #PHP version is better, node version is slow
*/10 * * * * node apps/crawler/updatePollsAndAudioSpace.mjs
```
* you can set `TWEETS_SAVE_PATH` to save tweet content as json in that path
* ~~we use another **bearer authorization token** to crawl more tweets, but this token is not supported some new feature like mix media, you can replace it by the latest token (then not support **nsfw** content)~~
* Account pools are not yet supported. In the future, you will need to prepare a large number of accounts to build account pools.
### Api
* execute `yarn run dbapi` to enable the api only used databases (tmv1, twitter monitor)
* execute `yarn run api` to enable full version api (dbapi and album api, online api, media proxy)
* You need to prepare a large number of accounts to build an account pool. To learn more about account pool, please read [open_account/README.md](https://github.com/BANKA2017/twitter-monitor/tree/node/apps/open_account). Then copy the `guest_accounts.json` created by the script to the project root directory/the same directory as the `app.mjs`/`app_online.mjs`, or paste its content into the variable `GUEST_ACCOUNTS` of `libs/assets/setting.mjs`
* *you can create a better api than mine
### Watch dog
* a script in `apps/scripts` named `watchDog.mjs` can be added to crontab to check whether some data being added in latest 30 minites
### Archiver
* archive userinfo, most tweets(included **reply**) and nearly **ALL MEDIA**(included avatar and banner) by search api / timeline api, `Following` and `Followers`
* spaces, boradcast (ffmpeg command)
* **PLEASE PRECHECK THE ACCOUNT HAVEN BEEN SEARCHBAN**
* Read more in [archiver/README.md](https://github.com/BANKA2017/twitter-monitor/tree/node/apps/archiver)
### CloudFlare Workers
* supported album api, online api, media proxy and translator api
* You need to prepare a large number of accounts to build an account pool. To learn more about account pool, please read [open_account/README.md](https://github.com/BANKA2017/twitter-monitor/tree/node/apps/open_account). Then save the content as `guest_accounts` to kv
* to deploy it is easy
* *read <https://developers.cloudflare.com/workers/wrangler/workers-kv/#create-a-kv-namespace-with-wrangler>
* create kv space named 'twitter-monitor-workers-kv' in <https://dash.cloudflare.com/> or executed `npx wrangler kv:namespace create kv`
* copy the value of 'id' into 'kv_namespaces[0].id'
* execute `npx wrangler publish`
### Rate limit checker
* set env `android_guest_account` likes
```javascript
{
"authorization": "Bearer AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAAMHCxpeSDG1gLNLghVe8d74hl6k4%3DRUMF4xAQLsbeBhTSRrCiQpJtxoGWeyHrDb5te2jpGskWDFW82F",
"oauth_token": "...",
"oauth_token_secret": "..."
}
```
* then execute `node apps/rate_limit_checker/run.mjs`
* Read more in [rate-limit-checker/README.md](https://github.com/BANKA2017/twitter-monitor/tree/node/apps/rate_limit_checker)
### OAuth open account pool
* Read more in [open_account/README.md](https://github.com/BANKA2017/twitter-monitor/tree/node/apps/open_account)
### ↑~ Web*Push X!↓
* Read more in [web_push/README.md](https://github.com/BANKA2017/twitter-monitor/tree/node/apps/web_push)
## Supported Cards
* summary*
* summary_large_image
* promo_image_convo
* promo_video_convo
* promo_website
* audio*
* player
* periscope_broadcast
* broadcast
* promo_video_website
* promo_image_app
* app
* direct_store_link_app
* live_event
* moment**
* poll2choice_text_only
* poll3choice_text_only
* poll4choice_text_only
* poll2choice_image
* poll3choice_image
* poll4choice_image
* appplayer
* audiospace
* unified_card
* image_website
* video_website
* image_carousel_website
* video_carousel_website
* image_app
* video_app
* image_carousel_app
* video_carousel_app
* image_multi_dest_carousel_website
* video_multi_dest_carousel_website
* mixed_media_multi_dest_carousel_website
* mixed_media_single_dest_carousel_website
* mixed_media_single_dest_carousel_app
* image_collection_website
* twitter_list_details
* media_with_details_horizontal (for topic ?)
* twitter_article
* community_details
* grok_share
## Sub packages
* `AxiosHelper` used to fix the issue that CloudFlare Workers unable to use axios, [Read more](https://github.com/BANKA2017/twitter-monitor/tree/node/packages/axios-helper)
## Thanks
* `GoogleTokenGenerator.php` from [google-translate-php](https://github.com/Stichoza/google-translate-php), I remove some unnecessary code //now we need't use tk in Google translate
* function `get_mime` is rewrited from [Tieba-Cloud-Sign](https://github.com/MoeNetwork/Tieba-Cloud-Sign/blob/c4ab393045bcabde97c1a70fbe8e8d56be8f7f1e/lib/sfc.functions.php#L790)
* you may notice a package named `sequelize-automate-gm` in `package.json`, this is a tiny tool to help me create model from exist tables
## How it works
- [怎么爬twitter(zh-Hans)](https://blog.nest.moe/posts/how-to-crawl-twitter/)
- [怎么爬twitter Graphql(zh-Hans)](https://blog.nest.moe/posts/how-to-crawl-twitter-with-graphql/)
- [怎么爬 Twitter(Android)(zh-Hans)](https://blog.nest.moe/posts/how-to-crawl-twitter-with-android/)
## 环境要求
* x86_64/arm64 (理论上是全平台支持的)
* Node.js 18+
* Nginx
* MySQL 8.0 (使用MariaDB等基于 MySQL 5.x 的发行版可能不能支持函数 `ANY_VALUE()`, 解析器 `ngram`,会导致部分api不可用,若不使用api可忽略此处) / SQLite
## 使用方法
### 设定
* 如无必要,保持原样
* `SQL_CONFIG` 中的 **tmv1** 以及 **analytics** 的服务都不是必须的,除非您在 2022-03 重写前就使用了 twitter monitor
### Core
* **没有 Typescript**,其实是不知怎么入手
* 拷贝编辑 `libs/assets/settings_sample.mjs` 并另存为 `libs/assets/settings.mjs`
* 开始使用吧!
### Graphql
**如无必要,请不要跑这些脚本**
* 运行 `node apps/scripts/updateQueryIdList.mjs` 以更新 `queryId`
* 更新 `queryId` 后运行 `node apps/scripts/checkGraphqlFeaturesStatus.mjs` 进行检查(不通过怎么办?可以来提issue)
### 爬虫
* 编辑 `SQL_CONFIG` 以启用本服务
* 运行 `yarn run init` 或 `npm run init` 以导入数据表以及在路径 `libs/assets/config.json` 创建配置文件
[在线编辑器](https://banka2017.github.io/twitter-monitor/apps/online_tools/config.html)
* 我推荐使用 [PM2](https://pm2.keymetrics.io/),但 nohup 和 screen 也不是不行
```shell
pm2 start apps/crawler/get_tweets.mjs
```
* 编辑crontab
```shell
#这些都是示例,用你自己的路径
* * * * * node apps/crawler/blurhash.mjs #PHP版更好
*/10 * * * * node apps/crawler/updatePollsAndAudioSpace.mjs
```
* 设定 `TWEETS_SAVE_PATH` 可以将推文内容保存为json文件
* ~~我们使用旧版**bearer authorization token**以获得更多的推文,但这个token并不支持一些新版的特性,比如混合媒体,你可以用新的token替换之(然后不支持 **NSFW** 内容)~~
* 目前还不支持帐号池,未来需要自行准备大量帐号构建帐号池
### Api
* 运行 `yarn run dbapi` 启用仅使用数据库的api (tmv1,twitter monitor)
* 运行 `yarn run api` 启用全部 api (上一项的全部以及 album api,online api,media proxy)
* 需要自行准备大量帐号构建帐号池,关于帐号池请查看 [open_account/README.md](https://github.com/BANKA2017/twitter-monitor/tree/node/apps/open_account)。然后将脚本创建的 `guest_accounts.json` 拷贝到 项目根目录/执行脚本相同目录,或者将其内容粘贴到 `libs/assets/setting.mjs` 的变量 `GUEST_ACCOUNTS` 中
* *欢迎自行开发一个更好的api
### 看门狗
* `apps/scripts` 里面有一个叫 `watchDog.mjs` 的脚本,可以添加到 crontab 中用以检查半小时内是否有数据增加
### Archiver
* 通过**搜索API**或**时间线API**备份帐号的用户信息,大多数推文(包括回复)和媒体文件(包括当前头像和banner),`Following` 和 `Followers`
* 备份Spaces、播客(生成 ffmpeg 命令)
* **使用前请检查待备份帐号是否被搜索封禁**
* 使用方式请阅读 [archiver/README.md](https://github.com/BANKA2017/twitter-monitor/tree/node/apps/archiver)
### CloudFlare Workers
* 支持 album api, online api, media proxy 以及 翻译 api
* 需要自行准备大量帐号构建帐号池,关于帐号池请查看 [open_account/README.md](https://github.com/BANKA2017/twitter-monitor/tree/node/apps/open_account)。然后将脚本创建的 `guest_accounts.json` 的内容命名为 `guest_accounts`,保存到 `kv`
* 部署只需要简单的几步
* *(可跳过)阅读 <https://developers.cloudflare.com/workers/wrangler/workers-kv/#create-a-kv-namespace-with-wrangler>
* 在 <https://dash.cloudflare.com/> 创建一个叫 'twitter-monitor-workers-kv' 的 kv 命名空间,或者直接执行 `npx wrangler kv:namespace create kv`
* 将返回的 'id' 的值拷贝到 'kv_namespaces[0].id'
* 执行 `npx wrangler publish`
### Rate limit checker
* 设置环境变量 `android_guest_account`
```javascript
{
"authorization": "Bearer AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAAMHCxpeSDG1gLNLghVe8d74hl6k4%3DRUMF4xAQLsbeBhTSRrCiQpJtxoGWeyHrDb5te2jpGskWDFW82F",
"oauth_token": "...",
"oauth_token_secret": "..."
}
```
* 然后执行 `node apps/rate_limit_checker/run.mjs`
* 了解更多 [rate-limit-checker/README.md](https://github.com/BANKA2017/twitter-monitor/tree/node/apps/rate_limit_checker)
### OAuth 帐号池
* 了解更多 [open_account/README.md](https://github.com/BANKA2017/twitter-monitor/tree/node/apps/open_account)
### ↑~X 推*送!↓
* 了解更多 [web_push/README.md](https://github.com/BANKA2017/twitter-monitor/tree/node/apps/web_push)
## 支持的卡片类型
* 支持的卡片类型 (```*``` 表示使用小图, ```**``` 表示使用小图且图片在卡片右侧)
* summary*
* summary_large_image
* promo_image_convo
* promo_video_convo
* promo_website
* audio*
* player
* periscope_broadcast
* broadcast
* promo_video_website
* promo_image_app
* app
* direct_store_link_app
* live_event
* moment**
* poll2choice_text_only
* poll3choice_text_only
* poll4choice_text_only
* poll2choice_image
* poll3choice_image
* poll4choice_image
* appplayer
* audiospace
* unified_card
* image_website
* video_website
* image_carousel_website
* video_carousel_website
* image_app
* video_app
* image_carousel_app
* video_carousel_app
* image_multi_dest_carousel_website
* video_multi_dest_carousel_website
* mixed_media_multi_dest_carousel_website
* mixed_media_single_dest_carousel_website
* mixed_media_single_dest_carousel_app
* image_collection_website
* twitter_list_details
* media_with_details_horizontal (for topic ?)
* twitter_article
* community_details
* grok_share
## 子包
* `AxiosHelper` 用于处理解决 Workers 无法使用 axios 的问题, [阅读更多](https://github.com/BANKA2017/twitter-monitor/tree/node/packages/axios-helper)
## 出处 & 感谢
* `GoogleTokenGenerator.php` 这个文件出自 [google-translate-php](https://github.com/Stichoza/google-translate-php) 并经过本人小幅修改; 现在使用 Google translate 不再需要生成 TKK,故已移除本文件
* 函数 `get_mime` 重写自 [Tieba-Cloud-Sign](https://github.com/MoeNetwork/Tieba-Cloud-Sign/blob/c4ab393045bcabde97c1a70fbe8e8d56be8f7f1e/lib/sfc.functions.php#L790)
* 在 `package.json` 里面有一个叫 `sequelize-automate-gm` 的包,这是我用来从现有的数据库中导出 model 用的小工具(虽然有一些奇奇怪怪的 bug)
## 资料
- [怎么爬twitter(zh-Hans)](https://blog.nest.moe/posts/how-to-crawl-twitter/)
- [怎么爬twitter Graphql(zh-Hans)](https://blog.nest.moe/posts/how-to-crawl-twitter-with-graphql/)
- [怎么爬 Twitter(Android)(zh-Hans)](https://blog.nest.moe/posts/how-to-crawl-twitter-with-android/)
## About v2
The PHP version will be updated soon... maybe?
================================================
FILE: apps/backend/CoreFunctions/album/Album.mjs
================================================
import { isEmpty, isObject } from 'lodash-es'
import { getEmbedConversation, getTweets } from '../../../../libs/core/Core.fetch.mjs'
import { Log, VerifyQueryString } from '../../../../libs/core/Core.function.mjs'
import { Tweet } from '../../../../libs/core/Core.tweet.mjs'
import { apiTemplate } from '../../../../libs/share/Constant.mjs'
import { GenerateData } from '../online/OnlineTweet.mjs'
const AlbumSearch = async (req, env) => {
const platformList = { ns: 'nintendo_switch_share', ps: 'PlayStation®Network', xbox: 'xbox_one_social', xbox_game_bar: 'xbox_game_bar' }
const platform = ['ns', 'ps', 'xbox'].includes(req.query.platform) ? platformList[req.query.platform] : platformList['ns']
const name = VerifyQueryString(req.query.name, '')
const tweetId = VerifyQueryString(req.query.tweet_id, '')
const gameName = VerifyQueryString(req.query.game, '')
const queryArray = ['filter:twimg OR filter:consumer_video OR filter:pro_video', `source:${platform}`]
if (platform === 'xbox_one_social') {
queryArray.push(`OR source:xbox_game_bar OR #XboxShare`) //for game bar and android/iOS app
}
const isPhotos = !!req.query.photos
if (!isPhotos) {
if (name !== '') {
queryArray.push(
name
.split(' ')
.filter((tmpName) => tmpName)
.map((tmpName) => `from:${tmpName.startsWith('@') ? tmpName.slice(1) : tmpName}`)
.join(' OR ')
)
}
if (tweetId !== '') {
queryArray.push(`max_id:${tweetId}`)
}
if (gameName !== '') {
queryArray.push(`#${gameName}`)
}
}
let tweets = {}
try {
if (isPhotos) {
tweets = await getEmbedConversation({ tweet_id: tweetId })
} else {
tweets = await getTweets({
queryString: queryArray.join(' '),
cursor: '',
guest_token: env.guest_token3,
count: 20,
online: true,
graphqlMode: false,
searchMode: true
})
//TODO update guest_account status
//updateGuestToken
//await env.updateGuestToken(env, 'guest_token2', 4, tweets.headers.get('x-rate-limit-remaining') < 1, 'Search')
}
} catch (e) {
Log(false, 'error', `[${new Date()}]: #Album #${e.code} ${e.message}`)
return env.json(apiTemplate(e.code, e.message, {}, 'album'))
}
if ((isPhotos && !isObject(tweets.data)) || isEmpty(tweets.data)) {
Log(false, 'log', tweets)
return env.json(apiTemplate(404, 'No such tweet', {}, 'album'))
}
let tweetsContent = []
let tweetsInfo = {}
if (isPhotos) {
const tweetData = Tweet(tweets.data, {}, [], {}, false, false, true)
const guessSource = (() => {
if (tweetData.tags.some((tag) => ['ps6share', 'ps5share', 'ps4share', 'ps3share', 'ps2share', 'psshare'].includes(tag.text.toLowerCase()))) {
return 'PlayStation®Network'
} else if (tweetData.tags.some((tag) => ['nintendoswitch'].includes(tag.text.toLowerCase()))) {
return 'Nintendo Switch Share'
} else if (tweetData.tags.some((tag) => ['xbox', 'pcgaming', 'xboxshare', 'xboxseriesx', 'xboxseriess', 'xboxone'].includes(tag.text.toLowerCase()))) {
return 'Xbox'
} else {
return ''
}
})()
const tags = tweetData.tags
.filter(
(entity) =>
entity.type === 'hashtag' && !['ps6share', 'ps5share', 'ps4share', 'ps3share', 'ps2share', 'psshare', 'nintendoswitch', 'xbox', 'pcgaming', 'xboxshare', 'xboxseriesx', 'xboxseriess', 'xboxone'].includes(entity.text.toLowerCase())
)
.map((entity) => entity.text)
tweetsContent.push({
media: tweetData.media,
entities: tags,
source: guessSource, //tweetData.GeneralTweetData.source,// embed not include this
time: tweetData.GeneralTweetData.time,
tweet_id: tweetData.GeneralTweetData.tweet_id,
uid: tweetData.GeneralTweetData.uid,
name: tweetData.GeneralTweetData.name,
display_name: tweetData.GeneralTweetData.display_name
})
} else {
let { tweetsInfo, tweetsContent } = GenerateData(tweets, false, '', false, req)
if (tweetsInfo.errors.code !== 0) {
return env.json(apiTemplate(tweetsInfo.errors.code, tweetsInfo.errors.message, {}, 'album'))
}
tweetsContent = tweetsContent
.filter((content) => (isPhotos ? content.tweet_id === tweetId : true))
.map((content) => ({
media: content.mediaObject,
entities: content.entities
.filter(
(entity) =>
entity.type === 'hashtag' &&
!['ps6share', 'ps5share', 'ps4share', 'ps3share', 'ps2share', 'psshare', 'nintendoswitch', 'xbox', 'pcgaming', 'xboxshare', 'xboxseriesx', 'xboxseriess', 'xboxone'].includes(entity.text.toLowerCase())
)
.map((entity) => entity.text),
source: content.source,
time: content.time,
tweet_id: content.tweet_id,
uid: content.uid,
name: content.name,
display_name: content.display_name
}))
}
return env.json(
apiTemplate(
200,
'OK',
{
tweets: tweetsContent,
hasmore: !!tweetsContent.length,
top_tweet_id: tweetsInfo?.tweetRange?.max || '0',
bottom_tweet_id: tweetsInfo?.tweetRange?.min || '0'
},
'album'
)
)
}
export { AlbumSearch }
================================================
FILE: apps/backend/CoreFunctions/online/OnlineLogin.mjs
================================================
import { postLogout } from '../../../../libs/core/Core.fetch.mjs'
import { Log, Login, VerifyQueryString } from '../../../../libs/core/Core.function.mjs'
import { apiTemplate } from '../../../../libs/share/Constant.mjs'
import { GenerateAccountInfo } from '../../../../libs/core/Core.info.mjs'
const ApiLoginFlow = async (req, env) => {
//get others data
const att = VerifyQueryString(req.postBody.get('att'), '')
const _twitter_sess = VerifyQueryString(req.postBody.get('_twitter_sess'), '')
const _2fa = VerifyQueryString(req.postBody.get('_2fa'), '')
const acid = VerifyQueryString(req.postBody.get('acid'), '')
const screen_name = VerifyQueryString(req.postBody.get('screen_name'), '')
const password = VerifyQueryString(req.postBody.get('password'), '')
const subtask_id = VerifyQueryString(req.postBody.get('subtask_id'), '')
const flow_token = VerifyQueryString(req.postBody.get('flow_token'), '')
const exitFlow = (response) => response.flow_data.subtask_id === 'Ended'
const exitFlowResponse = (response) => {
let responseHeaders = new Headers()
responseHeaders.append('Set-Cookie', `att=; Max-Age=0; Path=/; Secure`)
responseHeaders.append('Set-Cookie', `_twitter_sess=; Max-Age=0; Path=/; Secure`)
return env.ResponseWrapper(apiTemplate(403, response?.message || 'Unknown error', {}, 'account'), 200, responseHeaders)
}
// TODO fix rate limit
// X-Rate-Limit-Limit: 187
// X-Rate-Limit-Remaining: 185
let tmpResponse, loginFlow
//Log(false, 'log', {att, _twitter_sess, flow_token, subtask_id, _2fa, acid})
if (att && _twitter_sess && flow_token && subtask_id && (_2fa || acid)) {
// part 2
loginFlow = new Login({}, { att, _twitter_sess }, flow_token)
if (subtask_id === 'LoginTwoFactorAuthChallenge') {
tmpResponse = await loginFlow.LoginTwoFactorAuthChallenge(_2fa)
//if (exitFlow(tmpResponse)) {return exitFlowResponse(tmpResponse)}
}
if (subtask_id === 'LoginAcid') {
tmpResponse = await loginFlow.LoginAcid(acid)
//if (exitFlow(tmpResponse)) {return exitFlowResponse(tmpResponse)}
}
} else if (screen_name && password) {
// part 1
loginFlow = new Login(env.guest_token2)
tmpResponse = await loginFlow.Init()
//updateGuestToken
await env.updateGuestToken(env, 'guest_token2', 4, tmpResponse.headers.get('x-rate-limit-remaining') < 1, 'Login')
if (exitFlow(tmpResponse)) {
return exitFlowResponse(tmpResponse)
}
tmpResponse = await loginFlow.LoginJsInstrumentationSubtask()
if (exitFlow(tmpResponse)) {
return exitFlowResponse(tmpResponse)
}
tmpResponse = await loginFlow.LoginEnterUserIdentifierSSO(screen_name)
if (exitFlow(tmpResponse)) {
return exitFlowResponse(tmpResponse)
}
//TODO we needn't this!
//if (loginFlow.getItem('subtask_id') === 'LoginEnterAlternateIdentifierSubtask') {
// tmpResponse = await loginCheck.LoginEnterAlternateIdentifierSubtask(screen_name)
//}
tmpResponse = await loginFlow.LoginEnterPassword(password)
if (exitFlow(tmpResponse)) {
return exitFlowResponse(tmpResponse)
}
tmpResponse = await loginFlow.AccountDuplicationCheck()
if (exitFlow(tmpResponse)) {
return exitFlowResponse(tmpResponse)
}
if (loginFlow.getItem('subtask_id') !== 'LoginSuccessSubtask') {
if (subtask_id === 'LoginTwoFactorAuthChallenge') {
if (!tmpResponse.data.subtasks[0]?.enter_text) {
tmpResponse = await loginFlow.LoginTwoFactorAuthChooseMethod('0')
}
} else if (subtask_id === 'Ended') {
return env.ResponseWrapper(apiTemplate(403, 'Screen_name and Password / Cookies required', {}, 'account'), 200)
}
const tmpCookies = loginFlow.getItem('cookie')
return env.ResponseWrapper(
apiTemplate(
401,
'2FA required',
{
subtask_id: loginFlow.getItem('subtask_id'),
flow_token: loginFlow.getItem('flow_token'),
att: tmpCookies.att,
_twitter_sess: tmpCookies._twitter_sess
},
'account'
),
200
)
}
} else {
return env.ResponseWrapper(apiTemplate(403, 'Screen_name and Password / Cookies required', {}, 'account'), 200)
}
tmpResponse = await loginFlow.Viewer()
const tmpCookies = loginFlow.getItem('cookie')
let responseHeaders = new Headers()
if (tmpCookies.auth_token) {
responseHeaders.append('Set-Cookie', `auth_token=${tmpCookies.auth_token}; Max-Age=157670000; Path=/; HttpOnly; Secure; SameSite=None`)
}
if (tmpCookies.ct0) {
responseHeaders.append('Set-Cookie', `ct0=${tmpCookies.ct0}; Max-Age=157670000; Path=/; HttpOnly; Secure; SameSite=None`)
}
try {
const accountInfo = GenerateAccountInfo(tmpResponse.data.data, {})
return env.ResponseWrapper(
apiTemplate(
200,
'OK',
{
account: accountInfo.GeneralAccountData || {},
cookie: { auth_token: tmpResponse.cookie?.auth_token || '', ct0: tmpResponse.cookie?.ct0 || '' }
},
'account'
),
200,
responseHeaders
)
} catch (e) {
//Log(false, 'error', e)
return env.ResponseWrapper(
apiTemplate(
500,
'Unable to parse userinfo',
{
account: {},
cookie: { auth_token: tmpResponse.cookie?.auth_token || '', ct0: tmpResponse.cookie?.ct0 || '' }
},
'account'
),
200,
responseHeaders
)
}
}
const ApiLogout = async (req, env) => {
//Log(false, 'log', req.rawHeaders, req?.cookies)
let responseHeaders = new Headers()
if (!(req?.cookies?.ct0 && req?.cookies?.auth_token)) {
return env.ResponseWrapper(apiTemplate(403, 'Invalid cookies', {}, 'account'), 200, responseHeaders)
}
try {
const tmpResponse = await postLogout({ cookie: { ct0: req.cookies.ct0, auth_token: req.cookies.auth_token } })
// TODO rate limit 100
// success {status: "ok"}
responseHeaders.append('Set-Cookie', `auth_token=; Max-Age=0; Path=/; Secure`)
responseHeaders.append('Set-Cookie', `ct0=; Max-Age=0; Path=/; Secure`)
return env.ResponseWrapper(apiTemplate(200, 'OK', tmpResponse.data, 'account'), 200, responseHeaders)
} catch (e) {
// Log(false, 'log', e)
return env.ResponseWrapper(apiTemplate(e.code || 500, e.message || 'Unknown error', {}, 'account'), 200, responseHeaders)
}
}
export { ApiLoginFlow, ApiLogout }
================================================
FILE: apps/backend/CoreFunctions/online/OnlineMisc.mjs
================================================
import { GenerateAccountInfo, GenerateCommunityInfo } from '../../../../libs/core/Core.info.mjs'
import { getCommunityInfo, getCommunitySearch, getListInfo, getListMember, getTypeahead } from '../../../../libs/core/Core.fetch.mjs'
import { Log, GetEntitiesFromText, VerifyQueryString } from '../../../../libs/core/Core.function.mjs'
import { TweetsInfo } from '../../../../libs/core/Core.tweet.mjs'
import { apiTemplate } from '../../../../libs/share/Constant.mjs'
const ApiTypeahead = async (req, env) => {
const text = VerifyQueryString(req.query.text, '')
let tmpTypeahead = {
users: [],
topics: []
}
try {
const tmpTypeaheadResponse = await getTypeahead({ text, guest_token: env.guest_token3 })
//TODO update guest_account status
//no rate limit
tmpTypeahead.topics = tmpTypeaheadResponse.data.topics
tmpTypeahead.users = tmpTypeaheadResponse.data.users.map((user) => GenerateAccountInfo(user).GeneralAccountData)
} catch (e) {
Log(false, 'log', e)
Log(false, 'error', `[${new Date()}]: #OnlineTypeahead #${text} #${e.code} ${e.message}`)
return env.json(apiTemplate(500, 'Something wrong', { users: [], topics: [] }, 'online'))
}
return env.json(apiTemplate(200, 'OK', tmpTypeahead, 'online'))
}
const ApiListInfo = async (req, env) => {
const listId = VerifyQueryString(req.query.list_id, 0)
const screenName = VerifyQueryString(req.query.name, '').toLocaleLowerCase()
const listSlug = VerifyQueryString(req.query.slug, '').toLocaleLowerCase()
//all empty
if (!(listId || (screenName && listSlug))) {
return env.json(apiTemplate(403, 'Invalid Request', {}, 'online'))
}
try {
let listInfoResponse = await getListInfo({ id: listId ? listId : '', screenName, listSlug, guest_token: env.guest_token3, authorization: 1, cookie: req.cookies })
//TODO update guest_account status
//updateGuestToken
//await env.updateGuestToken(env, 'guest_token2', 4, listInfoResponse.headers.get('x-rate-limit-remaining') < 1, 'ListInfo')
if (!listInfoResponse.data) {
return env.json(apiTemplate(500, 'Songthing wrong', {}, 'online'))
}
if (listId) {
listInfoResponse = listInfoResponse.data.data.list
} else {
listInfoResponse = listInfoResponse.data.data.user_by_screen_name.list
}
//get user
let { GeneralAccountData } = GenerateAccountInfo(listInfoResponse.user_results.result)
if (GeneralAccountData.description) {
GeneralAccountData.description = GeneralAccountData.description.replaceAll('\n', '\n<br>')
}
GeneralAccountData.top = String(GeneralAccountData.top)
GeneralAccountData.header = GeneralAccountData.header.replaceAll(/http(|s):\/\//gm, '')
GeneralAccountData.uid_str = String(GeneralAccountData.uid)
//GeneralAccountData.uid = Number(GeneralAccountData.uid)
let originalTextAndEntities = GetEntitiesFromText(GeneralAccountData.description)
GeneralAccountData.description_original = originalTextAndEntities.originalText
GeneralAccountData.description_entities = originalTextAndEntities.entities
let responseData = {
user_info: GeneralAccountData,
name: listInfoResponse.name ?? '',
description: listInfoResponse.description ?? '',
id: listInfoResponse.id_str ?? '',
member_count: listInfoResponse.member_count ?? 0,
subscriber_count: listInfoResponse.subscriber_count ?? 0,
created_at: Math.ceil((listInfoResponse.created_at ?? 0) / 1000),
banner: {
url: listInfoResponse?.custom_banner_media_results?.result?.media_info?.original_img_url ?? listInfoResponse?.default_banner_media_results?.result?.media_info?.original_img_url ?? '',
original_height: listInfoResponse?.custom_banner_media_results?.result?.media_info?.original_img_height ?? listInfoResponse?.default_banner_media_results?.result?.media_info?.original_img_height ?? 0,
original_width: listInfoResponse?.custom_banner_media_results?.result?.media_info?.original_img_width ?? listInfoResponse?.default_banner_media_results?.result?.media_info?.original_img_width ?? 0,
media_key: listInfoResponse?.custom_banner_media_results?.result?.media_key ?? listInfoResponse?.default_banner_media_results?.result?.media_key ?? ''
}
}
return env.json(apiTemplate(200, 'OK', responseData, 'online'))
} catch (e) {
Log(false, 'log', e)
Log(false, 'error', `[${new Date()}]: #OnlineListInfo ${listId ? '#' + listId : '[@' + screenName + '](' + listSlug + ')'} #${e.code} ${e.message}`)
return env.json(apiTemplate(500, 'Songthing wrong', {}, 'online'))
}
}
const ApiListMemberList = async (req, env) => {
const listId = VerifyQueryString(req.query.list_id, 0)
const cursor = VerifyQueryString(req.query.cursor, '')
const count = VerifyQueryString(req.query.count, 20)
if (!listId) {
return env.json(apiTemplate(403, 'Invalid Request', {}, 'online'))
}
try {
let listMemberResponse = await getListMember({ id: listId, cursor, count, guest_token: env.guest_token3, authorization: 1, cookie: req.cookies })
//TODO update guest_account status
//updateGuestToken
//await env.updateGuestToken(env, 'guest_token2', 4, listMemberResponse.headers.get('x-rate-limit-remaining') < 1, 'ListMember')
if (!listMemberResponse.data) {
return env.json(apiTemplate(500, 'Songthing wrong', {}, 'online'))
}
const ParseList = TweetsInfo(listMemberResponse.data, true)
return env.json(
apiTemplate(
200,
'OK',
{
users: Object.entries(ParseList.users).map((user) => {
let { GeneralAccountData } = GenerateAccountInfo(user[1])
if (GeneralAccountData.description) {
GeneralAccountData.description = GeneralAccountData.description.replaceAll('\n', '\n<br>')
}
GeneralAccountData.top = String(GeneralAccountData.top)
GeneralAccountData.header = GeneralAccountData.header.replaceAll(/http(|s):\/\//gm, '')
GeneralAccountData.uid_str = String(GeneralAccountData.uid)
//GeneralAccountData.uid = Number(GeneralAccountData.uid)
let originalTextAndEntities = GetEntitiesFromText(GeneralAccountData.description)
GeneralAccountData.description_original = originalTextAndEntities.originalText
GeneralAccountData.description_entities = originalTextAndEntities.entities
return GeneralAccountData
}),
cursor: ParseList.cursor
},
'online'
)
)
} catch (e) {
Log(false, 'log', e)
Log(false, 'error', `[${new Date()}]: #OnlineListMemberList #${listId} #${e.code} ${e.message}`)
return env.json(apiTemplate(500, 'Songthing wrong', {}, 'online'))
}
}
const ApiCommunityInfo = async (req, env) => {
const communityId = VerifyQueryString(req.query.community_id, 0)
//all empty
if (!communityId) {
return env.json(apiTemplate(403, 'Invalid Request', {}, 'online'))
}
try {
let communityInfoResponse = await getCommunityInfo({ id: communityId, guest_token: env.guest_token3, authorization: 1 })
//TODO update guest_account status
//updateGuestToken
//await env.updateGuestToken(env, 'guest_token2', 4, communityInfoResponse.headers.get('x-rate-limit-remaining') < 1, 'CommunityInfo')
if (!communityInfoResponse.data) {
return env.json(apiTemplate(500, 'Songthing wrong', {}, 'online'))
}
const tmpCommunityInfoResponse = communityInfoResponse.data?.data?.communityResults?.result
if (!tmpCommunityInfoResponse) {
return env.json(apiTemplate(500, 'Songthing wrong', {}, 'online'))
}
let responseData = GenerateCommunityInfo(tmpCommunityInfoResponse)
return env.json(apiTemplate(200, 'OK', responseData, 'online'))
} catch (e) {
Log(false, 'log', e)
Log(false, 'error', `[${new Date()}]: #OnlineCommunityInfo ${'#' + communityId} #${e.code} ${e.message}`)
return env.json(apiTemplate(500, 'Songthing wrong', {}, 'online'))
}
}
const ApiCommunitySearch = async (req, env) => {
const queryString = VerifyQueryString(req.query.q, '')
const cursor = VerifyQueryString(req.query.cursor, '')
// Note: now 'count' is unused, it might useful in future
const count = VerifyQueryString(req.query.count, 0)
if (!queryString) {
return env.json(apiTemplate(403, 'Invalid Request', {}, 'online'))
}
try {
const tmpCommunitySearchResponse = await getCommunitySearch({ queryString, cursor, count, guest_token: env.guest_token3, authorization: 1 })
//TODO update guest_account status
const communitiesList = []
// community
if (Array.isArray(tmpCommunitySearchResponse.data.data?.communities_search_slice?.items_results)) {
for (const tmpCommunityInfo of tmpCommunitySearchResponse.data.data?.communities_search_slice?.items_results) {
const tmpCommunityResult = tmpCommunityInfo.result
communitiesList.push({
name: tmpCommunityResult.name ?? '',
id: tmpCommunityResult.rest_id ?? '',
member_count: tmpCommunityResult.member_count ?? 0,
default_theme: tmpCommunityResult.default_theme ?? tmpCommunityResult.custom_theme ?? '_',
banner: {
url: tmpCommunityResult?.custom_banner_media?.media_info?.original_img_url ?? tmpCommunityResult?.default_banner_media?.media_info?.original_img_url ?? '',
original_height: tmpCommunityResult?.custom_banner_media?.media_info?.original_img_height ?? tmpCommunityResult?.default_banner_media?.media_info?.original_img_height ?? 0,
original_width: tmpCommunityResult?.custom_banner_media?.media_info?.original_img_width ?? tmpCommunityResult?.default_banner_media?.media_info?.original_img_width ?? 0,
media_key: tmpCommunityResult?.custom_banner_media?.id ?? tmpCommunityResult?.default_banner_media?.id ?? ''
}
})
}
}
// cursor
const nextCursor = tmpCommunitySearchResponse.data.data?.communities_search_slice?.slice_info?.next_cursor || ''
return env.json(
apiTemplate(
200,
'OK',
{
communities_list: communitiesList,
cursor: nextCursor,
hasmore: !!nextCursor
},
'online'
)
)
} catch (e) {
Log(false, 'error', e)
Log(false, 'error', `[${new Date()}]: #OnlineCommunitySearch ${queryString} #${e.code} ${e.message}`)
return env.json(apiTemplate(500, 'Songthing wrong', {}, 'online'))
}
}
export { ApiTypeahead, ApiListInfo, ApiListMemberList, ApiCommunityInfo, ApiCommunitySearch }
================================================
FILE: apps/backend/CoreFunctions/online/OnlineTrends.mjs
================================================
import { getTrends } from '../../../../libs/core/Core.fetch.mjs'
import { Log } from '../../../../libs/core/Core.function.mjs'
import { apiTemplate } from '../../../../libs/share/Constant.mjs'
const ApiTrends = async (req, env) => {
let tmpTrends = []
try {
const tmpTrendsRequest = await getTrends({ initial_tab_id: 'trends', count: 20, guest_token: env.guest_token3, cookie: req.cookies })
//TODO update guest_account status
//updateGuestToken
//await env.updateGuestToken(env, 'guest_token2', 4, tmpTrendsRequest.headers.get('x-rate-limit-remaining') < 1, 'Trending')
tmpTrends = tmpTrendsRequest.data.timeline.instructions[1].addEntries.entries
.find((entity) => entity.entryId === 'trends')
.content.timelineModule.items.map((item) => ({
name: item?.item?.content?.trend?.name ?? '',
domainContext: item?.item?.content?.trend?.trendMetadata?.domainContext ?? '',
metaDescription: item?.item?.content?.trend?.trendMetadata?.metaDescription ?? undefined,
displayedRelatedVariants: item?.item?.clientEventInfo?.details?.guideDetails?.transparentGuideDetails?.trendMetadata?.displayedRelatedVariants ?? undefined
}))
} catch (e) {
Log(false, 'log', e)
return env.json(apiTemplate(500, 'Ok', [], 'online'))
}
return env.json(apiTemplate(200, 'OK', tmpTrends, 'online'))
}
export { ApiTrends }
================================================
FILE: apps/backend/CoreFunctions/online/OnlineTweet.mjs
================================================
import { Parser } from 'm3u8-parser'
import path2array from '../../../../libs/core/Core.apiPath.mjs'
import { getAudioSpace, getLiveVideoStream, getConversation, getPollResult, getTweets, getBroadcast, getListTimeLine, AxiosFetch, getCommunityTweetsTimeline, getEmbedConversation } from '../../../../libs/core/Core.fetch.mjs'
import { Log, GetEntitiesFromText, VerifyQueryString, IsNumber } from '../../../../libs/core/Core.function.mjs'
import { AudioSpace, Broadcast, Time2SnowFlake, Tweet, TweetsInfo } from '../../../../libs/core/Core.tweet.mjs'
import { apiTemplate } from '../../../../libs/share/Constant.mjs'
import { Rss } from '../../../../libs/core/Core.Rss.mjs'
import { isEmpty, isObject } from 'lodash-es'
const ApiTweets = async (req, env) => {
const isRssMode = ['rss', 'xml'].includes(req.query.format)
const queryCount = VerifyQueryString(req.query.count, 0)
const count = queryCount ? (queryCount > 100 ? 100 : queryCount < 1 ? 1 : queryCount) : isRssMode ? 20 : 10
const tweet_id = VerifyQueryString(req.query.tweet_id, 0)
const cursor = String(req.query.cursor ?? req.query.tweet_id ?? '0') //TODO Notice, VerifyQueryString()
const refresh = (req.query.refresh || '0') !== '0'
const name = VerifyQueryString(req.query.name, '')
const uid = VerifyQueryString(req.query.uid, 0)
const queryArray = []
//use $tweet_id to replace $cursor
//TODO reuse cursor as name
// display type all, self, retweet, media, album, space
const displayType = ['all', 'include_reply'].includes(req.query.display) ? req.query.display : 'all'
//conversation
const isConversation = !!(Number(req.query.is_status, 0) && cursor !== '0')
const loadConversation = VerifyQueryString(req.query.load_conversation, 0) !== 0
//list
const listId = VerifyQueryString(req.query.list_id, 0)
//community
const communityId = VerifyQueryString(req.query.community_id, 0)
let tweets = {}
let graphqlMode = true
let searchMode = false
if (listId) {
try {
tweets = await getListTimeLine({
id: listId,
count,
guest_token: env.guest_token3,
authorization: 1,
graphqlMode,
cursor: !IsNumber(cursor, true, true) ? (cursor ? cursor : '') : '',
cookie: req.cookies
})
//TODO update guest_account status
//updateGuestToken
//await env.updateGuestToken(env, 'guest_token2', 4, tweets.headers.get('x-rate-limit-remaining') < 1, 'ListTimeLime')
} catch (e) {
Log(false, 'log', e)
Log(false, 'error', `[${new Date()}]: #OnlineListTimeline #${tweet_id} #${e.code} ${e.message}`)
return env.json(apiTemplate(e.code, e.message))
}
} else if (communityId) {
try {
tweets = await getCommunityTweetsTimeline({
id: communityId,
count,
guest_token: env.guest_token3,
authorization: 1,
graphqlMode,
cursor: !IsNumber(cursor, true, true) ? (cursor ? cursor : '') : '',
cookie: req.cookies
})
//TODO update guest_account status
//updateGuestToken
//await env.updateGuestToken(env, 'guest_token2', 4, tweets.headers.get('x-rate-limit-remaining') < 1, 'CommunityTimeLime')
} catch (e) {
Log(false, 'log', e)
Log(false, 'error', `[${new Date()}]: #OnlineCommunityTimeline #${tweet_id} #${e.code} ${e.message}`)
return env.json(apiTemplate(e.code, e.message))
}
} else if (isConversation) {
try {
const useWeb = env.guest_token3.success
tweets = await getConversation({ tweet_id, guest_token: useWeb ? env.guest_token2 : env.guest_token3, graphqlMode, cursor: !IsNumber(cursor, true, true) ? (cursor ? cursor : '') : '', cookie: req.cookies, web: useWeb ? 2 : false })
//TODO mix mode, tweet and replies
//updateGuestToken
await env.updateGuestToken(env, useWeb ? 'guest_token2' : 'guest_token3', 4, tweets.headers.get('x-rate-limit-remaining') < 1, 'TweetDetail')
} catch (e) {
Log(false, 'error', `[${new Date()}]: #OnlineTweetsConversation #${tweet_id} #${e.code} ${e.message}`)
return env.json(apiTemplate(e.code, e.message))
}
}
//else if (name !== '' && displayType === 'all') {
// try {
// tweets = await getTweets(name, cursor, env.guest_token2, 40, true, true, false)
// } catch (e) {
// Log(false, 'error', `[${new Date()}]: #OnlineTweetsConversation #${cursor} #${e.code} ${e.message}`)
// return env.json(apiTemplate(e.code, e.message))
// }
//}
else {
if (uid === '') {
return env.json(apiTemplate(404, 'No such account'))
}
//queryArray.push('-filter:replies')
//if (name) {
// queryArray.push(`from:${name}`)
//}
//switch (displayType) {
// case 'self':
// queryArray.push('-filter:nativeretweets', '-filter:retweets', 'include:quote')
// break
// case 'retweet':
// queryArray.push('filter:nativeretweets', 'filter:retweets', 'include:quote')
// break
// case 'media':
// queryArray.push('filter:media')
// break
// case 'album':
// queryArray.push('-filter:nativeretweets', '-filter:retweets', 'include:quote', 'filter:media')
// break
// case 'space':
// queryArray.push('filter:spaces')
// break
// case 'include_reply':
// queryArray.push('include:reply')
// break;
// default:
// queryArray.push('include:nativeretweets', 'include:retweets', 'include:quote')
//}
//$queryString = "from:$name since:2000-01-01 include:nativeretweets include:retweets include:quote";//$name 2000-01-01 include retweets
//if (cursor !== '0') {
// queryArray.push((VerifyQueryString(req.query.refresh, '0') !== '0') ? `since_id:${BigInt(cursor) + BigInt(1)}` : `max_id:${BigInt(cursor) - BigInt(1)}`)
//}
try {
//if (displayType === 'include_reply') {
graphqlMode = true //displayType === 'include_reply'
tweets = await getTweets({
queryString: uid,
cursor: cursor === '0' ? '' : cursor,
bottomCursor: !refresh,
guest_token: env.guest_token3,
count,
online: true,
web: false, //displayType !== 'include_reply',
graphqlMode: graphqlMode, //displayType === 'include_reply',
searchMode: false,
withReply: displayType === 'include_reply',
cookie: req.cookies
})
//tweets = await getTweets(queryArray.join(' '), '', global.guest_token2.token, count, true, false, true)
//TODO update guest_account status
//updateGuestToken
//await env.updateGuestToken(env, 'guest_token2', 4, tweets.headers.get('x-rate-limit-remaining') < 1, 'UserTweets')
//}
//else {
// graphqlMode = false
// searchMode = true
// tweets = await getTweets({
// queryString: `from:${name} ${tweet_id ? (req.query.refresh !== '0' ? 'since_id:' + tweet_id : 'max_id:' + (BigInt(tweet_id) - BigInt(1)).toString()) : ''}`, //uid,
// //cursor: cursor === '0' ? '' : cursor,
// guest_token: env.guest_token2,
// count,
// online: true,
// graphqlMode,
// searchMode,
// withReply: false, //displayType === 'include_reply',
// cookie: req.cookies
// })
// //tweets = await getTweets(queryArray.join(' '), '', global.guest_token2.token, count, true, false, true)
// //updateGuestToken
// await env.updateGuestToken(env, 'guest_token2', 4, false, 'Search')
//}
} catch (e) {
Log(false, 'error', `[${new Date()}]: #OnlineTweetsTimeline #'${queryArray.join(' ')}' #${e.code} ${e.message}`)
return env.json(apiTemplate(e.code, e.message))
}
}
const { tweetsInfo, tweetsContent, rssContent } = GenerateData(tweets, isConversation, loadConversation || listId || communityId || displayType === 'include_reply' ? '' : uid, graphqlMode, req)
if (tweetsInfo.errors.code !== 0) {
return env.json(apiTemplate(tweetsInfo.errors.code, tweetsInfo.errors.message))
} else if (isRssMode) {
return env.xml(rssContent)
}
return env.json(
apiTemplate(200, 'OK', {
tweets: isConversation ? tweetsContent.reverse() : tweetsContent,
hasmore: searchMode || !!(tweetsContent.length ? tweetsInfo.cursor.bottom || tweetsInfo.tweetRange.min || false : false),
//top_tweet_id: tweetsInfo.tweetRange.max || '0',
//bottom_tweet_id: tweetsInfo.tweetRange.min || '0'
top_tweet_id: tweetsInfo.cursor.top || tweetsInfo.tweetRange.max || '',
bottom_tweet_id: tweetsInfo.cursor.bottom || tweetsInfo.tweetRange.min || ''
})
)
}
const ApiSearch = async (req, env) => {
const isRssMode = ['rss', 'xml'].includes(req.query.format)
const type = req.type //req.params[0]
const advancedSearchMode = (req.query.advanced || '0') === '1'
const cursor = BigInt(VerifyQueryString(req.query.tweet_id, 0))
const queryCount = VerifyQueryString(req.query.count, 20)
const refresh = (req.query.refresh || '0') !== '0'
const start = Number(VerifyQueryString(req.query.start, 0))
const end = Number(VerifyQueryString(req.query.end, 0))
let tweets = []
const queryArray = []
if (type === 'hashtag') {
if (!VerifyQueryString(req.query.hash, false)) {
return env.json(apiTemplate())
}
queryArray.push(`#${req.query.hash}`)
} else if (type === 'cashtag') {
if (!VerifyQueryString(req.query.hash, false)) {
return env.json(apiTemplate())
}
queryArray.push(`$${req.query.hash}`)
} else if (advancedSearchMode) {
const textOrMode = (req.query.text_or_mode || '0') !== '0'
const textNotMode = (req.query.text_not_mode || '0') !== '0'
const userOrMode = (req.query.user_and_mode || '0') !== '0'
const userNotMode = (req.query.user_not_mode || '0') !== '0'
//const tweetType = isNaN(req.query.tweet_type) ? 0 : ([0,1,2].includes(Number(req.query.tweet_type)) ? Number(req.query.tweet_type) : 0)
const getMedia = !!(req.query.tweet_media || false)
//keywords
queryArray.push(
VerifyQueryString(req.query.q, '')
.split(' ')
.map((keyword, index) => {
if (index > 0 && textOrMode) {
return `OR ` + (textNotMode ? '-' : '') + keyword
}
return (textNotMode ? '-' : '') + keyword
})
.join(' ')
)
//names
queryArray.push(
VerifyQueryString(req.query.user, '')
.replaceAll('@', '')
.split(' ')
.map((keyword, index) => {
if (index > 0 && userOrMode) {
return `OR ` + (userNotMode ? '-' : '') + keyword
}
return (userNotMode ? '-' : '') + keyword
})
.join(' ')
)
if (getMedia) {
queryArray.push('filter:media')
}
} else if (VerifyQueryString(req.query.q, '')) {
queryArray.push(VerifyQueryString(req.query.q, ''))
}
//time
///start
if (cursor !== BigInt(0) && refresh) {
queryArray.push('since_id:' + String(cursor + BigInt(1)))
} else if (start > 0) {
queryArray.push('since_id:' + String(Time2SnowFlake(start * 1000)))
} else {
queryArray.push('since_id:0')
}
///end
if (cursor !== BigInt(0) && !refresh) {
queryArray.push('max_id:' + String(cursor - BigInt(1)))
} else if (end > 0 && end > start) {
queryArray.push('max_id:' + String(Time2SnowFlake(end * 1000)))
}
try {
tweets = await getTweets({
queryString: queryArray.join(' '),
cursor: '',
guest_token: env.guest_token3,
count: queryCount,
online: true,
graphqlMode: true,
searchMode: true,
cookie: req.cookies
})
//TODO update guest_account status
//updateGuestToken
//await env.updateGuestToken(env, 'guest_token2', 4, tweets.headers.get('x-rate-limit-remaining') < 1, 'Search')
} catch (e) {
Log(false, 'error', `[${new Date()}]: #OnlineTweetsSearch #'${queryArray.join(' ')}' #${e.code} ${e.message}`)
return env.json(apiTemplate(e.code, e.message))
}
const { tweetsInfo, tweetsContent, rssContent } = GenerateData(tweets, false, '', true, req)
if (tweetsInfo.errors.code !== 0) {
return env.json(apiTemplate(tweetsInfo.errors.code, tweetsInfo.errors.message))
} else if (isRssMode) {
return env.xml(rssContent)
}
return env.json(
apiTemplate(200, 'OK', {
tweets: tweetsContent,
hasmore: !!tweetsContent.length,
top_tweet_id: tweetsInfo.tweetRange.max || '0',
bottom_tweet_id: tweetsInfo.tweetRange.min || '0'
})
)
}
const ApiPoll = async (req, env) => {
const tweet_id = VerifyQueryString(req.query.tweet_id, 0)
if (!tweet_id) {
return env.json(apiTemplate())
}
const tmpPollData = await getPollResult({ tweet_id, guest_token: env.guest_token2, cookie: req.cookies })
//updateGuestToken
await env.updateGuestToken(env, 'guest_token2', 4, tmpPollData.headers.get('x-rate-limit-remaining') < 1, 'TweetDetail')
if (tmpPollData.code === 200) {
return env.json(
apiTemplate(
200,
'OK',
tmpPollData.data.map((poll) => Number(poll))
)
)
} else {
Log(false, 'error', `[${new Date()}]: #OnlinePoll #${tweet_id} #${tmpPollData.code} Something wrong`)
return env.json(apiTemplate(tmpPollData.code, 'Something wrong', []))
}
}
const ApiAudioSpace = async (req, env) => {
const id = VerifyQueryString(req.query.id, '')
if (!id) {
return env.json(apiTemplate())
}
let tmpAudioSpaceData = null
try {
//TODO fix cache response
if (env.audio_apsce_cache[id]) {
tmpAudioSpaceData = { data: env.audio_apsce_cache[id] }
} else {
tmpAudioSpaceData = await getAudioSpace({ id, guest_token: env.guest_token3, cookie: req.cookies })
}
//cache response
// if (!env.audio_apsce_cache[id]) {
// env.audio_apsce_cache[id] = tmpAudioSpaceData.data
// env.mediaCacheSave(JSON.stringify(env.audio_apsce_cache), '_audio_apsce_cache.json')
// }
} catch (e) {
Log(false, 'error', `[${new Date()}]: #OnlineAudioSpace #${id} #500 Unkonwn Error`, e)
return env.json(apiTemplate())
}
//TODO update guest_account status
//updateGuestToken
//await env.updateGuestToken(env, 'guest_token2', 4, tmpAudioSpaceData.headers.get('x-rate-limit-remaining') < 1, 'AudioSpaceById')
if (tmpAudioSpaceData.data?.data?.audioSpace || false) {
let tmpAudioSpace = AudioSpace(tmpAudioSpaceData.data)
//get link
if (tmpAudioSpace.is_available_for_replay || (Number(tmpAudioSpace.start) <= Date.now() && tmpAudioSpace.end === '0')) {
try {
const tmpAudioSpaceLink = await getLiveVideoStream({ media_key: tmpAudioSpace.media_key })
if (tmpAudioSpaceLink.data?.source?.noRedirectPlaybackUrl) {
tmpAudioSpace.playback = tmpAudioSpaceLink.data?.source?.noRedirectPlaybackUrl.replaceAll('?type=replay', '').replaceAll('?type=live', '')
}
} catch (e) {
Log(false, 'error', e)
}
}
return env.json(apiTemplate(200, 'OK', tmpAudioSpace))
} else if (tmpAudioSpaceData.data?.errors || tmpAudioSpaceData.data?.code) {
Log(false, 'error', `[${new Date()}]: #OnlineAudioSpace #${id} #500 Something wrong`, tmpAudioSpaceData.data?.code, tmpAudioSpaceData.data?.errors)
return env.json(apiTemplate(500, 'Something wrong'))
} else if (!tmpAudioSpaceData.data?.data?.audioSpace) {
Log(false, 'error', `[${new Date()}]: #OnlineAudioSpace #${id} #404 No such space`)
return env.json(apiTemplate(404, 'No such space'))
} else {
Log(false, 'error', `[${new Date()}]: #OnlineAudioSpace #${id} #500 Unkonwn Error`)
return env.json(apiTemplate())
}
}
const ApiBroadcast = async (req, env) => {
const id = VerifyQueryString(req.query.id, '')
if (!id) {
return env.json(apiTemplate())
}
try {
const tmpBroadcastData = await getBroadcast({ id, cookie: req.cookies })
let tmpBroadcast = Broadcast(tmpBroadcastData.data)
//get link
if (tmpBroadcast.is_available_for_replay || (Number(tmpBroadcast.start) <= Date.now() && tmpBroadcast.end === '0')) {
try {
const tmpBroadcastLink = await getLiveVideoStream({ media_key: tmpBroadcast.media_key })
if (tmpBroadcastLink.data?.source?.noRedirectPlaybackUrl) {
let m3u8Url = tmpBroadcastLink.data?.source?.noRedirectPlaybackUrl
try {
const tmpParsedM3u8Url = new URL(m3u8Url)
const urlPrefix = tmpParsedM3u8Url.origin
if (tmpParsedM3u8Url.pathname.split('/').pop().includes('master_dynamic_')) {
const m3u8Data = (await AxiosFetch.get(m3u8Url)).data
const m3u8Parser = new Parser()
m3u8Parser.push(m3u8Data)
m3u8Parser.end()
m3u8Url = urlPrefix + m3u8Parser.manifest.playlists.sort((a, b) => b.attributes.BANDWIDTH - a.attributes.BANDWIDTH)[0].uri
}
} catch (e) {
Log(false, 'error', e)
Log(false, 'log', `[${new Date()}]: Unable to parse playlists from '${m3u8Url}', fallback. #OnlineBroadcast`)
}
tmpBroadcast.playback = m3u8Url.replaceAll('?type=replay', '').replaceAll('?type=live', '')
}
} catch (e) {
Log(false, 'error', e)
}
}
return env.json(apiTemplate(200, 'OK', tmpBroadcast))
} catch (e) {
if (!(e?.code && e?.message) && e?.errors) {
e = e.errors[0]
}
if (e?.code && e?.message) {
Log(false, 'error', `[${new Date()}]: #OnlineBroadcast #${id} #500 Something wrong`, e.code, e.message)
return env.json(apiTemplate(500, `#${e.code} ${e.message}`))
} else {
Log(false, 'error', `[${new Date()}]: #OnlineBroadcast #${id} #500 Unkonwn Error`)
return env.json(apiTemplate())
}
}
}
const ApiMedia = async (req, env) => {
const tweet_id = VerifyQueryString(req.query.tweet_id, 0)
if (!tweet_id) {
return env.json(apiTemplate())
}
try {
const tmpConversation = await getEmbedConversation({
tweet_id
})
if (!isObject(tmpConversation.data) || isEmpty(tmpConversation.data)) {
Log(false, 'log', tmpConversation)
return env.json(apiTemplate(404, 'No such tweet'))
}
const tweetData = Tweet(tmpConversation.data, {}, [], {}, false, false, true)
return env.json(
apiTemplate(200, 'OK', {
video: !(Array.isArray(tweetData.video) && tweetData.video.length === 0),
card_info: ((card) => {
if (['broadcast', 'periscope_broadcast', 'audiospace'].includes(card?.type)) {
return {
type: card.type,
id: card.url
}
} else if (card?.type === 'live_event' && tweetData.original_data?.card?.binding_values?.broadcast_id?.string_value) {
return {
type: card.type,
id: tweetData.original_data?.card?.binding_values?.broadcast_id?.string_value
}
}
return undefined
})(tweetData.card),
video_info: tweetData.video,
media_info: tweetData.media
.filter((media) => media.source !== 'cover')
.map((media) => {
media.cover = media.cover.replaceAll(/(https:\/\/|http:\/\/)/gm, '')
media.url = media.url.replaceAll(/(https:\/\/|http:\/\/)/gm, '')
return media
})
})
)
} catch (e) {
Log(false, 'log', e)
Log(false, 'error', `[${new Date()}]: #OnlineTweetMedia #${tweet_id} #${e.code} ${e.message}`)
return env.json(apiTemplate(e.code, e.message))
}
}
const TweetsData = (content = {}, users = {}, contents = [], precheckUid = '', graphqlMode = true, isConversation = false) => {
let exportTweet = Tweet(content, users, contents, {}, graphqlMode, false, true)
exportTweet.GeneralTweetData.favorite_count = exportTweet.interactiveData?.favorite_count || 0
exportTweet.GeneralTweetData.retweet_count = exportTweet.interactiveData?.retweet_count || 0
exportTweet.GeneralTweetData.quote_count = exportTweet.interactiveData?.quote_count || 0
exportTweet.GeneralTweetData.reply_count = exportTweet.interactiveData?.reply_count || 0
exportTweet.GeneralTweetData.view_count = exportTweet.interactiveData?.view_count || 0 //TODO only supported graphql now
//rtl
exportTweet.GeneralTweetData.rtl = exportTweet.isRtl
// display text range
exportTweet.GeneralTweetData.display_text_range = exportTweet.displayTextRange
//vibe
if (exportTweet.vibe?.text || exportTweet.vibe?.imgDescription) {
exportTweet.GeneralTweetData.vibe = exportTweet.vibe
}
//place
if (exportTweet.place?.id) {
exportTweet.GeneralTweetData.place = exportTweet.place
}
//rich text
if (exportTweet.richtext?.richtext) {
exportTweet.GeneralTweetData.richtext = exportTweet.richtext.richtext
}
//community
if (exportTweet.community && Object.keys(exportTweet.community).length > 0) {
exportTweet.GeneralTweetData.community = exportTweet.community
}
//birdwatch
if (exportTweet.birdwatch && Object.keys(exportTweet.birdwatch).length > 0) {
exportTweet.GeneralTweetData.birdwatch = exportTweet.birdwatch
}
//socialContent
if ((exportTweet?.socialContext?.contextType || '').toLocaleLowerCase() === 'pin') {
exportTweet.GeneralTweetData.is_top = true
}
//check poster
if (isConversation || precheckUid === '' || precheckUid === exportTweet.GeneralTweetData.uid) {
return {
code: 200,
userInfo: exportTweet.userInfo,
retweetUserInfo: exportTweet.retweetUserInfo,
data: returnDataForTweets(exportTweet.GeneralTweetData, true, exportTweet.tags, exportTweet.polls, exportTweet.card, exportTweet.cardApp, exportTweet.quote, exportTweet.media)
}
}
return { code: 0, data: {} }
}
const returnDataForTweets = (tweet = {}, historyMode = false, tweetEntities = [], tweetPolls = [], tweetCard = {}, tweetCardApp = {}, tweetQuote = {}, tweetMedia = []) => {
tweet.type = 'tweet'
if (historyMode) {
//处理history模式
tweet['entities'] = tweetEntities
}
//$tweet["full_text_original"] = preg_replace('/ https:\/\/t.co\/[\w]+/', '', $tweet["full_text_original"]);//TODO for history mode
//处理投票
tweet.pollObject = {}
if (tweet.poll && tweetPolls.length) {
//TODO check tweetID
//Log(false, 'log', String(poll.tweet_id), String(tweet.tweet_id), poll.tweet_id, tweet.tweet_id, poll.tweet_id === tweet.tweet_id)
tweet.pollObject = tweetPolls
.filter((poll) => poll.tweet_id === tweet.tweet_id)
.map((poll) => {
delete poll.tweet_id
poll.checked = !!poll.checked
//poll.count = 0
return poll
})
}
//处理卡片
tweet.cardObject = {}
if (tweet.card && Object.keys(tweetCard).length) {
tweet.cardObject = tweetCard
if (tweet.cardObject.unified_card_app) {
tweet.cardObject.unified_card_app = !!tweet.cardObject.unified_card_app
tweet.cardObject.app = tweetCardApp
}
}
//处理引用
tweet.quoteObject = {}
if (tweet.quote_status && Object.keys(tweetQuote).length) {
tweet.quoteObject = tweetQuote
tweet.quoteObject.id_str = tweet.quoteObject.tweet_id
tweet.quoteObject.tweet_id = tweet.quoteObject.tweet_id
tweet.quote_status_str = tweet.quoteObject.id_str
const { originalText, entities } = GetEntitiesFromText(tweet.quoteObject.full_text, 'quote')
tweet.quoteObject.full_text = originalText
tweet.quoteObject.entities = entities
}
//media
let tmpInageText = ''
tweet.mediaObject = []
if (tweet.media || tweet.cardObject.media || tweet.quoteObject.media) {
for (let queryMediaSingle of tweetMedia) {
//TODO check equal tweet id
if (queryMediaSingle.tweet_id === tweet.tweet_id || queryMediaSingle.tweet_id === tweet.quote_status) {
queryMediaSingle.cover = queryMediaSingle.cover.replaceAll(/http(s|):\/\//gm, '')
queryMediaSingle.url = queryMediaSingle.url.replaceAll(/http(s|):\/\//gm, '')
if (!queryMediaSingle.title) {
delete queryMediaSingle.title
}
if (!queryMediaSingle.description) {
delete queryMediaSingle.description
}
if (queryMediaSingle.source === 'tweets' && queryMediaSingle.tweet_id === tweet.tweet_id) {
tmpInageText += `<img src="https://pbs.twimg.com/media/${queryMediaSingle.filename}?format=${queryMediaSingle.extension}&name=orig">`
tweet.mediaObject.push(queryMediaSingle)
} else if (queryMediaSingle.source === 'cards' || queryMediaSingle.source === 'quote_status') {
tweet.mediaObject.push(queryMediaSingle)
}
}
}
//去重
tweet.mediaObject = [...new Set(tweet.mediaObject)]
}
tweet.tweet_id_str = String(tweet.tweet_id) //Number.MAX_SAFE_INTEGER => 9007199254740991 "9007199254740991".length => 16
tweet.uid_str = String(tweet.uid)
return tweet
}
const GenerateData = (tweets, isConversation = false, precheckUid = '', graphqlMode = false, req = null) => {
const tweetsInfo = TweetsInfo(tweets.data, graphqlMode)
if (tweetsInfo.errors.code !== 0) {
return { tweetsInfo: tweetsInfo, tweetsContent: [] }
}
let reverse = true
let tweetsContent = tweetsInfo.contents
.map((content) => {
if (!content) {
return false
}
if (['TimelineTimelineItem'].includes(content?.content?.entryType || content?.content?.__typename)) {
let tmpData = TweetsData(content, {}, [], '', graphqlMode, false)
if (tmpData.code === 200 && Object.keys(tmpData.data).length) {
tmpData.data.user_info = tmpData.userInfo
tmpData.data.retweet_user_info = tmpData.retweetUserInfo
return tmpData.data
}
return false
} else if (['TimelineTimelineModule', 'VerticalConversation'].includes(content?.content?.displayType)) {
if (content?.content?.displayType === 'TimelineTimelineModule') {
reverse = false
}
return content.content.items.map((item) => {
let tmpData = TweetsData(item, tweetsInfo.users, tweetsInfo.contents, precheckUid, graphqlMode, isConversation)
if (tmpData.code === 200 && Object.keys(tmpData.data).length) {
tmpData.data.user_info = tmpData.userInfo
tmpData.data.retweet_user_info = tmpData.retweetUserInfo
return tmpData.data
}
return false
})
} else {
let tmpData = TweetsData(content, tweetsInfo.users, tweetsInfo.contents, precheckUid, graphqlMode, isConversation)
if (tmpData.code === 200 && Object.keys(tmpData.data).length) {
tmpData.data.user_info = tmpData.userInfo
tmpData.data.retweet_user_info = tmpData.retweetUserInfo
return tmpData.data
}
}
return false
})
.flat()
.filter((tweet) => tweet?.tweet_id)
if (!reverse || isConversation) {
tweetsContent = tweetsContent.reverse() //sort((a, b) => b.tweet_id - a.tweet_id)
}
//rss content
const rss = new Rss()
//get account list
let tmpAccount
if (precheckUid) {
tmpAccount = tweetsContent.find((content) => content.uid === precheckUid)?.user_info || {}
}
const buildRssCursor = (url, tweet_id, cursor, top = false) => {
url.searchParams.set('tweet_id', String(tweet_id))
url.searchParams.set('cursor', String(cursor))
url.searchParams.set('refresh', top ? '1' : '0')
const tmpSearchParame = url.searchParams.toString()
return '/online/api/v3' + url.pathname + (tmpSearchParame ? '?' + tmpSearchParame : '')
}
rss.channel({
title: { text: tmpAccount?.name ? ` ${tmpAccount?.display_name} (@${tmpAccount?.name})` : 'Twitter Monitor Timeline', cdata: true },
link: { text: tmpAccount?.name ? 'https://twitter.com' : `https://twitter.com/${tmpAccount?.name || ''}`, cdata: false },
description: { text: tmpAccount?.description ? tmpAccount.description : 'Monitor timeline', cdata: true }, //TODOs
generator: { text: 'Twitter Monitor', cdata: false },
webMaster: { text: 'NEST.MOE', cdata: false },
language: { text: 'zh-cn', cdata: false },
lastBuildDate: {
text: new Date()
.toString()
.replaceAll(/\(.*\)/gm, '')
.trim(),
cdata: false
},
ttl: { text: 60, cdata: false },
...(tmpAccount?.header
? {
image: {
text: {
title: { text: `${tmpAccount?.display_name} (@${tmpAccount?.name})`, cdata: false },
link: { text: `https://twitter.com/${tmpAccount?.name}/`, cdata: false },
url: { text: `/media/proxy/${tmpAccount.header}`, cdata: false },
width: { text: 128, cdata: false },
height: { text: 128, cdata: false }
},
cdata: false
}
}
: {}),
...(req?.url?.searchParams
? {
topCursor: { text: buildRssCursor(new URL('http://localhost' + req.url), tweetsInfo.tweetRange.max, tweetsInfo.cursor.top, true), cdata: true },
bottomCursor: { text: buildRssCursor(new URL('http://localhost' + req.url), tweetsInfo.tweetRange.min, tweetsInfo.cursor.bottom, false), cdata: true }
}
: {})
})
for (const x in tweetsContent) {
const tmpImageText = tweetsContent[x].mediaObject
.map((media) => {
const tmpContent = `<img src="https://${media.url}" alt="${(media?.title || '') + (media?.description || '') || 'media'}" />`
return tmpContent
})
.join(' ')
rss.item({
title: { text: tweetsContent[x].full_text_original, cdata: true },
description: {
text: tweetsContent[x].full_text.replaceAll(/<a href="([^"]+)" id="([^"]+)"(| target="_blank")>([^<]+)<\/a>/gm, (...match) => (match[2] === 'url' ? match[1] : match[4])) + ' ' + tmpImageText,
cdata: true
},
pubDate: {
text: new Date(tweetsContent[x].time * 1000)
.toString()
.replaceAll(/\(.*\)/gm, '')
.trim(),
cdata: false
},
guid: { text: `https://twitter.com/${tweetsContent[x].name}/status/${tweetsContent[x].tweet_id}`, cdata: false },
link: { text: `https://twitter.com/${tweetsContent[x].name}/status/${tweetsContent[x].tweet_id}`, cdata: false },
author: { text: `${tweetsContent[x].retweet_from_name ? 'RT ' : ''}${tweetsContent[x].retweet_from || tweetsContent[x].display_name} (@${tweetsContent[x].retweet_from_name || tweetsContent[x].name})`, cdata: true }
})
}
return { tweetsInfo, tweetsContent, rssContent: rss.value }
}
export { ApiTweets, ApiSearch, ApiPoll, ApiAudioSpace, ApiBroadcast, ApiMedia, GenerateData }
================================================
FILE: apps/backend/CoreFunctions/online/OnlineUserInfo.mjs
================================================
import { GenerateAccountInfo } from '../../../../libs/core/Core.info.mjs'
import { getUserInfo } from '../../../../libs/core/Core.fetch.mjs'
import { Log, GetEntitiesFromText, VerifyQueryString } from '../../../../libs/core/Core.function.mjs'
import { apiTemplate } from '../../../../libs/share/Constant.mjs'
const ApiUserInfo = async (req, env) => {
const name = VerifyQueryString(req.query.name, '')
const uid = VerifyQueryString(req.query.uid, '0')
//TODO errors
if (!(name || uid)) {
return apiTemplate(404, 'No such account')
}
let userInfo = {}
try {
userInfo = await getUserInfo({ user: [name || uid, uid !== '0' && Number(uid) > 0 ? -2 : -3], guest_token: env.guest_token2, cookie: req.cookies, authorization: env.guest_token2.authorization })
//updateGuestToken
await env.updateGuestToken(env, 'guest_token2', env.guest_token2.authorization, userInfo.headers.get('x-rate-limit-remaining') < 1, !name ? 'UserByRestId' : 'UserByScreenName')
} catch (e) {
Log(false, 'error', `[${new Date()}]: #OnlineUserInfo #${name || uid} #${e.code} ${e.message}`)
return env.json(apiTemplate(e.code, e.message))
}
let { GeneralAccountData } = GenerateAccountInfo(userInfo.data, {
hidden: 0,
lockes: 0,
deleted: 0,
organization: 0
})
if (!GeneralAccountData.uid) {
return env.json(apiTemplate(404, 'No such account'))
}
if (GeneralAccountData.description) {
GeneralAccountData.description = GeneralAccountData.description.replaceAll('\n', '\n<br>')
}
GeneralAccountData.top = String(GeneralAccountData.top)
GeneralAccountData.header = GeneralAccountData.header.replaceAll(/http(|s):\/\//gm, '')
GeneralAccountData.uid_str = String(GeneralAccountData.uid)
//GeneralAccountData.uid = Number(GeneralAccountData.uid)
let originalTextAndEntities = GetEntitiesFromText(GeneralAccountData.description)
GeneralAccountData.description_original = originalTextAndEntities.originalText
GeneralAccountData.description_entities = originalTextAndEntities.entities
return env.json(apiTemplate(200, 'OK', GeneralAccountData))
}
export { ApiUserInfo }
================================================
FILE: apps/backend/CoreFunctions/translate/OnlineTranslate.mjs
================================================
import { getTranslate } from '../../../../libs/core/Core.fetch.mjs'
import { Log, VerifyQueryString } from '../../../../libs/core/Core.function.mjs'
import { Translate } from '../../../../libs/core/Core.translate.mjs'
import { apiTemplate } from '../../../../libs/share/Constant.mjs'
const ApiTranslate = async (req, env) => {
const target = VerifyQueryString(req.query.to, 'en')
const platform = VerifyQueryString(req.query.platform, 'google').toLowerCase()
const text = VerifyQueryString(req.postBody.get('text'), '')
if (text) {
let trInfo = { full_text: text, cache: false, target, translate_source: 'Twitter Monitor Translator', translate: '', entities: [] }
const { message, content } = await Translate(trInfo, target, platform)
if (!message) {
return env.json(apiTemplate(200, 'OK', content, 'translate'))
} else {
return env.json(apiTemplate(500, 'Unable to get translate content', content, 'translate'))
}
} else {
return env.json(apiTemplate(404, 'No translate text', {}, 'translate'))
}
}
const ApiOfficialTranslate = async (req, env) => {
const id = VerifyQueryString(req.query.id, '')
if (!id) {
return env.json(apiTemplate(403, 'Invalid id(tweet_id/uid)', {}, 'translate'))
}
const type = VerifyQueryString(req.query.type, 'tweets')
const target = VerifyQueryString(req.query.target, 'en')
try {
const tmpTranslate = await getTranslate({ id, type, target, guest_token: env.guest_token2 })
//updateGuestToken
await env.updateGuestToken(env, 'guest_token2', 4, tmpTranslate.headers.get('x-rate-limit-remaining') < 1, 'Translation')
if (tmpTranslate.data || (tmpTranslate.data?.translationState ?? '').toLowerCase() !== 'success') {
let tmpReaponse = {
full_text: tmpTranslate.data.translation,
translate: tmpTranslate.data.translation,
cache: false,
target: tmpTranslate.data.destinationLanguage,
translate_source: tmpTranslate.data.translationSource + ' (for Twitter)',
entities: Object.keys(tmpTranslate.data.entities)
.map((key) =>
tmpTranslate.data.entities[key].map((value) => ({
expanded_url: ((key, value) => {
switch (key) {
case 'hashtags':
return `#/hashtag/${value.text}`
case 'symbols':
return `#/cashtag/${value.text}`
case 'user_mentions':
return `https://twitter.com/${value.screen_name}`
case 'urls':
return value.expanded_url
default:
return ''
}
})(key, value),
indices_end: value.indices[1],
indices_start: value.indices[0],
text: value.text ?? value.display_url ?? `@${value.screen_name}` ?? '',
type: key.slice(0, -1)
}))
)
.flat()
.sort((a, b) => a.indices_start - b.indices_start)
}
return env.json(apiTemplate(200, 'OK', tmpReaponse, 'translate'))
} else {
return env.json(apiTemplate(500, 'Unable to get translate content', {}, 'translate'))
}
} catch (e) {
Log(false, 'error', e)
return env.json(apiTemplate(500, 'Unable to get translate content', {}, 'translate'))
}
}
export { ApiTranslate, ApiOfficialTranslate }
================================================
FILE: apps/backend/CoreFunctions/translate/Translate.mjs
================================================
import { VerifyQueryString } from '../../../../libs/core/Core.function.mjs'
import { Translate } from '../../../../libs/core/Core.translate.mjs'
import { apiTemplate } from '../../../../libs/share/Constant.mjs'
const ApiPredict = async (req, res) => {
const text = VerifyQueryString(req.query.text, '')
if (!text) {
res.json(apiTemplate(403, 'Empty content', {}, 'translate'))
} else {
if (!global.LanguageIdentification) {
res.json(apiTemplate(500, 'Unable to load the cld3 model', {}, 'translate'))
} else {
const identifier = global.LanguageIdentification.create(0, 1000)
const tmpLang = identifier.findLanguage(text)
identifier.dispose()
res.json(apiTemplate(200, 'OK', tmpLang, 'translate'))
}
}
}
export { ApiPredict }
================================================
FILE: apps/backend/app.mjs
================================================
import express from 'express'
import { Log, GuestToken, GuestAccount } from '../../libs/core/Core.function.mjs'
import { apiTemplate } from '../../libs/share/Constant.mjs'
import { basePath } from '../../libs/share/NodeConstant.mjs'
import { loadModule } from 'cld3-asm'
//Online api
import online from './service/online.mjs'
import album from './service/album.mjs'
import translate from './service/translate.mjs'
import { json, xml, updateGuestToken, ResponseWrapper, mediaExistPreCheck, mediaCacheSave } from './share.mjs'
import { existsSync, readFileSync } from 'fs'
import { resolve } from 'path'
//settings
global.dbmode = false
let settingsFile = ''
let EXPRESS_PORT = 3000
let EXPRESS_HOST = '0.0.0.0'
let EXPRESS_ALLOW_ORIGIN = ['*']
let STATIC_PATH = ''
let ACTIVE_SERVICE = []
let GUEST_ACCOUNT_HANDLE = new GuestAccount()
let AUDIO_SPACE_CACHE = {}
for (const argvContent of process.argv.slice(2)) {
if (argvContent === 'dbmode') {
global.dbmode = true
} else if (argvContent.startsWith('--config=')) {
settingsFile = argvContent.replace('--config=', '')
} else if (argvContent === '--noSettings') {
settingsFile = ''
}
}
if (settingsFile && existsSync(settingsFile)) {
const settings = await import(settingsFile)
EXPRESS_PORT = settings.EXPRESS_PORT
EXPRESS_HOST = settings.EXPRESS_HOST
EXPRESS_ALLOW_ORIGIN = settings.EXPRESS_ALLOW_ORIGIN
STATIC_PATH = settings.STATIC_PATH
ACTIVE_SERVICE = settings.ACTIVE_SERVICE
if (settings.GUEST_ACCOUNT_HANDLE && Array.isArray(settings.GUEST_ACCOUNT_HANDLE) && settings.GUEST_ACCOUNT_HANDLE.length > 0) {
GUEST_ACCOUNT_HANDLE.AddNewAccounts(false, settings.GUEST_ACCOUNT_HANDLE)
}
if (settings.GUEST_ACCOUNT_POOL && settings.GUEST_ACCOUNT_POOL_TOKEN) {
GUEST_ACCOUNT_HANDLE.UpdatePoolLink(`${settings.GUEST_ACCOUNT_POOL}/data/random?count=5&key=${settings.GUEST_ACCOUNT_POOL_TOKEN}`)
}
}
// guest accounts
if (existsSync(basePath + '/../guest_accounts.json')) {
GUEST_ACCOUNT_HANDLE.AddNewAccounts(false, JSON.parse(readFileSync(basePath + '/../guest_accounts.json').toString()))
} else if (existsSync(resolve('.') + '/guest_accounts.json')) {
GUEST_ACCOUNT_HANDLE.AddNewAccounts(false, JSON.parse(readFileSync(resolve('.') + '/guest_accounts.json').toString()))
}
// get guest account from guest account pool
if (GUEST_ACCOUNT_HANDLE.Link) {
await GUEST_ACCOUNT_HANDLE.GetNewAccountsByRemote(true)
setInterval(
async () => {
await GUEST_ACCOUNT_HANDLE.GetNewAccountsByRemote(true)
GUEST_ACCOUNT_HANDLE.RemoveUselessAccounts()
},
1000 * 60 * 60 * 8
) // per 8 hours
}
const app = express()
// const media = express()
const port = EXPRESS_PORT
const host = EXPRESS_HOST
app.use(express.urlencoded({ extended: false }))
app.use(express.json())
//get init token
// userinfo, tweet_result_by_id, broadcast, live_stream, following, followers, onbroading
global.guest_token = new GuestToken(4)
// others
global.guest_token3 = new GuestToken('android')
//for search and album
//global.guest_token3 = new GuestToken('android')
//if (!global.dbmode) {
// //await global.guest_token.updateGuestToken(0)
// await global.guest_token2.updateGuestToken(1)
//}
app.use((req, res, next) => {
req.env = {
json,
xml,
updateGuestToken,
ResponseWrapper,
mediaExistPreCheck,
mediaCacheSave,
guest_token2_handle: global.guest_token,
guest_token2: {},
guest_token3_handle: global.guest_token3,
guest_token3: {},
guest_accounts: GUEST_ACCOUNT_HANDLE,
audio_apsce_cache: AUDIO_SPACE_CACHE
}
res.setHeader('X-Powered-By', 'Twitter Monitor Api')
if (EXPRESS_ALLOW_ORIGIN && req.headers.referer) {
const origin = new URL(req.headers.referer).origin
const tmpReferer = EXPRESS_ALLOW_ORIGIN.includes('*') ? '*' : EXPRESS_ALLOW_ORIGIN.includes(origin) ? origin : ''
if (tmpReferer) {
res.append('Access-Control-Allow-Origin', tmpReferer)
}
}
res.append('Access-Control-Allow-Methods', '*')
res.append('Access-Control-Allow-Credentials', 'true')
next()
})
//local api
// if (ACTIVE_SERVICE.includes('tmv1')) {
// const { default: legacy } = await import('./service/legacy.mjs')
// app.use('/api/v1', legacy)
// }
// if (ACTIVE_SERVICE.includes('twitter_monitor')) {
// const { default: local } = await import('./service/local.mjs')
// app.use('/api/v3', local)
// }
//translate api
app.use('/translate', translate)
//proxy api
app.use('/online/api/v3', online)
app.use('/album', album)
// app.use('/media', media)
// media.use((req, res, next) => {
// if (global.dbmode) {
// res.json(apiTemplate(403, 'DB Mode is not included media proxy api'))
// return
// }
// next()
// })
//LanguageIdentification
global.LanguageIdentification = await loadModule()
Log(false, 'log', 'tmv3: Enabled language identification service')
//media proxy
// media.use(
// '/cache',
// express.static(basePath + '/../apps/backend/cache', {
// setHeaders: function (res, path, stat) {
// res.set('X-TMCache', 1)
// }
// })
// )
// media.get(/(proxy)\/(.*)/, async (req, res) => {
// req.params.link = req.params?.[1] || ''
// const _res = await MediaProxy(req, req.env)
// for (const header of [..._res.headers]) {
// res.setHeader(header[0], header[1])
// }
// switch (_res.status) {
// case 301:
// case 302:
// case 307:
// res.status(_res.status).redirect(_res.data)
// break
// case 200:
// if (_res.data?.pipe) {
// _res.data.pipe(res)
// } else {
// res.send(_res.data)
// }
// break
// default:
// res.status(_res.status).end()
// }
// })
// app.get(/^\/(ext_tw_video|amplify_video)\/(.*)/, async (req, res) => {
// req.params.link = req.params?.[1] || ''
// const _res = await MediaProxy(req, req.env)
// for (const header of [..._res.headers]) {
// res.setHeader(header[0], header[1])
// }
// switch (_res.status) {
// case 301:
// case 302:
// case 307:
// res.status(_res.status).redirect(_res.data)
// break
// case 200:
// if (_res.data?.pipe) {
// _res.data.pipe(res)
// } else {
// res.send(_res.data)
// }
// break
// default:
// res.status(_res.status).end()
// }
// }) //for m3u8
//global static file
// if (STATIC_PATH) {
// app.use('/static', express.static(STATIC_PATH))
// }
//robots.txt
app.all('/robots.txt', (req, res) => {
res.type('txt').send('User-agent: *\nDisallow: /*')
})
//error control
app.all('/{*splat}', (req, res) => {
res.status(403).json(apiTemplate(403, 'Invalid Request', {}, 'global_api'))
})
app.use((err, req, res, next) => {
Log(false, 'error', new Date(), err)
res.status(500).json(apiTemplate(500, 'Unknown error', {}, 'global_api'))
})
app.listen(port, host, () => {
Log(false, 'log', `V3Api listening on port ${host}:${port}`)
})
================================================
FILE: apps/backend/service/album.mjs
================================================
import express from 'express'
import { ApiUserInfo } from '../CoreFunctions/online/OnlineUserInfo.mjs'
import { ApiTweets } from '../CoreFunctions/online/OnlineTweet.mjs'
import { AlbumSearch } from '../CoreFunctions/album/Album.mjs'
import { apiTemplate } from '../../../libs/share/Constant.mjs'
import { Log } from '../../../libs/core/Core.function.mjs'
const album = express()
album.use(async (req, res, next) => {
if (global.dbmode) {
res.json(apiTemplate(403, 'DB Mode is not included album api'))
return
}
await req.env.guest_token2_handle.updateGuestToken(4)
// await req.env.guest_token3_handle.openAccountInit(req.env.guest_accounts.RandomItem)
if (req.env.guest_token2_handle.token.nextActiveTime) {
Log(false, 'error', `[${new Date()}]: #Album #GuestToken #429 Wait until ${req.env.guest_token2_handle.token.nextActiveTime}`)
res.json(apiTemplate(429, `Wait until ${req.env.guest_token2_handle.token.nextActiveTime}`), {}, 'album')
} else {
req.env.guest_token2 = req.env.guest_token2_handle.token
// req.env.guest_token3 = req.env.guest_token3_handle.token
next()
}
})
//album
album.get('/data/userinfo/', async (req, res) => {
const _res = await ApiUserInfo(req, req.env)
res.status(_res.status).json(_res.data)
})
album.get('/data/tweets/', async (req, res) => {
const _res = await ApiTweets(req, req.env)
res.status(_res.status).json(_res.data)
})
album.get('/data/list/', async (req, res) => {
const _res = await AlbumSearch(req, req.env)
res.status(_res.status).json(_res.data)
})
export default album
================================================
FILE: apps/backend/service/online.mjs
================================================
import express from 'express'
import { ApiUserInfo } from '../CoreFunctions/online/OnlineUserInfo.mjs'
import { ApiTweets, ApiSearch, ApiPoll, ApiAudioSpace, ApiMedia, ApiBroadcast } from '../CoreFunctions/online/OnlineTweet.mjs'
import { apiTemplate } from '../../../libs/share/Constant.mjs'
import { ApiTrends } from '../CoreFunctions/online/OnlineTrends.mjs'
import { ApiCommunityInfo, ApiCommunitySearch, ApiListInfo, ApiListMemberList, ApiTypeahead } from '../CoreFunctions/online/OnlineMisc.mjs'
import { ApiLoginFlow, ApiLogout } from '../CoreFunctions/online/OnlineLogin.mjs'
import cookieParser from 'cookie-parser'
import { Log } from '../../../libs/core/Core.function.mjs'
const online = express()
online.use(cookieParser())
online.use(async (req, res, next) => {
if (global.dbmode) {
res.json(apiTemplate(403, 'DB Mode is not included onlone api'))
return
}
//await global.guest_token2.updateGuestToken(0)
await req.env.guest_token2_handle.updateGuestToken(4)
// await req.env.guest_token3_handle.openAccountInit(req.env.guest_accounts.RandomItem)
//if (global.guest_token2.token.nextActiveTime) {
// Log(false, 'error', `[${new Date()}]: #Online #GuestToken #429 Wait until ${global.guest_token2.token.nextActiveTime}`)
// res.json(apiTemplate(429, `Wait until ${global.guest_token2.token.nextActiveTime}`))
//} else
if (req.env.guest_token2_handle.token.nextActiveTime) {
Log(false, 'error', `[${new Date()}]: #Online #GuestToken #429 Wait until ${req.env.guest_token2_handle.token.nextActiveTime}`)
res.json(apiTemplate(429, `Wait until ${req.env.guest_token2_handle.token.nextActiveTime}`))
} else {
req.env.guest_token2 = req.env.guest_token2_handle.token
// req.env.guest_token3 = req.env.guest_token3_handle.token
next()
}
})
// online api
online.get('/data/accounts/', (req, res) => {
res.json(apiTemplate(200, 'OK'))
})
online.get('/data/userinfo/', async (req, res) => {
const _res = await ApiUserInfo(req, req.env)
res.json(_res.data)
})
online.get('/data/tweets/', async (req, res) => {
const _res = await ApiTweets(req, req.env)
if (_res.format === 'xml') {
res.append('content-type', 'application/xml;charset=UTF-8')
res.setHeader('Access-Control-Allow-Origin', '*')
res.send(_res.data)
} else {
res.json(_res.data)
}
})
online.get('/data/chart/', (req, res) => {
res.json(apiTemplate(200, 'No record found', []))
})
online.get(['/data/hashtag', '/data/cashtag', '/data/search'], async (req, res) => {
req.type = req?._parsedUrl?.pathname?.split('/')?.filter(x => x)?.pop() || ''
const _res = await ApiSearch(req, req.env)
if (_res.format === 'xml') {
res.append('content-type', 'application/xml;charset=UTF-8')
res.setHeader('Access-Control-Allow-Origin', '*')
res.send(_res.data)
} else {
res.json(_res.data)
}
//res.json(apiTemplate(404, 'Search endpoint is not yet avaliable', {}, 'online'))
})
online.get('/data/poll/', async (req, res) => {
const _res = await ApiPoll(req, req.env)
res.json(_res.data)
})
online.get('/data/audiospace/', async (req, res) => {
const _res = await ApiAudioSpace(req, req.env)
res.json(_res.data)
})
online.get('/data/broadcast/', async (req, res) => {
const _res = await ApiBroadcast(req, req.env)
res.json(_res.data)
})
online.get('/data/media/', async (req, res) => {
const _res = await ApiMedia(req, req.env)
res.json(_res.data)
})
online.get('/data/trends/', async (req, res) => {
const _res = await ApiTrends(req, req.env)
res.json(_res.data)
})
online.get('/data/typeahead/', async (req, res) => {
const _res = await ApiTypeahead(req, req.env)
res.json(_res.data)
})
online.get('/data/listinfo/', async (req, res) => {
const _res = await ApiListInfo(req, req.env)
res.json(_res.data)
})
online.get('/data/listmember/', async (req, res) => {
const _res = await ApiListMemberList(req, req.env)
res.json(_res.data)
})
online.get('/data/communityinfo/', async (req, res) => {
const _res = await ApiCommunityInfo(req, req.env)
res.json(_res.data)
})
online.get('/data/communitysearch/', async (req, res) => {
const _res = await ApiCommunitySearch(req, req.env)
res.json(_res.data)
})
// cookie required
online.post('/account/taskflow/', async (req, res) => {
req.postBody = new Map(Object.entries(req.body))
//Log(false, 'log', req.body)
const _res = await ApiLoginFlow(req, req.env)
for (const header of [..._res.headers]) {
res.append(header[0], header[1])
}
res.json(_res.data)
})
online.post('/account/logout/', async (req, res) => {
const _res = await ApiLogout(req, req.env)
for (const header of [..._res.headers]) {
res.append(header[0], header[1])
}
res.json(_res.data)
})
export default online
================================================
FILE: apps/backend/service/translate.mjs
================================================
import express from 'express'
import { ApiPredict } from '../CoreFunctions/translate/Translate.mjs'
import { ApiOfficialTranslate, ApiTranslate } from '../CoreFunctions/translate/OnlineTranslate.mjs'
const translate = express()
//translate
// translate.get('/local/', ApiLocalTranslate)
translate.post('/online/', async (req, res) => {
req.postBody = new Map(Object.entries(req.body))
const _res = await ApiTranslate(req, req.env)
res.json(_res.data)
})
translate.get('/predict/', ApiPredict)
//translate.get('/', async (req, res) => {
// const _res = await ApiOfficialTranslate(req, req.env)
// res.json(_res.data)
//})
export default translate
================================================
FILE: apps/backend/share.mjs
================================================
import { existsSync, writeFileSync } from 'fs'
import { basePath } from '../../libs/share/NodeConstant.mjs'
import { Log } from '../../libs/core/Core.function.mjs'
const json = (data, status = 200) => ({
status,
data,
format: 'json'
})
const xml = (data, status = 200) => ({
status,
data,
format: 'xml'
})
const updateGuestToken = async (env, k, tokenType = 0, update = true, type = '') => {
if (update) {
env[`${k}_handle`].updateRateLimit(type, 0)
} else if (type) {
env[`${k}_handle`].updateRateLimit(type)
}
return {}
}
const ResponseWrapper = (data, status = 403, headers = new Headers()) => ({
data,
status,
headers
})
const mediaExistPreCheck = (name = '') => existsSync(`${basePath}/../apps/backend/cache/${name}`)
const mediaCacheSave = (data, name) => {
try {
writeFileSync(`${basePath}/../apps/backend/cache/${name}`, data)
} catch (e) {
Log(false, 'error', `Cache: #Cache error`, e)
}
}
export { json, xml, updateGuestToken, ResponseWrapper, mediaExistPreCheck, mediaCacheSave }
================================================
FILE: apps/backend/static/.gitkeep
================================================
================================================
FILE: apps/backend/static/xml/rss.xsl
================================================
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title><xsl:value-of select="/rss/channel/title"/></title>
<meta charset="UTF-8" />
<meta http-equiv="x-ua-compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1,shrink-to-fit=no" />
<style type="text/css">
.main{
margin: 2rem 1rem;
}
@media screen and (min-width:640.01px){
.main{
margin: 2rem 5rem;
}
}
@media screen and (min-width:768.01px){
.main{
margin: 2rem 10rem;
}
}
@media screen and (min-width:1024.01px){
.main{
margin: 2rem 16rem;
}
}
</style>
</head>
<body class="main">
<header>
<h1>Twitter Monitor RSS</h1>
<a hreflang="en" target="_blank">
<xsl:attribute name="href">
<xsl:value-of select="/rss/channel/link"/>
</xsl:attribute>
<h2>
<xsl:value-of select="/rss/channel/title"/>
→
</h2>
</a>
<p>
<xsl:value-of select="/rss/channel/description"/>
</p>
</header>
<main>
<hr />
<a hreflang="en">
<xsl:attribute name="href">
<xsl:value-of select="/rss/channel/topCursor"/>
</xsl:attribute>
<h3>↑ Newer ↑</h3>
</a>
<xsl:for-each select="/rss/channel/item">
<hr />
<article>
<h3><xsl:value-of select="author"/></h3>
<p><xsl:value-of select="title"/></p>
<footer>
Published:
<time>
<xsl:value-of select="pubDate" />
</time><br />
<a hreflang="en" target="_blank">
<xsl:attribute name="href">
<xsl:value-of select="link"/>
</xsl:attribute>
View tweet →
</a>
</footer>
</article>
</xsl:for-each>
<hr />
<a hreflang="en">
<xsl:attribute name="href">
<xsl:value-of select="/rss/channel/bottomCursor"/>
</xsl:attribute>
<h3>↓ More ↓</h3>
</a>
</main>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
================================================
FILE: apps/online_tools/config.html
================================================
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta name="robots" content="nofollow">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="Banka2017 (https://nest.moe)">
<meta name="description" content="Twitter Monitor Config">
<title>Twitter Monitor Config</title>
<!-- CSS only -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css"
crossorigin="anonymous">
</head>
<body>
<div id="app">
<nav class="navbar navbar-expand-lg navbar-light text-center bg-light "
style="display: flex; justify-content: space-between;">
<span class="navbar-brand mb-0 h1">Twitter Monitor Tools</span>
<div class="btn-group" role="group">
<button
:class="{btn: true, 'btn-sm': true, 'btn-primary': lang === 'zh', 'btn-outline-primary': lang !== 'zh'}"
type="button" @click="lang='zh'">
中
</button>
<button
:class="{btn: true, 'btn-sm': true, 'btn-primary': lang === 'en', 'btn-outline-primary': lang !== 'en'}"
type="button" @click="lang='en'">
En
</button>
</div>
</nav>
<div class="my-4"></div>
<div class="container">
<div class="row">
<div class="col-md-8">
<h4>{{getI18n('createConfigFile')}}</h4>
<nav>
<div class="nav nav-tabs" id="nav-tab" role="tablist">
<span :class="{'nav-item': true, 'nav-link': true, active: activeTab === 'user'}"
style="cursor: pointer;" @click="activeTab = 'user'" id="config-user-tab"
data-toggle="tab" role="tab" aria-controls="config-user"
aria-selected="true">{{getI18n('account')}} <span class="badge badge-light">{{
realUserListLength + '/' + config.users.length }}</span></span>
<span :class="{'nav-item': true, 'nav-link': true, active: activeTab === 'url'}"
style="cursor: pointer;" @click="activeTab = 'url'" id="config-url-tab"
data-toggle="tab" role="tab" aria-controls="config-url"
aria-selected="false">{{getI18n('link')}} <span class="badge badge-light">{{
config.links.length }}</span></span>
</div>
</nav>
<div class="tab-content" id="nav-tabContent">
<div :class="{'tab-pane': true, fade: true, show: activeTab === 'user', active: activeTab === 'user'}"
id="config-user" role="tabpanel" aria-labelledby="config-user-tab">
<template id="nameList">
<div class="my-4"></div>
<template v-for="(info, index) in userList" :key="index">
<a role="button"
:class="`text-decoration-none badge badge-pill badge-` + (info.nsfw ? 'warning' : (info.organization ? 'success' : 'primary'))"
:href="`#item` + index">{{ info.display_name }}</a>
<span></span>
</template>
<div class="my-4"></div>
</template>
<div v-for="(user, s) in config.users" :id="`item` + s">
<div
style="position: sticky; top: 0; background-color: white; padding: 1em 0; z-index: 9999; display: flex; justify-content: space-between;">
<span style="font-size: large; font-weight: 700;">{{user.display_name}}</span>
<span style="font-size: small; font-weight: 500;">{{user.name ? '@' + user.name :
''}}</span>
</div>
<div class="form-group">
<label :for="`user`+s+`name`"
:style="user.name ? '' : 'color: orange'">{{getI18n('id')}}</label>
<input type="text" class="form-control" aria-describedby="idHelp"
v-model="config.users[s].name" :id="`user`+s+`name`">
<small id="idHelp"
class="form-text text-muted">{{getI18n('twitterScreenName')}}</small>
</div>
<div class="form-group">
<label :for="`user`+s+`display_name`"
:style="(!user.display_name && !user.name) ? 'color: red' : ''">{{getI18n('displayName')}}</label>
<input type="text" class="form-control" aria-describedby="display_nameHelp"
v-model="config.users[s].display_name" :id="`user`+s+`display_name`">
<small id="display_nameHelp"
class="form-text text-muted">{{getI18n('keepEmptyToUseAccountName')}}</small>
</div>
<div class="form-group" v-if="user.uid">
<label :for="`user`+s+`uid`">{{getI18n('uid')}}</label>
<input type="text" class="form-control" aria-describedby="uidHelp"
v-model="config.users[s].uid" :id="`user`+s+`uid`">
<small id="uidHelp" class="form-text text-muted">{{getI18n('twitterUid')}}</small>
</div>
<div v-if="user.projects.length">
<template v-for="(project, ss) in config.users[s].projects">
<div class="input-group">
<input type="text" class="form-control"
v-model="config.users[s].projects[ss][0]"
:placeholder="getI18n('firstPath')">
<div class="input-group-append" :id="`project`+ss">
<span class="input-group-text">-></span>
</div>
<input type="text" class="form-control input-group-append"
v-model="config.users[s].projects[ss][1]"
:placeholder="getI18n('secondPath')">
<div class="input-group-append" :id="`project`+ss">
<button class="btn btn-outline-danger"
@click="action('del', 'project', s, ss)"
type="button">{{getI18n('delete')}}</button>
</div>
</div>
<div class="my-4"></div>
</template>
</div>
<template
v-for="(checkInfo, checkType) in {hidden: getI18n('hideAccount'), deleted: getI18n('deletedAccount'), locked: getI18n('protectedAccount'), organization: getI18n('organizationAccount'), not_analytics: getI18n('notForAnalytic'), nsfw: getI18n('nsfw')}">
<div class="form-check mb-2">
<input type="checkbox" class="form-check-input" :id="`user`+s+checkType"
v-model="config.users[s][checkType]">
<label class="form-check-label" :for="`user`+s+checkType">{{ checkInfo
}}</label>
</div>
</template>
<button class="btn btn-primary mx-1"
@click="action('add', 'project', s)">{{getI18n('addPath')}}</button>
<button
:class="`btn btn-primary mx-1`+((!user.display_name && !user.name) ? ' disabled' : '')"
@click="(!user.display_name && !user.name) ? '' : action('add', 'users', s)">{{getI18n('addAccount')}}</button>
<button class="btn btn-outline-danger mx-1" @click="action('del', 'users', s)"
v-if="config.users.length > 1">{{getI18n('deleteAccount')}}</button>
<hr class="my-4">
</div>
</div>
<div :class="{'tab-pane': true, fade: true, show: activeTab === 'url', active: activeTab === 'url'}"
id="config-url" role="tabpanel" aria-labelledby="config-url-tab">
<div v-for="(url, s) in config.links">
<div class="form-group">
<label :for="`url`+s+`url`">url <small class="text-muted"
v-if="!(/(http|https|ftp):\/\/[^\.]+\..*/gm.test(url.url))"><span
v-html="getI18n('useRouterLink')"></span></small></label>
<input type="text" class="form-control" v-model="config.links[s].url"
:id="`url`+s+`url`">
</div>
<div class="form-group">
<label :for="`url`+s+`display`">{{getI18n('alias')}}</label>
<input type="text" class="form-control" v-model="config.links[s].display"
:id="`url`+s+`display`">
</div>
<!--<div class="form-group">-->
<!--<label :for="`url`+s+`badgeClass`">类型</label>-->
<!--<select :id="`url`+s+`badgeClass`" v-model="config.links[s].badgeClass" class="form-control">-->
<!--<option :value="badgeClass[0]" v-for="badgeClass in [['primary', 'Primary'], ['secondary', 'Secondary'], ['success', 'Success'], ['danger', 'Danger'], ['warning', 'Warning'], ['info', 'Info'], ['light', 'Light'], ['dark', 'Dark']]">{{ badgeClass[1] }}</option>-->
<!--</select>-->
<!--</div>-->
<button
:class="`btn btn-primary`+(/(http|https|ftp):\/\/[^\.]+\..*/gm.test(url.url) ? '' : ' disabled')"
@click="/(http|https|ftp):\/\/[^\.]+\..*/gm.test(url.url) ? action('add', 'links', config.links.length) : ''"
v-if="s+1==config.links.length">{{getI18n('add')}}</button> <button
class="btn btn-outline-danger"
@click="action('del', 'links', s)">{{getI18n('delete')}}</button>
<hr class="my-4">
</div>
<template v-if="config.links.length===0">
<div class="my-4"></div>
<button class="btn btn-primary"
@click="action('add', 'links')">{{getI18n('add')}}</button>
</template>
</div>
</div>
</div>
<div class="col-md-4">
<div style="position: sticky; top: 1.5rem;">
<!--生成的数据-->
<label for="uploadFile">{{getI18n('importFromFile')}}</label>
<div class="custom-file">
<input type="file" class="custom-file-input" id="uploadFile" lang="zh"
@change="jsonFileChange" accept="application/json">
<label class="custom-file-label" for="uploadFile">config.json</label>
</div>
<div class="my-4"></div>
<button class="btn btn-primary btn-block"
@click="download('config.json', JSON.stringify(config))">{{getI18n('downloadConfig')}}</button>
<div class="my-4"></div>
<div class="input-group">
<textarea class="form-control" rows="10" v-model="textareaData"></textarea>
</div>
<div class="text-center my-2">
>_ Twitter Monitor
</div>
</div>
</div>
</div>
</div>
<button type="button" class="btn btn-primary fixed-button"
style="opacity: 0.8;position: fixed;bottom: 0;right: 0" @click='toTop'>
<svg class="bi bi-chevron-up" width="25" height="25" viewBox="0 0 20 20" fill="currentColor"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9.646 6.646a.5.5 0 01.708 0l6 6a.5.5 0 01-.708.708L10 7.707l-5.646 5.647a.5.5 0 01-.708-.708l6-6z"
clip-rule="evenodd" />
</svg>
</button>
</div>
<!--load css and js-->
<script src="https://cdn.jsdelivr.net/npm/vue@3.4.0"></script>
<script>
const { createApp } = Vue
createApp({
data: () => ({
activeTab: 'user',//user url
lang: window.navigator.language.startsWith('zh') ? 'zh' : 'en',
config: {
users: [
{
name: "Example_user",
display_name: "Example user",
hidden: false,
deleted: false,
locked: false,
uid: "",
organization: false,
not_analytics: false,
nsfw: false,
projects: [
["project1", "tag1"],
],
}
],
links: [
{
url: "https://example.com",
display: "Example Domain",
//badgeClass: "primary",
}
]
},
Templates: {
users: {
name: "",
display_name: "",
hidden: false,
deleted: false,
locked: false,
uid: "",
organization: false,
not_analytics: false,
nsfw: false,
projects: [["project1", "tag1"]],
},
links: {
url: "",
display: "",
//badgeClass: "primary",
}
},
textareaData: "",
textareaSettings: {
copy: false,
style: false,
},
projects: [],
i18n: {
zh: {
createConfigFile: "创建配置文件 Config.json",
account: "帐号",
link: "链接",
id: "id",
twitterScreenName: "Twitter帐号id",
displayName: "显示名称",
keepEmptyToUseAccountName: "留空则使用帐号名称",
uid: "UID",
twitterUid: "Twitter帐号uid,无需理会",
firstPath: "一级目录",
secondPath: "二级目录",
add: "增加",
delete: "删除",
hideAccount: "隐藏帐号",
deletedAccount: "帐号已删除",
protectedAccount: "推文已被保护",
organizationAccount: "机构帐号",
notForAnalytic: "不统计数据",
nsfw: "NSFW",
addPath: "添加目录",
addAccount: "新增帐号",
deleteAccount: "删除帐号",
alias: "别名",
importFromFile: "导入配置文件",
downloadConfig: "下载配置",
type: "类型",
useRouterLink: "使用 <code><router-link></code>"
},
en: {
createConfigFile: "create Config.json",
account: "Account",
link: "Link",
id: "id",
twitterScreenName: "Twitter screen name",
displayName: "Display name",
keepEmptyToUseAccountName: "Keep empty to use account name",
uid: "UID",
twitterUid: "Twitter uid, added by crawler",
firstPath: "First path",
secondPath: "Second path",
add: "Add",
delete: "Delete",
hideAccount: "Hidden account",
deletedAccount: "Deleted account",
protectedAccount: "Protected Account",
organizationAccount: "Organization account",
notForAnalytic: "Not for analytic",
nsfw: "NSFW",
addPath: "Add path",
addAccount: "Add account",
deleteAccount: "Delete account",
alias: "Alias",
importFromFile: "Import config from file",
downloadConfig: "Download",
type: "Type",
useRouterLink: "Use <code><router-link></code>"
}
}
}),
computed: {
userList: function () {
return this.config.users.map(x => { return { name: x.name, display_name: x.display_name, organization: x.organization, nsfw: x.nsfw } })
},
realUserListLength: function () {
let realUsersLength = 0;
this.config.users.map(x => {
if (x.name !== "" && !x.deleted && !x.locked) {
realUsersLength++
}
})
return realUsersLength
},
},
watch: {
"config": {
handler: function () {
this.textareaData = JSON.stringify(this.config, null, 4);
},
deep: true,
},
"textareaData": function () {
if (this.textareaData) {
try {
let textareaDataArray = JSON.parse(this.textareaData);
if (textareaDataArray !== this.config && (textareaDataArray.users && textareaDataArray.links)) {
textareaDataArray.users.map(user => { user.uid = user.uid !== undefined ? String(user.uid) : ""; return user })
this.config = textareaDataArray;
}
} catch {
//console.log('error');
}
}
}
},
mounted: function () {
if (localStorage.getItem('twitter_monitor_config')) {
this.config = JSON.parse(localStorage.getItem('twitter_monitor_config'));
this.autoSave();
}
this.textareaData = JSON.stringify(this.config, null, 4);
},
methods: {
toTop: function () {
window.scrollTo({ top: 0, behavior: "smooth" })
},
action: function (action, where = "users", l1 = 0, l2 = 0) {
if (where) {
switch (action) {
case "add":
switch (where) {
case 'project':
this.config.users[l1].projects = this.config.users[l1].projects.concat([["", ""]]);
break;
default:
this.config[where] = (l1 === this.config[where].length) ? this.config[where].concat(this.Templates[where]) : this.config[where].slice(0, l1 + 1).concat(this.Templates[where]).concat(this.config[where].slice(l1 + 1));
}
break;
case "del":
switch (where) {
case 'project':
this.config.users[l1].projects.splice(l2, 1);
break;
default:
this.config[where].splice(l1, 1);
}
break;
}
}
},
jsonFileChange: function (e) {
let oFReader = new FileReader();
let oFile = document.getElementById("uploadFile").files[0];
oFReader.readAsText(oFile);
oFReader.onload = (e) => {
try {
this.config = JSON.parse(oFReader.result);
} catch {
console.log('文件不可解析');
}
}
},
download: function (filename, text) {
let element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
},
autoSave: function () {
localStorage.setItem('twitter_monitor_config', JSON.stringify(this.config));
setTimeout(() => { this.autoSave() }, 30000);//每30秒保存
},
getI18n: function (key = '') {
if (this.i18n[this.lang][key]) {
return this.i18n[this.lang][key]
} else {
return ''
}
}
}
}).mount('#app')
</script>
</body>
</html>
================================================
FILE: apps/online_tools/oauth_signature_builder.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="robots" content="nofollow">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="Banka2017 (https://nest.moe)">
<meta name="description" content="Twitter OAuth Signature Builder">
<title>Twitter OAuth Signature Builder</title>
<!-- CSS only -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css"
crossorigin="anonymous">
</head>
<body>
<div id="app">
<nav class="navbar navbar-expand-lg navbar-light text-center bg-light px-2">
<span class="navbar-brand mb-0 h1">Twitter Monitor Tools</span>
</nav>
<div class="my-4"></div>
<div class="container">
<div class="row">
<div class="col-md-8">
<h4>OAuth tools</h4>
<div class="mb-3">
<label class="form-label" for="oauth_consumer_key">oauth_consumer_key</label>
<div class="input-group">
<input type="text" class="form-control" id="oauth_consumer_key"
v-model="oauth_consumer_key">
</div>
<div class="form-text" id="form-text-oauth_consumer_key">From Android Client</div>
</div>
<div class="mb-3">
<label class="form-label" for="oauth_consumer_secret">oauth_consumer_secret</label>
<div class="input-group">
<input id="oauth_consumer_secret" type="text" class="form-control"
v-model="oauth_consumer_secret">
</div>
<div class="form-text" id="form-text-oauth_consumer_secret">From Android Client</div>
</div>
<div class="mb-3">
<label class="form-label" for="oauth_token">oauth_token</label>
<div class="input-group">
<input id="oauth_token" type="text" class="form-control" v-model="oauth_token">
</div>
</div>
<div class="mb-3">
<label class="form-label" for="oauth_token_secret">oauth_token_secret</label>
<div class="input-group">
<input id="oauth_token_secret" type="text" class="form-control"
v-model="oauth_token_secret">
</div>
</div>
<div class="mb-3">
<label class="form-label" for="method">method</label>
<div class="input-group">
<select id="method" class="form-select" v-model="method">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
<option value="OPTIONS">OPTIONS</option>
<option value="HEAD">HEAD</option>
<option value="CONNECT">CONNECT</option>
<option value="TRACE">TRACE</option>
<option value="PATCH">PATCH</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="url">url</label>
<div class="input-group">
<textarea id="url" class="form-control" rows="5" v-model="url"></textarea>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="body">body</label>
<div class="input-group">
<textarea id="body" class="form-control" rows="5" v-model="body"></textarea>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="timestamp">timestamp</label>
<div class="input-group">
<input id="timestamp" type="number" class="form-control" v-model="timestamp">
<button :class="{btn: true, 'btn-outline-danger': !stop, 'btn-danger': stop}" type="button"
id="button-timestamp" @click="stop = !stop">Stop</button>
</div>
</div>
<div class="mb-3">
<label class="form-label" id="oauth_nonce">oauth_nonce</label>
<div class="input-group">
<input id="oauth_nonce" type="text" class="form-control" v-model="oauth_nonce">
<button class="btn btn-outline-secondary" type="button" id="button-oauth_nonce"
@click="oauth_nonce = updateOauthNonce()">Random</button>
</div>
<div class="form-text" id="form-text-oauth_consumer_secret">The <code>oauth_nonce</code>
parameter is a unique token your application should generate for each unique request.
Twitter will use this value to determine whether a request has been submitted multiple
times. The value for this request was generated by base64 encoding 32 bytes of random data,
and stripping out all non-word characters, but any approach which produces a relatively
random alphanumeric string should be OK here.</div>
</div>
<hr />
</div>
<div class="col-md-4">
<button
:class="{btn: true, 'btn-sm': true, 'btn-outline-danger': !stop, 'btn-danger': stop, 'mb-1': true}"
type="button" @click="stop = !stop">Stop automatic timestamp</button>
<hr />
<h3>Authorization</h3>
<div id="authorization" class="p-2 mb-3 rounded"
style="background-color: rgb(247, 247, 247); user-select: all;">
<code>{{ `OAuth realm="http://api.twitter.com/", oauth_version="1.0", oauth_token="${signature.oauth_token}", oauth_nonce="${signature.oauth_nonce}", oauth_timestamp="${signature.timestamp}", oauth_signature="${encodeURIComponent(signature.sign)}", oauth_consumer_key="${signature.oauth_consumer_key}", oauth_signature_method="HMAC-SHA1"` }}</code>
</div>
<h3>All data</h3>
<div id="all-data" class="p-2 mb-3 rounded" style="background-color: rgb(247, 247, 247);">
<code><pre>{{ JSON.stringify(signature, null, 4) }}</pre></code>
</div>
<h3>More...</h3>
<div id="all-data" class="p-2 rounded" style="background-color: rgb(247, 247, 247);">
<ul>
<li><a href="https://developer.twitter.com/en/docs/authentication/oauth-1-0a/creating-a-signature"
target="_blank">Creating a signature</a></li>
<li><a href="https://blog.nest.moe/posts/how-to-crawl-twitter-with-android#%E5%88%9B%E5%BB%BA-oauth-%E7%AD%BE%E5%90%8D"
target="_blank">创建 OAuth 签名</a></li>
</ul>
</div>
</div>
</div>
</div>
<div class="text-center my-2">
>_ Twitter Monitor
</div>
</div>
<!--load css and js-->
<script src="https://cdn.jsdelivr.net/npm/vue@3.4.0"></script>
<script>
const TW_CONSUMER_KEY = '3nVuSoBZnx6U4vzUxf5w'
const TW_CONSUMER_SECRET = 'Bcs59EFbbsdF6Sl9Ng71smgStWEGwXXKSjYvPVt7qys'
const { createApp } = Vue
createApp({
data: () => ({
oauth_consumer_key: TW_CONSUMER_KEY,
oauth_consumer_secret: TW_CONSUMER_SECRET,
oauth_token: "370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb",
oauth_token_secret: "LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE",
method: "POST",
url: "https://api.twitter.com/1.1/statuses/update.json?include_entities=true",
body: "status=Hello%20Ladies%20%2b%20Gentlemen%2c%20a%20signed%20OAuth%20request%21",
timestamp: "0",
oauth_nonce: "0",
stop: false,
signature: ''
}),
watch: {
"now": function () {
this.timestamp = this.now
}
},
mounted: function () {
this.oauth_nonce = this.updateOauthNonce()
setInterval(async () => {
this.updateNow()
this.signature = await this.getOauthAuthorization(this.oauth_token, this.oauth_token_secret, this.method, this.url, this.body, this.timestamp, this.oauth_nonce)
}, 500)
},
methods: {
updateNow: function () {
if (!this.stop) {
this.timestamp = Math.floor(Date.now() / 1000)
}
},
updateOauthNonce: function () {
if (typeof crypto.randomUUID === "undefined") {
return btoa(new Array(2).fill(Math.random().toString()).join('').slice(4)).replaceAll('+', '').replaceAll('/', '').replaceAll('=', '')
} else {
return btoa(crypto.randomUUID().replaceAll('-', '')).replaceAll('+', '').replaceAll('/', '').replaceAll('=', '')
}
},
getOauthAuthorization: async function (oauth_token, oauth_token_secret, method = 'GET', url = '', body = '', timestamp = Math.floor(Date.now() / 1000), oauth_nonce = this.updateOauthNonce()) {
if (!url) {
return ''
}
method = method.toUpperCase()
const parseUrl = new URL(url)
const link = parseUrl.origin + parseUrl.pathname
const payload = [...parseUrl.searchParams.entries()]
if (body) {
let isJson = false
try {
JSON.parse(body)
isJson = true
} catch (e) { }
if (!isJson) {
payload.push(...new URLSearchParams(body).entries())
}
}
payload.push(['oauth_version', '1.0'])
payload.push(['oauth_signature_method', 'HMAC-SHA1'])
payload.push(['oauth_consumer_key', TW_CONSUMER_KEY])
payload.push(['oauth_token', oauth_token])
payload.push(['oauth_nonce', oauth_nonce])
payload.push(['oauth_timestamp', String(timestamp)])
const forSign = method + '&' + encodeURIComponent(link) + '&' + new URLSearchParams(payload.sort((a, b) => (a[0] > b[0] ? 1 : a[0] < b[0] ? -1 : 0))).toString().replaceAll('+', '%20').replaceAll('%', '%25').replaceAll('=', '%3D').replaceAll('&', '%26')
let key = await crypto.subtle.importKey("raw", new TextEncoder('utf-8').encode(TW_CONSUMER_SECRET + '&' + (oauth_token_secret ? oauth_token_secret : '')), { name: "HMAC", hash: "SHA-1" }, false, ["sign", "verify"])
let sign = await crypto.subtle.sign('HMAC', key, new TextEncoder('utf-8').encode(forSign))
return {
method,
url,
parse_url: parseUrl,
timestamp,
oauth_nonce,
oauth_token,
oauth_token_secret,
oauth_consumer_key: this.oauth_consumer_key,
oauth_consumer_secret: this.oauth_consumer_secret,
payload,
forSign,
sign: this.buffer_to_base64(sign)
}
},
buffer_to_base64: buf => {
let binary = '';
const bytes = new Uint8Array(buf);
for (var i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary)
}
}
}).mount('#app')
</script>
</body>
</html>
================================================
FILE: apps/online_tools/snowflake.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="robots" content="nofollow">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="Banka2017 (https://nest.moe)">
<meta name="description" content="Twitter Monitor Snowflake Tool">
<title>Snowflake</title>
<!-- CSS only -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css"
crossorigin="anonymous">
</head>
<body>
<div id="app">
<nav class="navbar navbar-expand-lg navbar-light text-center bg-light "
style="display: flex; justify-content: space-between;">
<span class="navbar-brand mb-0 h1">Snowflake</span>
</nav>
<div class="my-4"></div>
<div class="container">
<div class="row">
<div class="col-md-8">
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="Snowflake" v-model="snowflake">
</div>
<div>
<table class="table table-hover">
<tbody>
<tr>
<th scope="row">Created date</th>
<td><code>{{ parsedSnowflakeInfo.creation_time_milli ? new Date(parsedSnowflakeInfo.creation_time_milli) : '' }}</code>
</td>
</tr>
<tr>
<th scope="row">Timestamp</th>
<td><input type="number" min="0" class="form-control"
v-model="parsedSnowflakeInfo.creation_time_milli" placeholder="Timestamp">
</td>
</tr>
<tr>
<th scope="row">Sequence id</th>
<td><input type="number" min="0" max="4095" class="form-control"
v-model="parsedSnowflakeInfo.sequence_id" placeholder="Sequence id"></td>
</tr>
<tr>
<th scope="row">Machine id</th>
<td><input type="number" min="0" max="1023" class="form-control"
v-model="parsedSnowflakeInfo.machine_id" placeholder="Machine id"></td>
</tr>
<tr>
<th scope="row">Datacenter id</th>
<td><input type="number" min="0" max="31" class="form-control"
v-model="parsedSnowflakeInfo.datacenter_id" placeholder="Datacenter id">
</td>
</tr>
<tr>
<th scope="row">Server id</th>
<td><input type="number" min="0" max="31" class="form-control"
v-model="parsedSnowflakeInfo.server_id" placeholder="Server id"></td>
</tr>
</tbody>
</table>
<code
v-if="snowflake"><pre style="color: var(--bs-code-color);">{{ JSON.stringify(parsedSnowflakeInfo, null, 4) }}</pre></code>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body">
<p class="lead">More...</p>
<ul>
<li><a href="https://blog.nest.moe/posts/about-snowflakes#snowflakes%E7%9A%84%E7%94%B1%E6%9D%A5"
target="_blank" class="end-of-link">Snowflakes的由来</a></li>
<li><a href="https://docs.google.com/document/d/1xVrPoNutyqTdQ04DXBEZW4ZW4A5RAQW2he7qIpTmG-M/edit"
target="_blank" class="end-of-link">Reconstructing Twitter's Firehose</a></li>
<li><a href="https://github.com/igorbrigadir/twitter-advanced-search#snowflake-ids"
target="_blank"
class="end-of-link">github:igorbrigadir/twitter-advanced-search#snowflake-ids</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="text-center my-2">
>_ Twitter Monitor
</div>
<!--load css and js-->
<script src="https://cdn.jsdelivr.net/npm/vue@3.4.0"></script>
<script>
const { createApp, ref, watch, computed } = Vue
const baseUnixMilli = 1288834974657
const Time2SnowFlake = (date = new Date(), datacenter_id = 0, server_id = 0, sequence_id = 0, start = 1288834974657) => {
const diffDate = (typeof date === 'number' || typeof date === 'bigint' ? date : Date.parse(date)) - start
if (diffDate < 0) {
return BigInt(0)
}
return (BigInt(diffDate) << BigInt(22)) | BigInt((datacenter_id << 17) | (server_id << 12) | sequence_id)
}
const SnowFlake2Time = (snowflake, start = 1288834974657) => {
let tmpData = {
creation_time_milli: start,
sequence_id: 0,
machine_id: 0,
server_id: 0,
datacenter_id: 0
}
if (!/^[1-9](\d+|)$/gm.test(snowflake)) {
return tmpData
}
if (typeof snowflake === 'string' || typeof snowflake === 'number') {
snowflake = BigInt(snowflake)
// 0
if (!snowflake) {
return tmpData
}
}
// Sequence number
tmpData.sequence_id = Number(snowflake & BigInt(4095))
snowflake = Number(snowflake >> BigInt(12))
// Machine id
tmpData.machine_id = snowflake & 1023
tmpData.server_id = tmpData.machine_id & 31
tmpData.datacenter_id = (tmpData.machine_id >> 5) & 31
// Time
tmpData.creation_time_milli += Math.floor(snowflake / 1024)
return tmpData
}
createApp({
setup() {
const snowflake = ref('')
const parsedSnowflakeInfoData = ref({})
const parsedSnowflakeInfo = computed({
get: () => parsedSnowflakeInfoData.value,
set: (val) => {
parsedSnowflakeInfoData.value = val
}
})
if (window.location.hash) {
snowflake.value = window.location.hash.replace('#', '')
parsedSnowflakeInfo.value = SnowFlake2Time(snowflake.value)
}
watch(snowflake, (to, from) => {
if (to === from) {
return
}
if (/[^\d]/gm.test(snowflake.value)) {
snowflake.value = snowflake.value.replaceAll(/[^\d]/gm, '')
return
}
parsedSnowflakeInfo.value = SnowFlake2Time(snowflake.value)
window.location.hash = snowflake.value
})
watch(parsedSnowflakeInfo, (to, from) => {
snowflake.value = Time2SnowFlake(parsedSnowflakeInfo.value.creation_time_milli, parsedSnowflakeInfo.value.datacenter_id, parsedSnowflakeInfo.value.server_id, parsedSnowflakeInfo.value.sequence_id).toString()
}, { deep: true })
return { snowflake, parsedSnowflakeInfo }
}
}).mount('#app')
</script>
</body>
</html>
================================================
FILE: apps/online_tools/webpush.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="robots" content="nofollow">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="Banka2017 (https://nest.moe)">
<meta name="description" content="Webpush Tools">
<title>Webpush Tools</title>
<!-- CSS only -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
</head>
<body>
<div id="app">
<nav class="navbar navbar-expand-lg navbar-light text-center bg-light px-2">
<span class="navbar-brand mb-0 h1">Webpush</span>
</nav>
<div class="container">
<div class="my-4 card mb-3">
<div class="card-body">
<details open>
<summary>Config List</summary>
<div class="row" v-if="configList.length">
<div class="col-md-6 order-md-2 my-2">
<label class="form-label" for="auth">Config name</label>
<div class="input-group">
<input id="auth" type="text" class="form-control"
v-model="configList[configListIndex].name">
</div>
<span class="form-text" id="form-text-auth">Name of config</span>
<hr />
<label class="form-label" for="auth">Shared Link</label>
<div class="input-group">
<input id="auth" type="text" class="form-control"
:value="shared_link">
</div>
<hr />
<label for="upload-webpush-data">Import Config</label>
<input type="file" class="form-control" id="upload-webpush-data" lang="zh"
@change="(e)=>{addWebPushData(e, 'upload-webpush-data')}" accept="application/json">
<label class="form-text" for="upload-webpush-data">webpush_export.json</label>
<div class="my-2"></div>
<div class="d-flex justify-content-between">
<button class="btn btn-sm btn-outline-danger me-1 mb-1" type="button"
@click="deleteKeyPair">
⚠ Delete
</button>
<div class="mb-1">
<button class="btn btn-sm btn-outline-success me-1" type="button"
@click="initKeyPair">
New config
</button>
<button class="btn btn-sm btn-primary me-1"
@click="download('webpush_export.json', JSON.stringify(configList[configListIndex]))">Export
<span class="font-monospace">{{ configList[configListIndex].name
}}</span></button>
</div>
</div>
<ul>
<li class="form-text my-2" for="upload-webpush-data">Click the <span
class="fst-italic">Export</span> to export <span class="font-monospace">{{
configList[configListIndex].name }}</span> and message list</li>
</ul>
</div>
<div class="col-md-6 order-md-1 my-2">
<ul class="list-group" v-if="configList.length">
<li :class="{'list-group-item': true, active: keyIndex === configListIndex}"
v-for="(key, keyIndex) in configList"
:key="key.auth+key.jwk.d+key.jwk.x+key.jwk.y"
@click="configListIndex = keyIndex">{{key.name}}</li>
</ul>
</div>
</div>
</details>
</div>
</div>
<div class="row" v-if="configList[configListIndex]">
<div class="col-md-6">
<h3>ECC & Auth</h3>
<div class="d-flex justify-content-between">
<div class="btn-group" role="group">
<button
:class="{btn: true, 'btn-sm': true, 'btn-primary': keyDataEncoded === 'jwk', 'btn-outline-primary': keyDataEncoded !== 'jwk'}"
type="button" @click="keyDataEncoded='jwk'">
JWK
</button>
<button
:class="{btn: true, 'btn-sm': true, 'btn-primary': keyDataEncoded === 'base64', 'btn-outline-primary': keyDataEncoded !== 'base64'}"
type="button" @click="keyDataEncoded='base64'">
Base64
</button>
<button
:class="{btn: true, 'btn-sm': true, 'btn-primary': keyDataEncoded === 'base64url', 'btn-outline-primary': keyDataEncoded !== 'base64url'}"
type="button" @click="keyDataEncoded='base64url'">
Base64URL
</button>
<button
:class="{btn: true, 'btn-sm': true, 'btn-primary': keyDataEncoded === 'hex', 'btn-outline-primary': keyDataEncoded !== 'hex'}"
type="button" @click="keyDataEncoded='hex'">
Hex
</button>
<button
:class="{btn: true, 'btn-sm': true, 'btn-primary': keyDataEncoded === 'buffer', 'btn-outline-primary': keyDataEncoded !== 'buffer'}"
type="button" @click="keyDataEncoded='buffer'">
Buffer
</button>
</div>
</div>
<div v-if="['base64', 'base64url', 'hex'].includes(keyDataEncoded)" class="mb-3 mt-2">
<label class="form-label" for="public-key">PublicKey</label>
<div class="input-group">
<input id="public-key" type="text" class="form-control" v-model="text_input_public">
</div>
<label class="form-label" for="private-key">PrivateKey</label>
<div class="input-group">
<input id="private-key" type="text" class="form-control" v-model="text_input_private">
</div>
</div>
<div v-else-if="keyDataEncoded === 'buffer'" class="mb-3 mt-2">
Please open the Console...
{{console.log({public: eccKeyData.public.buffer, private: eccKeyData.private.buffer})}}
</div>
<div v-else class="input-group mb-3 mt-2">
<textarea class="form-control" rows="12" v-model="jwk_input"></textarea>
</div>
<hr />
<div class="mb-3">
<label class="form-label" for="auth">Auth</label>
<div class="input-group">
<input id="auth" type="text" class="form-control"
v-model="configList[configListIndex].auth">
</div>
<div class="form-text" id="form-text-auth">Random 16 bits value</div>
</div>
<hr />
</div>
<div class="col-md-6">
<h3>Tools</h3>
<details :open="open_decrypt">
<summary>Decrypt / Encrypt</summary>
<div class="mb-3">
<label class="form-label" for="de_encoding">Encoding</label>
<select id="de_encoding" class="form-select" aria-label="Encoding" v-model="de_encoding">
<option selected value="aesgcm">aesgcm</option>
<option value="aes128gcm">aes128gcm</option>
</select>
</div>
<div class="mb-3">
<label class="form-label" for="publish_dh">dh</label>
<div class="input-group">
<input id="publish_dh" type="text" class="form-control" v-model="publish_dh"
:disabled="de_encoding !== 'aesgcm'">
</div>
<div class="form-text" id="form-text-uaid">Another ECC public key (base64url)</div>
</div>
<div class="mb-3">
<label class="form-label" for="publish_salt">salt</label>
<div class="input-group">
<input id="publish_salt" type="text" class="form-control" v-model="publish_salt"
:disabled="de_encoding !== 'aesgcm'">
</div>
<div class="form-text" id="form-text-publish_salt">Another random 16 bits value (base64url)
</div>
</div>
<div class="mb-3">
<label class="form-label" for="encrypted_message_input">Encrypted Data</label>
<div class="input-group">
<textarea class="form-control" rows="5" v-model="encrypted_message_input"></textarea>
</div>
<div class="form-text" id="form-text-endpoint">From source (base64url)</div>
</div>
<div class="mb-3">
<label class="form-label" for="endpoint">Decrypted Data</label>
<div class="input-group">
<textarea class="form-control" rows="5" v-model="decrypted_message_input"
:disabled="de_encoding !== 'aesgcm'"></textarea>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="de_rs">record size (rs)</label>
<div class="input-group">
<input id="de_rs" type="text" class="form-control" :value="de_rs" disabled>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="padding_length">padding length</label>
<div class="input-group">
<input id="padding_length" type="text" class="form-control" :value="de_padding_length"
disabled>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="de_nonce">nonce(iv)</label>
<div class="input-group">
<input id="de_nonce" type="text" class="form-control" :value="de_nonce" disabled>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="de_cek">cek/contentEncryptionKey(key)</label>
<div class="input-group">
<input id="de_cek" type="text" class="form-control" :value="de_cek" disabled>
</div>
</div>
</details>
<details :open="open_websocket">
<summary>Autopush Websocket</summary>
<div>Status: <span :class="{'text-danger': wsClosed, 'text-success': !wsClosed}">{{ wsClosed ?
'disconnected' : 'connected' }}</span></div>
<button class="btn btn-sm btn-outline-primary mb-1" type="button" @click="wsSwitch">{{ wsClosed
?
'Connect' : 'Disconnect' }}</button>
<div v-if="!wsClosed">
<button class="btn btn-sm btn-outline-danger mb-1 me-1" type="button" @click="newUaid">New
Uaid</button>
<button class="btn btn-sm btn-outline-danger mb-1 me-1" type="button"
@click="registerRandomChannel">New Channel</button>
<button class="btn btn-sm btn-danger mb-1 me-1" type="button"
@click="unregisterChannel">Unregister Channel</button>
</div>
<br />
<button class="btn btn-sm btn-danger mb-1 me-1" type="button"
@click="() => {configList[configListIndex].original_messages=[];decryptedMessages=[]}">Delete
all
messages</button>
<div class="mb-3">
<label class="form-label" for="vapid">VAPID</label>
<div class="input-group">
<input id="vapid" type="text" class="form-control"
v-model="configList[configListIndex].firefox.vapid">
</div>
<div class="form-text" id="form-text-vapid">From Twitter or other subscription source</div>
</div>
<div class="mb-3">
<label class="form-label" for="uaid">uaid</label>
<div class="input-group">
<input id="uaid" type="text" class="form-control"
v-model="configList[configListIndex].firefox.uaid">
</div>
<div class="form-text" id="form-text-uaid">From Autopush and very important</div>
</div>
<div class="mb-3">
<label class="form-label" for="channelID">channelID</label>
<div class="input-group">
<input id="channelID" type="text" class="form-control"
v-model="configList[configListIndex].firefox.channelID">
</div>
<div class="form-text" id="form-text-channelID">From Autopush</div>
</div>
<div class="mb-3">
<label class="form-label"
for="remote_settings__monitor_changes">remote_settings/monitor_changes</label>
<div class="input-group">
<input id="remote_settings__monitor_changes" type="text" class="form-control"
v-model="configList[configListIndex].firefox.remote_settings__monitor_changes">
</div>
<div class="form-text" id="form-text-remote_settings__monitor_changes">A timestamp from
Autopush
and unknown what used for</div>
</div>
<div class="mb-3">
<label class="form-label" for="endpoint">Endpoint</label>
<div class="input-group">
<textarea class="form-control" rows="5"
v-model="configList[configListIndex].firefox.endpoint"></textarea>
</div>
<div class="form-text" id="form-text-endpoint">From Autopush</div>
</div>
<hr />
<h3>Message</h3>
<div class="btn-group mb-2" role="group">
<button
:class="{btn: true, 'btn-sm': true, 'btn-primary': messageType === 'all', 'btn-outline-primary': messageType !== 'all'}"
type="button" @click="messageType='all'">
All
</button>
<button
:class="{btn: true, 'btn-sm': true, 'btn-primary': messageType === 'data', 'btn-outline-primary': messageType !== 'data'}"
type="button" @click="messageType='data'">
Data
</button>
</div>
<div v-for="(message, index) in decryptedMessages" class="card mb-2" :key="message.tag">
<div :id="`tweet-`+message.tag" class="card-body">
<code
style="background-color: rgb(247, 247, 247);"><pre>{{ messageType === 'all' ? JSON.stringify(message, null, 4) : message.data}}</pre></code>
</div>
</div>
</details>
<hr />
<h3>More...</h3>
<div id="all-data" class="p-2 rounded" style="background-color: rgb(247, 247, 247);">
<ul>
<li><a href="https://datatracker.ietf.org/doc/html/rfc7517" target="_blank">(RFC 7517) JSON
Web Key (JWK)</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc8188" target="_blank">(RFC 8188)
Encrypted Content-Encoding for HTTP</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc8291" target="_blank">(RFC 8291)
Message Encryption for Web Push</a></li>
<li><a href="https://web.dev/articles/push-notifications-web-push-protocol"
target="_blank">The Web Push Protocol</a></li>
<li><a href="https://blog.mozilla.org/services/2016/08/23/sending-vapid-identified-webpush-notifications-via-mozillas-push-service/"
target="_blank">Sending VAPID identified WebPush Notifications via Mozilla’s Push
Service</a></li>
<li><a href="https://developer.chrome.com/blog/web-push-encryption?hl=en"
target="_blank">Web Push Payload Encryption</a></li>
<li><a href="https://taoshu.in/web/push.html" target="_blank">WebPush 工作原理</a></li>
<li><a href="https://mozilla-services.github.io/autopush-rs/" target="_blank">Mozilla
Autopush Server</a></li>
<li><a href="https://blog.nest.moe/posts/receive-the-latest-tweets-via-web-push"
target="_blank">通过 Web Push 接收最新的推文</a></li>
<li><a href="https://blog.nest.moe/posts/decrypt-aesgcm-messages-from-web-push"
target="_blank">解密来自 Web Push 的 aesgcm 消息</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="text-center my-2">
>_ Twitter Monitor
</div>
</div>
<!--load css and js-->
<script src="https://cdn.jsdelivr.net/npm/vue@3.4.0"></script>
<script>
const { createApp } = Vue
createApp({
data: () => ({
configList: [],
configListIndex: 0,
eccKeyHandle: null,
eccKeyData: {},
decryptedMessages: [],
// page control
keyDataEncoded: 'jwk',// jwk, base64, base64url, hex, buffer
messageType: 'all',
/// hide config
open_decrypt: false,
open_websocket: false,
// decrypt / encrypt
publish_dh: '',
publish_salt: '',
encrypted_data: '',
decrypted_data: '',
de_nonce: '',
de_cek: '',
de_rs: 0,
de_encoding: 'aesgcm',
de_padding_length: 0,
// ws
ws: null,
wsClosed: true,
wsAutoReconnect: true,
// templates
configListTemplate: {
name: '',
auth: "",
jwk: {
crv: "P-256",
d: "",
ext: true,
key_ops: ["deriveKey", "deriveBits"],
kty: "EC",
x: "",
y: ""
},
firefox: {
uaid: "",
channelID: "",
remote_settings__monitor_changes: "",
endpoint: "",
vapid: "",
},
original_messages: [],
},
vapidTemplate: "BF5oEo0xDUpgylKDTlsd8pZmxQA1leYINiY-rSscWYK_3tWAkz4VMbtf1MLE_Yyd6iII6o-e3Q9TCN5vZMzVMEs",
}),
computed: {
shared_link: function () {
return location.origin + location.pathname + '#/' + this.base64_to_base64url(btoa(JSON.stringify({ config: this.configList[this.configListIndex], ext_config: {
// page control
keyDataEncoded: this.keyDataEncoded,
messageType: this.messageType,
open_decrypt: this.open_decrypt,
open_websocket: this.open_websocket,
// decrypt / encrypt
publish_dh: this.publish_dh,
publish_salt: this.publish_salt,
encrypted_data: this.encrypted_data,
decrypted_data: this.decrypted_data,
de_nonce: this.de_nonce,
de_cek: this.de_cek,
de_rs: this.de_rs,
de_encoding: this.de_encoding,
de_padding_length: this.de_padding_length,
} })))
},
jwk_input: {
get: function () {
if (this.configList[this.configListIndex]) {
return JSON.stringify(this.configList[this.configListIndex].jwk, null, 4)
} else {
return '{}'
}
},
set: async function (newValue) {
if (this.configList[this.configListIndex]) {
this.configList[this.configListIndex].jwk = JSON.parse(newValue)
try {
this.eccKeyHandle = await this.importECCKey(this.configList[this.configListIndex].jwk)
} catch (e) { console.log(e) }
}
}
},
text_input_public: {
get: function () {
if (this.configList[this.configListIndex]) {
let tmpPublicKey = this.concatBuffer((new Uint8Array(1).fill(4)).buffer, this.base64_to_buffer(this.base64url_to_base64(this.configList[this.configListIndex].jwk.x)), this.base64_to_buffer(this.base64url_to_base64(this.configList[this.configListIndex].jwk.y)))
switch (this.keyDataEncoded) {
case 'base64':
tmpPublicKey = this.buffer_to_base64(tmpPublicKey)
break
case 'base64url':
tmpPublicKey = this.base64_to_base64url(this.buffer_to_base64(tmpPublicKey))
break
case 'hex':
tmpPublicKey = this.buffer_to_hex(tmpPublicKey)
break
default:
tmpPublicKey = ''
}
return tmpPublicKey
} else {
return ''
}
},
set: async function (newValue) {
let tmpPublicKey = null
switch (this.keyDataEncoded) {
case 'base64':
tmpPublicKey = this.base64_to_buffer(newValue)
break
case 'base64url':
tmpPublicKey = this.base64_to_buffer(this.base64url_to_base64(newValue))
break
case 'hex':
tmpPublicKey = this.hex_to_uintarray(newValue).buffer
break
}
if (!tmpPublicKey) {
console.log('Invalid public key')
return
}
if (this.configList[this.configListIndex]) {
let tmpJwk = this.configList[this.configListIndex].jwk
tmpJwk.x = this.base64_to_base64url(this.buffer_to_base64(tmpPublicKey.slice(1, 33)))
tmpJwk.y = this.base64_to_base64url(this.buffer_to_base64(tmpPublicKey.slice(33, 66)))
//console.log(tmpJwk)
try {
this.eccKeyHandle = await this.importECCKey(tmpJwk)
} catch (e) { console.log(e) }
}
}
},
text_input_private: {
get: function () {
if (this.configList[this.configListIndex]) {
let tmpPrivateKey = ''
switch (this.keyDataEncoded) {
case 'base64':
tmpPrivateKey = this.base64url_to_base64(this.configList[this.configListIndex].jwk.d)
break
case 'base64url':
tmpPrivateKey = this.configList[this.configListIndex].jwk.d
break
case 'hex':
tmpPrivateKey = this.buffer_to_hex(this.base64_to_buffer(this.base64url_to_base64(this.configList[this.configListIndex].jwk.d)))
break
}
return tmpPrivateKey
} else {
return ''
}
},
set: async function (newValue) {
let tmpPrivateKey = null
switch (this.keyDataEncoded) {
case 'base64':
tmpPrivateKey = this.base64_to_buffer(newValue)
break
case 'base64url':
tmpPrivateKey = this.base64_to_buffer(this.base64url_to_base64(newValue))
break
case 'hex':
tmpPrivateKey = this.hex_to_uintarray(newValue).buffer
break
}
if (!tmpPrivateKey) {
console.log('Invalid private key')
return
}
if (this.configList[this.configListIndex]) {
let tmpJwk = this.configList[this.configListIndex].jwk
tmpJwk.d = this.base64_to_base64url(this.buffer_to_base64(tmpPrivateKey))
//console.log(tmpJwk)
try {
this.eccKeyHandle = await this.importECCKey(tmpJwk)
} catch (e) { console.log(e) }
}
}
},
encrypted_message_input: {
get: function () {
return this.encrypted_data
},
set: async function (newValue) {
this.encrypted_data = newValue
if (!newValue) {
return
}
this.de_rs = 0
let messageData = this.base64_to_buffer(this.base64url_to_base64(newValue))
if (this.de_encoding === 'aes128gcm') {
this.publish_salt = this.base64_to_base64url(this.buffer_to_base64(messageData.slice(0, 16)))
//idlen = new DataView(messageData.slice(20, 21)).getUint8()// 65
this.publish_dh = this.base64_to_base64url(this.buffer_to_base64(messageData.slice(21, 86)))
this.de_rs = new DataView(messageData.slice(16, 20)).getUint32()
messageData = messageData.slice(86)
}
let nonce, contentEncryptionKey, decode
if (this.de_encoding === 'aes128gcm') {
//console.log(this.publish_dh, this.publish_salt, this.eccKeyData, this.configList[this.configListIndex].auth)
const tmp = await this.getAES128GCMNonceAndCekAndContent(this.publish_dh, this.publish_salt, this.eccKeyData, this.configList[this.configListIndex].auth)
nonce = tmp.nonce
contentEncryptionKey = tmp.cek
//console.log(nonce, contentEncryptionKey)
decode = await this.decrypt(nonce, contentEncryptionKey, messageData, this.de_rs, this.de_encoding)
} else {
const tmp = await this.getAESGCMNonceAndCekAndContent(this.publish_dh, this.publish_salt, this.eccKeyData, this.configList[this.configListIndex].auth)
nonce = tmp.nonce
contentEncryptionKey = tmp.cek
decode = await this.decrypt(nonce, contentEncryptionKey, messageData, this.de_rs, this.de_encoding)
}
//console.log(decode)
this.decrypted_data = new TextDecoder('utf-8').decode(decode.data)
this.de_nonce = this.base64_to_base64url(this.buffer_to_base64(nonce))
this.de_cek = this.base64_to_base64url(this.buffer_to_base64(contentEncryptionKey))
this.de_padding_length = decode.padding.length
}
},
decrypted_message_input: {
get: function () {
return this.decrypted_data
},
set: async function (newValue) {
this.decrypted_data = newValue
// we not yet supported aes128gcm
if (!newValue || this.de_encoding !== 'aesgcm') {
return
}
const { nonce, cek: contentEncryptionKey } = await this.getAESGCMNonceAndCekAndContent(this.publish_dh, this.publish_salt, this.eccKeyData, this.configList[this.configListIndex].auth)
const encode = await this.encrypt(nonce, contentEncryptionKey, new TextEncoder('utf-8').encode(newValue), 0, 'aesgcm')
this.encrypted_data = this.base64_to_base64url(this.buffer_to_base64(encode.data))
this.de_nonce = this.base64_to_base64url(this.buffer_to_base64(nonce))
this.de_cek = this.base64_to_base64url(this.buffer_to_base64(contentEncryptionKey))
this.de_padding_length = encode.padding.length
}
},
},
watch: {
"eccKeyHandle": async function () {
if (!this.eccKeyHandle) {
return
}
const exportedJwk = await crypto.subtle.exportKey('jwk', this.eccKeyHandle)
const privateKey = this.base64_to_buffer(this.base64url_to_base64(exportedJwk.d))
const publicKey = this.concatBuffer(new Uint8Array(1).fill(4).buffer, this.base64_to_buffer(this.base64url_to_base64(exportedJwk.x)), this.base64_to_buffer(this.base64url_to_base64(exportedJwk.y)))
this.eccKeyData = {
private: {
base64: this.buffer_to_base64(privateKey),
base64url: this.base64_to_base64url(this.buffer_to_base64(privateKey)),
hex: this.buffer_to_hex(privateKey),
buffer: privateKey
},
public: {
base64: this.buffer_to_base64(publicKey),
base64url: this.base64_to_base64url(this.buffer_to_base64(publicKey)),
hex: this.buffer_to_hex(publicKey),
buffer: publicKey
},
jwk: exportedJwk,
key: this.eccKeyHandle
}
},
"wsClosed": function () {
if (!this.wsClosed || !this.wsAutoReconnect) {
return
}
this.wsClosed = false
console.log('restart')
this.initWebsocket()
this.initWebsocketEvents()
},
"configListIndex": function () {
if (!this.wsClosed) {
this.wsAutoReconnect = false
this.ws.close()
}
},
},
mounted: async function () {
// config from hash
if (location.hash.length > 2) {
try {
const decodedConfig = JSON.parse(atob(location.hash.slice(2)))
//console.log(decodedConfig)
// config
this.configList.push(decodedConfig.config)
// config_ext
for (k in decodedConfig.ext_config) {
if (this[k] !== undefined) {
this[k] = decodedConfig.ext_config[k]
}
}
} catch {}
}
if (this.configList.length === 0) {
this.initKeyPair()
}
if (this.configList[this.configListIndex].jwk) {
try {
this.eccKeyHandle = await this.importECCKey(this.configList[this.configListIndex].jwk)
} catch (e) { console.log(e) }
}
if (!this.eccKeyData) {
return
}
await this.restoreOrignalMessages()
const tmpMessages = []
for (const message of this.configList[this.configListIndex].original_messages) {
const tmpMessageData = await this.decryptMessage(message)
//console.log(tmpMessageData)
tmpMessages.push(tmpMessageData)
}
this.decryptedMessages = tmpMessages
//this.initWebsocket()
//this.initWebsocketEvents()
},
methods: {
//https://stackoverflow.com/questions/21797299/convert-base64-string-to-arraybuffer
base64_to_buffer: function (base64) {
let binaryString = atob(base64)
let bytes = new Uint8Array(binaryString.length)
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i)
}
return bytes.buffer
},
//https://stackoverflow.com/questions/56846930/how-to-convert-raw-representations-of-ecdh-key-pair-into-a-json-web-key
hex_to_uintarray: hex => {
const a = []
for (let i = 0, len = hex.length; i < len; i += 2) {
a.push(parseInt(hex.substr(i, 2), 16))
}
return new Uint8Array(a)
},
buffer_to_base64: buf => {
let binary = ''
const bytes = new Uint8Array(buf)
for (var i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i])
}
return window.btoa(binary)
},
base64_to_base64url: base64 => base64.replaceAll('/', '_').replaceAll('+', '-').replaceAll('=', ''),
base64url_to_base64: base64url => base64url.replaceAll('_', '/').replaceAll('-', '+'),
//https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex
buffer_to_hex: function (buffer) { // buffer is an ArrayBuffer
return [...new Uint8Array(buffer)]
.map(x => x.toString(16).padStart(2, '0'))
.join('')
},
//https://gist.github.com/72lions/4528834
concatBuffer: function (...buffer) {
const length = buffer.reduce((acc, cur) => acc + cur.byteLength, 0)
let tmp = new Uint8Array(length)
buffer.reduce((acc, cur) => {
tmp.set(new Uint8Array(cur), acc)
return acc + cur.byteLength
}, 0)
return tmp.buffer
},
deriveSecretKey: function (privateKey, publicKey) {
return crypto.subtle.deriveKey(
{
name: "ECDH",
public: publicKey,
},
privateKey,
{
name: "AES-GCM",
length: 256,
},
true,
["encrypt", "decrypt"],
)
},
hmac_sha_256: async function (key, data) {
const keyData = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"],)
return crypto.subtle.sign("HMAC", keyData, data)
},
hkdf: async function (salt, ikm, info, length) {
let key = await this.hmac_sha_256(salt, ikm)
let signature = await this.hmac_sha_256(key, this.concatBuffer(info, new Uint8Array([1]).buffer))
return signature.slice(0, length)
},
importECCKey: function (jwk) {
return crypto.subtle.importKey("jwk", jwk, {
name: 'ECDH',
namedCurve: jwk.crv
},
true,
jwk.key_ops
)
},
initKeyPair: async function () {
this.configList.push(JSON.parse(JSON.stringify(this.configListTemplate)))
const keyPair = await crypto.subtle.generateKey({
name: 'ECDH',
namedCurve: 'P-256'
},
true,
['deriveKey', 'deriveBits']
)
this.configListIndex = this.configList.length - 1
// set name
this.configList[this.configListIndex].name = 'Config ' + this.configListIndex
this.configList[this.configListIndex].jwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey)
try {
this.eccKeyHandle = await this.importECCKey(this.configList[this.configListIndex].jwk)
} catch (e) { console.log(e) }
this.configList[this.configListIndex].auth = this.base64_to_base64url(this.buffer_to_base64(crypto.getRandomValues(new Uint8Array(16)).buffer))
// set twitter vapid
this.configList[this.configListIndex].firefox.vapid = this.vapidTemplate
},
deleteKeyPair: function () {
this.configList.splice(this.configListIndex, 1)
if (this.configListIndex === this.configList.length && this.configListIndex !== 0) {
this.configListIndex--
}
if (this.configList.length === 0) {
this.initKeyPair()
}
},
// note: eccKeyData is the subscriber's key object
getAESGCMNonceAndCekAndContent: async function (publicKey, salt_, eccKeyData, auth) {
// Convert subscription public key into a buffer.
const publishPublicKey = this.base64_to_buffer(this.base64url_to_base64(publicKey))
const pubDH = await crypto.subtle.importKey("raw", publishPublicKey, {
name: 'ECDH',
namedCurve: 'P-256'
},
true,
[]
)
const ecdh_secret_handle = await this.deriveSecretKey(eccKeyData.key, pubDH)
const ecdh_secret = await crypto.subtle.exportKey('raw', ecdh_secret_handle)
//console.log(eccKeyData, this.base64_to_buffer(this.base64url_to_base64(cryptoKey.dh)), this.buffer_to_hex(ecdh_secret))
// context
// https://web.dev/articles/push-notifications-web-push-protocol#context
const keyLabel = new TextEncoder('utf-8').encode('P-256\0')
const publishPublicKeyLength = new Uint8Array(2)
publishPublicKeyLength[0] = 0
publishPublicKeyLength[1] = publishPublicKey.byteLength
const subscriptionPublicKeyLength = new Uint8Array(2)
subscriptionPublicKeyLength[0] = 0
subscriptionPublicKeyLength[1] = eccKeyData.public.buffer.byteLength
const contextBuffer = this.concatBuffer(
keyLabel.buffer,
subscriptionPublicKeyLength.buffer,
eccKeyData.public.buffer,
publishPublicKeyLength.buffer,
publishPublicKey,
)
const auth_secret = this.base64_to_buffer(this.base64url_to_base64(auth))
const salt = this.base64_to_buffer(this.base64url_to_base64(salt_))
const authEncBuff = new TextEncoder('utf-8').encode('Content-Encoding: auth\0')
const prk = await this.hkdf(auth_secret, ecdh_secret, authEncBuff, 32)
const nonceEncBuffer = new TextEncoder('utf-8').encode('Content-Encoding: nonce\0')
const nonceInfo = this.concatBuffer(nonceEncBuffer, contextBuffer)
const cekEncBuffer = new TextEncoder('utf-8').encode('Content-Encoding: aesgcm\0')
const cekInfo = this.concatBuffer(cekEncBuffer, contextBuffer)
// The nonce should be 12 bytes long
const nonce = await this.hkdf(salt, prk, nonceInfo, 12)
// The CEK should be 16 bytes long
const contentEncryptionKey = await this.hkdf(salt, prk, cekInfo, 16)
return { nonce, cek: contentEncryptionKey, content: contextBuffer }
gitextract_drmlutwn/ ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .yarnrc.yml ├── LICENSE ├── README.MD ├── apps/ │ ├── backend/ │ │ ├── CoreFunctions/ │ │ │ ├── album/ │ │ │ │ └── Album.mjs │ │ │ ├── online/ │ │ │ │ ├── OnlineLogin.mjs │ │ │ │ ├── OnlineMisc.mjs │ │ │ │ ├── OnlineTrends.mjs │ │ │ │ ├── OnlineTweet.mjs │ │ │ │ └── OnlineUserInfo.mjs │ │ │ └── translate/ │ │ │ ├── OnlineTranslate.mjs │ │ │ └── Translate.mjs │ │ ├── app.mjs │ │ ├── service/ │ │ │ ├── album.mjs │ │ │ ├── online.mjs │ │ │ └── translate.mjs │ │ ├── share.mjs │ │ └── static/ │ │ ├── .gitkeep │ │ └── xml/ │ │ └── rss.xsl │ ├── online_tools/ │ │ ├── config.html │ │ ├── oauth_signature_builder.html │ │ ├── snowflake.html │ │ ├── webpush.html │ │ └── x_client_transaction_id.html │ ├── open_account/ │ │ ├── readme.md │ │ └── scripts/ │ │ ├── get_guest_token.js │ │ ├── get_open_account_info.mjs │ │ ├── login.mjs │ │ └── proxy.txt │ ├── rate_limit_checker/ │ │ ├── data/ │ │ │ └── .gitkeep │ │ ├── readme.md │ │ └── run.mjs │ ├── scripts/ │ │ ├── apiPathGenerator.mjs │ │ ├── loginflow.js │ │ ├── updateAndroidQueryIdList.mjs │ │ └── updateQueryIdList.mjs │ └── web_push/ │ ├── callback.mjs │ ├── config.mjs │ ├── config_example.json │ ├── decrypt.mjs │ ├── package.json │ ├── readme.md │ ├── twitter.mjs │ ├── utils.mjs │ ├── web_push.mjs │ └── websocket.mjs ├── libs/ │ ├── README.md │ ├── assets/ │ │ ├── config_sample.json │ │ ├── graphql/ │ │ │ ├── androidQueryIdList.js │ │ │ ├── featuresValueList.js │ │ │ ├── featuresValueList.json │ │ │ ├── graphqlQueryIdList.js │ │ │ └── graphqlQueryIdList.json │ │ └── setting_sample.mjs │ ├── core/ │ │ ├── Core.Rss.mjs │ │ ├── Core.android.mjs │ │ ├── Core.apiPath.mjs │ │ ├── Core.blurhash.mjs │ │ ├── Core.fetch.mjs │ │ ├── Core.function.mjs │ │ ├── Core.info.mjs │ │ ├── Core.push.mjs │ │ ├── Core.translate.mjs │ │ ├── Core.tweet.mjs │ │ └── Core.xClientTransactionID.mjs │ └── share/ │ ├── Constant.mjs │ ├── Mime.mjs │ ├── MockFuntions.mjs │ └── NodeConstant.mjs ├── package.json ├── packages/ │ ├── axios-helper/ │ │ ├── README.md │ │ ├── index.js │ │ ├── index.node.js │ │ └── package.json │ ├── crypto-helper/ │ │ ├── index.js │ │ ├── index.node.js │ │ └── package.json │ └── get-mime/ │ ├── index.js │ └── package.json ├── tests/ │ ├── backend.online.test.js │ ├── core.fetch.android.test.js │ ├── core.fetch.anonymous.test.js │ ├── mock/ │ │ └── express.js │ └── mock.express.test.js └── vitest.config.js
SYMBOL INDEX (149 symbols across 14 files)
FILE: apps/backend/app.mjs
constant EXPRESS_PORT (line 19) | let EXPRESS_PORT = 3000
constant EXPRESS_HOST (line 20) | let EXPRESS_HOST = '0.0.0.0'
constant EXPRESS_ALLOW_ORIGIN (line 21) | let EXPRESS_ALLOW_ORIGIN = ['*']
constant STATIC_PATH (line 22) | let STATIC_PATH = ''
constant ACTIVE_SERVICE (line 23) | let ACTIVE_SERVICE = []
constant GUEST_ACCOUNT_HANDLE (line 24) | let GUEST_ACCOUNT_HANDLE = new GuestAccount()
constant AUDIO_SPACE_CACHE (line 25) | let AUDIO_SPACE_CACHE = {}
FILE: apps/open_account/scripts/get_guest_token.js
constant TW_CONSUMER_KEY (line 3) | const TW_CONSUMER_KEY = '3nVuSoBZnx6U4vzUxf5w'
constant TW_CONSUMER_SECRET (line 4) | const TW_CONSUMER_SECRET = 'Bcs59EFbbsdF6Sl9Ng71smgStWEGwXXKSjYvPVt7qys'
constant TW_ANDROID_BASIC_TOKEN (line 6) | const TW_ANDROID_BASIC_TOKEN = `Basic ${btoa(TW_CONSUMER_KEY + ':' + TW_...
FILE: apps/web_push/config.mjs
class Config (line 5) | class Config {
method constructor (line 30) | constructor(path = '.') {
method initData (line 34) | async initData() {
method saveConfig (line 43) | saveConfig() {
method saveTweets (line 46) | saveTweets() {
method readFile (line 49) | readFile(path = '') {
method writeFile (line 52) | writeFile(path = '', data = '') {
FILE: apps/web_push/decrypt.mjs
class Decrypt (line 6) | class Decrypt {
method init (line 11) | async init(jwk = {}, auth = '') {
method exportKey (line 70) | async exportKey() {
method ecdh (line 77) | async ecdh(publicKey, privateKey) {
method hmac_sha_256 (line 91) | async hmac_sha_256(key, data) {
method get_ecdh_secret (line 95) | async get_ecdh_secret(dh) {
method get_cek_and_nonce (line 108) | async get_cek_and_nonce(dh, salt) {
method getNonce (line 121) | getNonce(nonce, SEQ) {
method splitData (line 134) | splitData(data, size) {
method decrypt (line 141) | async decrypt(nonce, contentEncryptionKey, content, rs = 0, encoding =...
FILE: apps/web_push/twitter.mjs
constant VAPID (line 10) | const VAPID = 'BF5oEo0xDUpgylKDTlsd8pZmxQA1leYINiY-rSscWYK_3tWAkz4VMbtf1...
class Twitter (line 12) | class Twitter {
method constructor (line 14) | constructor(cookies = {}) {
method login (line 17) | async login(account = '', password = '', authentication_secret = '') {
method postNotificationsAction (line 254) | postNotificationsAction(link = '', cookies = {}, payload = {}) {
method postNotificationsLogin (line 272) | postNotificationsLogin(loginPayload = {}) {
method postNotificationsLogout (line 275) | postNotificationsLogout(logoutPayload = {}) {
method postNotificationsCheckin (line 278) | postNotificationsCheckin(loginPayload = {}) {
method getNotificationsBadgeCount (line 281) | getNotificationsBadgeCount() {
method twitterSettingsPayloadBuilder (line 297) | twitterSettingsPayloadBuilder(endpoint, publicKey, auth, type = 'login...
FILE: apps/web_push/websocket.mjs
class WS (line 9) | class WS {
method constructor (line 20) | constructor(uaid = '', remote_settings__monitor_changes = '', endpoint...
method initWebsocket (line 27) | initWebsocket() {
method _send (line 40) | _send(msg) {
method register (line 48) | async register(VAPID, channelID = '') {
method unregister (line 60) | async unregister(channelID = '') {
method ack (line 68) | async ack(channelID, version) {
method close (line 71) | async close() {
method onOpen (line 77) | async onOpen() {
method onClosed (line 83) | async onClosed() {
method onPing (line 90) | async onPing() {
method onError (line 95) | async onError(error) {
method onMessage (line 98) | async onMessage(event) {
method initWebsocketEvents (line 122) | initWebsocketEvents() {
method selfCheck (line 132) | selfCheck() {
FILE: libs/assets/setting_sample.mjs
constant SQL_CONFIG (line 10) | const SQL_CONFIG = [
constant ACTIVE_SERVICE (line 34) | const ACTIVE_SERVICE = SQL_CONFIG.filter((x) => ((x.dbtype === 'sqlite' ...
constant CONFIG_ID (line 36) | const CONFIG_ID = 1 //just for multiple config
constant CYCLE_SECONDS (line 41) | const CYCLE_SECONDS = 60 //seconds
constant ALERT_TOKEN (line 52) | const ALERT_TOKEN = '' //for telegram bot, keep empty if needn't
constant ALERT_PUSH_TO (line 53) | const ALERT_PUSH_TO = '' //for telegram bot, keep empty if needn't
constant BOT_CHAT_ID (line 54) | const BOT_CHAT_ID = '' //for telegram api, keep empty if needn't
constant EXPRESS_PORT (line 56) | const EXPRESS_PORT = 3000
constant EXPRESS_HOST (line 57) | const EXPRESS_HOST = '0.0.0.0'
constant EXPRESS_ALLOW_ORIGIN (line 58) | const EXPRESS_ALLOW_ORIGIN = ['*']
constant STATIC_PATH (line 60) | const STATIC_PATH = basePath + '/../apps/backend/static'
constant TWEETS_SAVE_PATH (line 61) | const TWEETS_SAVE_PATH = basePath + '/../apps/crawler/savetweets/'
FILE: libs/core/Core.Rss.mjs
class Rss (line 1) | class Rss {
method channel (line 6) | channel(channelObject, addMode = false) {
method item (line 14) | item(itemArray) {
method obj2dom (line 18) | obj2dom(obj) {
method value (line 23) | get value() {
FILE: libs/core/Core.android.mjs
constant TW_ANDROID_BASIC_TOKEN (line 7) | const TW_ANDROID_BASIC_TOKEN = 'Basic M25WdVNvQlpueDZVNHZ6VXhmNXc6QmNzNT...
constant TW_CONSUMER_KEY (line 8) | const TW_CONSUMER_KEY = '3nVuSoBZnx6U4vzUxf5w'
constant TW_CONSUMER_SECRET (line 9) | const TW_CONSUMER_SECRET = 'Bcs59EFbbsdF6Sl9Ng71smgStWEGwXXKSjYvPVt7qys'
constant TW_ANDROID_BEARER_TOKEN (line 10) | const TW_ANDROID_BEARER_TOKEN = 'Bearer AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAA...
constant TW_ANDROID_PREFIX (line 12) | const TW_ANDROID_PREFIX = 'https://na.albtls.t.co'
constant TW_WEBAPI_PREFIX (line 13) | const TW_WEBAPI_PREFIX = 'https://api.twitter.com'
FILE: libs/core/Core.fetch.mjs
constant TW_AUTHORIZATION2 (line 57) | const TW_AUTHORIZATION2 = 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzU...
constant TW_AUTHORIZATION (line 58) | const TW_AUTHORIZATION = TW_AUTHORIZATION2// old token was expired
constant TWEETDECK_AUTHORIZATION2 (line 60) | const TWEETDECK_AUTHORIZATION2 = 'Bearer AAAAAAAAAAAAAAAAAAAAAFQODgEAAAA...
constant TW_WEBAPI_PREFIX (line 62) | const TW_WEBAPI_PREFIX = 'https://api.x.com'
constant TW_ANDROID_PREFIX (line 63) | const TW_ANDROID_PREFIX = 'https://global.albtls.t.co'
constant TW_ANDROID_SEARCH_PREFIX (line 64) | const TW_ANDROID_SEARCH_PREFIX = 'https://na.albtls.t.co'
FILE: libs/core/Core.function.mjs
class setGlobalServerInfo (line 8) | class setGlobalServerInfo {
method constructor (line 25) | constructor() {
method value (line 30) | get value() {
method getValue (line 33) | getValue(key = '') {
method updateValue (line 39) | updateValue(key = '', value = 1) {
class GuestToken (line 47) | class GuestToken {
method constructor (line 53) | constructor(type = 'browser') {
method openAccountInit (line 57) | async openAccountInit(openAccount = null, env = {}) {
method updateGuestToken (line 86) | async updateGuestToken(authorizationMode = 0, rateLimitOnly = false, e...
method updateRateLimit (line 120) | updateRateLimit(key = '', value = 1) {
method getRateLimit (line 127) | getRateLimit(key = '') {
method preCheck (line 133) | preCheck(key = '', value = 1) {
method token (line 139) | get token() {
class GuestAccount (line 144) | class GuestAccount {
method constructor (line 148) | constructor(pool_link, accounts = []) {
method UpdatePoolLink (line 152) | UpdatePoolLink(pool_link = '') {
method AddNewAccounts (line 161) | AddNewAccounts(replace = false, accounts = []) {
method GetNewAccountsByCFKV (line 168) | async GetNewAccountsByCFKV(kv, replace = false, count = 5) {
method GetNewAccountsByRemote (line 183) | async GetNewAccountsByRemote(replace = false) {
method RemoveUselessAccounts (line 199) | RemoveUselessAccounts(screen_name = '') {
method Link (line 207) | get Link() {
method List (line 210) | get List() {
method RandomItem (line 213) | get RandomItem() {
class Login (line 219) | class Login {
method constructor (line 224) | constructor(guest_token, cookie = {}, flow_token = '') {
method pureCookie (line 231) | get pureCookie() {
method getItem (line 234) | getItem(itemName = 'cookie') {
method updateItems (line 241) | updateItems(flowData = {}) {
method Init (line 255) | async Init() {
method LoginJsInstrumentationSubtask (line 261) | async LoginJsInstrumentationSubtask() {
method LoginEnterUserIdentifierSSO (line 281) | async LoginEnterUserIdentifierSSO(account = '') {
method LoginEnterAlternateIdentifierSubtask (line 306) | async LoginEnterAlternateIdentifierSubtask(screen_name = '') {
method LoginEnterPassword (line 322) | async LoginEnterPassword(password) {
method AccountDuplicationCheck (line 338) | async AccountDuplicationCheck() {
method LoginTwoFactorAuthChallenge (line 354) | async LoginTwoFactorAuthChallenge(_2fa) {
method LoginTwoFactorAuthChooseMethod (line 372) | async LoginTwoFactorAuthChooseMethod(type = '0') {
method LoginAcid (line 392) | async LoginAcid(acid) {
method Viewer (line 409) | async Viewer() {
FILE: libs/core/Core.xClientTransactionID.mjs
constant ADDITIONAL_RANDOM_NUMBER (line 5) | const ADDITIONAL_RANDOM_NUMBER = 3
class UnitBezier (line 64) | class UnitBezier {
method constructor (line 84) | constructor(p1x, p1y, p2x, p2y) {
method sampleCurveX (line 106) | sampleCurveX(t) {
method sampleCurveY (line 110) | sampleCurveY(t) {
method sampleCurveDerivativeX (line 114) | sampleCurveDerivativeX(t) {
method solveCurveX (line 118) | solveCurveX(x, epsilon) {
method solve (line 161) | solve(x, epsilon) {
function trimRight (line 168) | function trimRight(str, char) {
function encode (line 176) | function encode(n) {
function interpolate (line 186) | function interpolate(from, to, value) {
function interpolateNum (line 194) | function interpolateNum(from, to, value) {
function convertRotationToMatrix (line 198) | function convertRotationToMatrix(degrees) {
function doAnimation (line 214) | function doAnimation(numArr, frameTime) {
function sha256 (line 270) | function sha256(textEncoder) {
function timeToBytes (line 273) | function timeToBytes(val) {
function GetFrame (line 280) | function GetFrame(curFrame = '') {
function ParseTwitterMainPage (line 295) | function ParseTwitterMainPage(strPage = '', objValue = {}) {
function ParseOndemandS (line 338) | function ParseOndemandS(fileStr = '', objValue = {}) {
FILE: libs/share/MockFuntions.mjs
class MockDocument (line 22) | class MockDocument {
method constructor (line 24) | constructor() {
method createElement (line 28) | createElement(tagName) {
method getElementsByTagName (line 47) | getElementsByTagName(tagName) {
FILE: tests/mock/express.js
class MockExpress (line 9) | class MockExpress {
method constructor (line 24) | constructor() {
method updateGuestToken (line 28) | updateGuestToken() {
method init (line 31) | init(url = '', params = [], body = '', type = '') {
method setEnv (line 45) | setEnv(k, v) {
method req (line 49) | get req() {
method res (line 94) | get res() {
Condensed preview — 87 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,267K chars).
[
{
"path": ".gitignore",
"chars": 1072,
"preview": "apps/crawler/savetweets/*\n\napps/scripts/t.mjs\napps/scripts/save_spaces/\napps/scripts/astParser/\napps/backend/cache/*\napp"
},
{
"path": ".prettierignore",
"chars": 852,
"preview": "apps/crawler/savetweets/*\n\napps/scripts/t.mjs\napps/scripts/save_spaces/\napps/scripts/astParser/\napps/backend/cache/*\napp"
},
{
"path": ".prettierrc.json",
"chars": 140,
"preview": "{\n \"useTabs\": false,\n \"tabWidth\": 4,\n \"singleQuote\": true,\n \"trailingComma\": \"none\",\n \"printWidth\": 250,\n"
},
{
"path": ".yarnrc.yml",
"chars": 117,
"preview": "compressionLevel: mixed\n\nenableGlobalCache: false\n\nnodeLinker: node-modules\n\nyarnPath: .yarn/releases/yarn-4.5.0.cjs\n"
},
{
"path": "LICENSE",
"chars": 1073,
"preview": "MIT License\n\nCopyright (c) 2022-present BANKA2017\n\nPermission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "README.MD",
"chars": 13705,
"preview": "# Twitter Monitor v3 monorepo (RE?)\n\n---\n\n## ⚠ WARNING / 警告\n\nMarch 2026 Update: A large amount of content was removed. T"
},
{
"path": "apps/backend/CoreFunctions/album/Album.mjs",
"chars": 5973,
"preview": "import { isEmpty, isObject } from 'lodash-es'\nimport { getEmbedConversation, getTweets } from '../../../../libs/core/Cor"
},
{
"path": "apps/backend/CoreFunctions/online/OnlineLogin.mjs",
"chars": 7170,
"preview": "import { postLogout } from '../../../../libs/core/Core.fetch.mjs'\nimport { Log, Login, VerifyQueryString } from '../../."
},
{
"path": "apps/backend/CoreFunctions/online/OnlineMisc.mjs",
"chars": 11588,
"preview": "import { GenerateAccountInfo, GenerateCommunityInfo } from '../../../../libs/core/Core.info.mjs'\nimport { getCommunityIn"
},
{
"path": "apps/backend/CoreFunctions/online/OnlineTrends.mjs",
"chars": 1473,
"preview": "import { getTrends } from '../../../../libs/core/Core.fetch.mjs'\nimport { Log } from '../../../../libs/core/Core.functio"
},
{
"path": "apps/backend/CoreFunctions/online/OnlineTweet.mjs",
"chars": 34190,
"preview": "import { Parser } from 'm3u8-parser'\nimport path2array from '../../../../libs/core/Core.apiPath.mjs'\nimport { getAudioSp"
},
{
"path": "apps/backend/CoreFunctions/online/OnlineUserInfo.mjs",
"chars": 2221,
"preview": "import { GenerateAccountInfo } from '../../../../libs/core/Core.info.mjs'\nimport { getUserInfo } from '../../../../libs/"
},
{
"path": "apps/backend/CoreFunctions/translate/OnlineTranslate.mjs",
"chars": 3946,
"preview": "import { getTranslate } from '../../../../libs/core/Core.fetch.mjs'\nimport { Log, VerifyQueryString } from '../../../../"
},
{
"path": "apps/backend/CoreFunctions/translate/Translate.mjs",
"chars": 835,
"preview": "import { VerifyQueryString } from '../../../../libs/core/Core.function.mjs'\nimport { Translate } from '../../../../libs/"
},
{
"path": "apps/backend/app.mjs",
"chars": 7328,
"preview": "import express from 'express'\nimport { Log, GuestToken, GuestAccount } from '../../libs/core/Core.function.mjs'\nimport {"
},
{
"path": "apps/backend/service/album.mjs",
"chars": 1632,
"preview": "import express from 'express'\n\nimport { ApiUserInfo } from '../CoreFunctions/online/OnlineUserInfo.mjs'\nimport { ApiTwee"
},
{
"path": "apps/backend/service/online.mjs",
"chars": 4938,
"preview": "import express from 'express'\nimport { ApiUserInfo } from '../CoreFunctions/online/OnlineUserInfo.mjs'\nimport { ApiTweet"
},
{
"path": "apps/backend/service/translate.mjs",
"chars": 666,
"preview": "import express from 'express'\nimport { ApiPredict } from '../CoreFunctions/translate/Translate.mjs'\nimport { ApiOfficial"
},
{
"path": "apps/backend/share.mjs",
"chars": 1094,
"preview": "import { existsSync, writeFileSync } from 'fs'\nimport { basePath } from '../../libs/share/NodeConstant.mjs'\nimport { Log"
},
{
"path": "apps/backend/static/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "apps/backend/static/xml/rss.xsl",
"chars": 3877,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xsl:stylesheet version=\"3.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\" xm"
},
{
"path": "apps/online_tools/config.html",
"chars": 24401,
"preview": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n\n<head>\n <meta name=\"robots\" content=\"nofollow\">\n <meta charset=\"utf-8\">\n <"
},
{
"path": "apps/online_tools/oauth_signature_builder.html",
"chars": 13306,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n <meta name=\"robots\" content=\"nofollow\">\n <meta charset=\"utf-8\">\n <met"
},
{
"path": "apps/online_tools/snowflake.html",
"chars": 8349,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n <meta name=\"robots\" content=\"nofollow\">\n <meta charset=\"utf-8\">\n <met"
},
{
"path": "apps/online_tools/webpush.html",
"chars": 63720,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n <meta name=\"robots\" content=\"nofollow\">\n <meta charset=\"utf-8\">\n <met"
},
{
"path": "apps/online_tools/x_client_transaction_id.html",
"chars": 28850,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n <meta name=\"robots\" content=\"nofollow\">\n <meta charset=\"utf-8\">\n <met"
},
{
"path": "apps/open_account/readme.md",
"chars": 4850,
"preview": "# Open Accounts\n\n---\n\n## What is open account\n\nOpen Account is the account used for OAuth requests, which can come from "
},
{
"path": "apps/open_account/scripts/get_guest_token.js",
"chars": 6195,
"preview": "// Node v18.15.0 / Deno / Bun...\n\nconst TW_CONSUMER_KEY = '3nVuSoBZnx6U4vzUxf5w'\nconst TW_CONSUMER_SECRET = 'Bcs59EFbbsd"
},
{
"path": "apps/open_account/scripts/get_open_account_info.mjs",
"chars": 1097,
"preview": "import { coreFetch } from '../../../libs/core/Core.fetch.mjs'\nimport { GuestToken, Log } from '../../../libs/core/Core.f"
},
{
"path": "apps/open_account/scripts/login.mjs",
"chars": 6362,
"preview": "// thanks https://github.com/zedeus/nitter/issues/983#issuecomment-169002582\n// and RSSHub https://github.com/DIYgod/RSS"
},
{
"path": "apps/open_account/scripts/proxy.txt",
"chars": 201,
"preview": "# if one line is not starts with `http`, script will ignore it\n# <- ignore\n# https://192.168.1.100:7890 <- ignore\n# http"
},
{
"path": "apps/rate_limit_checker/data/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "apps/rate_limit_checker/readme.md",
"chars": 2161,
"preview": "# Rate limit checker\n\n---\n\nRate limit checker is a tool to check rate limit of twitter api.\n\nView data: <https://github."
},
{
"path": "apps/rate_limit_checker/run.mjs",
"chars": 14215,
"preview": "import { writeFileSync } from 'fs'\nimport { getBearerToken, postOpenAccountInit } from '../../libs/core/Core.android.mjs"
},
{
"path": "apps/scripts/apiPathGenerator.mjs",
"chars": 6219,
"preview": "import { writeFileSync } from 'node:fs'\nimport { basePath } from '../../libs/share/NodeConstant.mjs'\n\nconst apiPathList "
},
{
"path": "apps/scripts/loginflow.js",
"chars": 2061,
"preview": "import { Log, GuestToken, Login } from '../../libs/core/Core.function.mjs'\nimport { authenticator } from 'otplib'\n\n/*\n- "
},
{
"path": "apps/scripts/updateAndroidQueryIdList.mjs",
"chars": 8849,
"preview": "import { writeFileSync } from 'fs'\nimport { basePath } from '../../libs/share/NodeConstant.mjs'\nimport { Log } from '../"
},
{
"path": "apps/scripts/updateQueryIdList.mjs",
"chars": 9501,
"preview": "import { writeFileSync } from 'fs'\nimport { basePath } from '../../libs/share/NodeConstant.mjs'\nimport axiosFetch from '"
},
{
"path": "apps/web_push/callback.mjs",
"chars": 720,
"preview": "const callback = async (dataObject, ...otherArgs) => {\n // do anything with the data\n // save...\n globalThis._c"
},
{
"path": "apps/web_push/config.mjs",
"chars": 1511,
"preview": "//--> node.js/deno/bun...\nimport { existsSync, readFileSync, writeFileSync } from 'fs'\n//<--\n\nexport default class Confi"
},
{
"path": "apps/web_push/config_example.json",
"chars": 391,
"preview": "{\n \"twitter\": {\n \"screen_name\": \"\",\n \"password\": \"\",\n \"authentication_secret\": \"\",\n \"retr"
},
{
"path": "apps/web_push/decrypt.mjs",
"chars": 6867,
"preview": "//-> node.js only\nimport crypto from 'crypto'\n//<--\nimport { base64_to_base64url, base64_to_buffer, base64url_to_base64,"
},
{
"path": "apps/web_push/package.json",
"chars": 181,
"preview": "{\n \"name\": \"tmv3-web-push\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"type\": \"module\",\n \"dependencies\": {\n"
},
{
"path": "apps/web_push/readme.md",
"chars": 5372,
"preview": "# ↑~ Web*Push X!↓\n\n## ⚠️ Warning\n\nPlease note that using this program may result in the suspension of your twitter accou"
},
{
"path": "apps/web_push/twitter.mjs",
"chars": 17621,
"preview": "//-->\n//TODO use native lib instead of otplib\nimport { authenticator } from 'otplib'\nimport { base64_to_base64url, buffe"
},
{
"path": "apps/web_push/utils.mjs",
"chars": 1631,
"preview": "export const base64_to_buffer = (base64 = '') => {\n let binaryString = atob(base64)\n let bytes = new Uint8Array(bi"
},
{
"path": "apps/web_push/web_push.mjs",
"chars": 3991,
"preview": "import Config from './config.mjs'\nimport Decrypt from './decrypt.mjs'\nimport Twitter, { VAPID, loginToTwitter, setupTwit"
},
{
"path": "apps/web_push/websocket.mjs",
"chars": 6438,
"preview": "//-> node.js only\nimport { WebSocket } from 'ws'\nimport crypto from 'crypto'\n//<--\nimport { fireFoxUserAgent } from './t"
},
{
"path": "libs/README.md",
"chars": 411,
"preview": "Twitter monitor core files\n---\n\nCore files for twitter monitor\n\n```plaintext\n- assets/\n- core/\n- model/\n- share/\n```\n\n* "
},
{
"path": "libs/assets/config_sample.json",
"chars": 505,
"preview": "{\n \"users\": [\n {\n \"name\": \"Example_user\",\n \"display_name\": \"Example user\",\n \""
},
{
"path": "libs/assets/graphql/androidQueryIdList.js",
"chars": 9248,
"preview": "export const _UserResultByIdQuery = {\"queryId\":\"8BTUdO2H4nAu26mgdE7_aQ\",\"operationName\":\"UserResultByIdQuery\",\"operation"
},
{
"path": "libs/assets/graphql/featuresValueList.js",
"chars": 184833,
"preview": "export const _2fa_temporary_password_enabled = false\nexport const _account_country_setting_countries_whitelist = [\"ad\",\""
},
{
"path": "libs/assets/graphql/featuresValueList.json",
"chars": 77042,
"preview": "{\n \"2fa_temporary_password_enabled\": false,\n \"account_country_setting_countries_whitelist\": [\n \"ad\",\n "
},
{
"path": "libs/assets/graphql/graphqlQueryIdList.js",
"chars": 546900,
"preview": "export const _UserPreferences = {\"queryId\":\"xFxU-O8hEYe74ovNVU74jA\",\"operationName\":\"UserPreferences\",\"operationType\":\"q"
},
{
"path": "libs/assets/graphql/graphqlQueryIdList.json",
"chars": 702379,
"preview": "{\n \"UserPreferences\": {\n \"queryId\": \"xFxU-O8hEYe74ovNVU74jA\",\n \"operationName\": \"UserPreferences\",\n "
},
{
"path": "libs/assets/setting_sample.mjs",
"chars": 2383,
"preview": "import { basePath } from '../share/NodeConstant.mjs'\n\n/*\n.service\n twitter_monitor: the latest version of twitter monit"
},
{
"path": "libs/core/Core.Rss.mjs",
"chars": 1134,
"preview": "export class Rss {\n rss\n channelObject = {}\n itemArray = []\n\n channel(channelObject, addMode = false) {\n "
},
{
"path": "libs/core/Core.android.mjs",
"chars": 9545,
"preview": "import axiosFetch from 'axios-helper'\nimport { coreFetch, preCheckCtx } from './Core.fetch.mjs'\nimport cryptoHandle from"
},
{
"path": "libs/core/Core.apiPath.mjs",
"chars": 6899,
"preview": "const path2array = (pathName = '', source = {}) => {\n const tmpPath = {\n \"rest_id\": () => source?.id_str ?? so"
},
{
"path": "libs/core/Core.blurhash.mjs",
"chars": 1000,
"preview": "import { encode } from 'blurhash'\nimport sharp from 'sharp'\nimport axiosFetch from 'axios-helper'\n\n//https://github.com/"
},
{
"path": "libs/core/Core.fetch.mjs",
"chars": 86924,
"preview": "import path2array from './Core.apiPath.mjs'\n\nimport {\n _AudioSpaceById,\n _Bookmarks,\n _CommunityQuery,\n _Com"
},
{
"path": "libs/core/Core.function.mjs",
"chars": 21874,
"preview": "import { cloneDeep, shuffle } from 'lodash-es'\nimport { getBearerToken, postOpenAccount, postOpenAccountInit } from './C"
},
{
"path": "libs/core/Core.info.mjs",
"chars": 5887,
"preview": "import { VerifiedInt } from '../share/Constant.mjs'\nimport path2array from './Core.apiPath.mjs'\n\nconst GenerateAccountIn"
},
{
"path": "libs/core/Core.push.mjs",
"chars": 1166,
"preview": "import { ALERT_TOKEN, ALERT_PUSH_TO } from '../../libs/assets/setting.mjs'\nimport axiosFetch from 'axios-helper'\nimport "
},
{
"path": "libs/core/Core.translate.mjs",
"chars": 5192,
"preview": "//Translator\nimport Translator, { IsChs, IsCht } from '@kdwnil/translator-utils'\nimport { Log, GetEntitiesFromText } fro"
},
{
"path": "libs/core/Core.tweet.mjs",
"chars": 70428,
"preview": "import { SupportedCardNameList } from '../share/Constant.mjs'\nimport { Log, GetEntitiesFromText, PathInfo, IsNumber } fr"
},
{
"path": "libs/core/Core.xClientTransactionID.mjs",
"chars": 13447,
"preview": "import cryptoHandle from 'crypto-helper'\nimport { JSDOM } from 'jsdom'\n\nconst keyWord = 'obfiowerehiring'\nconst ADDITION"
},
{
"path": "libs/share/Constant.mjs",
"chars": 2071,
"preview": "//for cards\nconst SupportedCardNameList = [\n 'summary',\n 'summary_large_image',\n 'promo_website',\n 'audio',\n"
},
{
"path": "libs/share/Mime.mjs",
"chars": 5774,
"preview": "//loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooon"
},
{
"path": "libs/share/MockFuntions.mjs",
"chars": 1491,
"preview": "const PregMatchAll = (regex = new RegExp('', 'gm'), text = '') => {\n let handle\n let match = []\n\n while ((handl"
},
{
"path": "libs/share/NodeConstant.mjs",
"chars": 223,
"preview": "import { fileURLToPath } from 'node:url'\nimport { dirname } from 'node:path'\n\nconst __filename = fileURLToPath(import.me"
},
{
"path": "package.json",
"chars": 1282,
"preview": "{\n \"name\": \"tmv3\",\n \"version\": \"2.0.0\",\n \"type\": \"module\",\n \"private\": true,\n \"license\": \"MIT\",\n \"scri"
},
{
"path": "packages/axios-helper/README.md",
"chars": 91,
"preview": "Axios helper\n---\n\ncreate axios handle between `redaxios`(for browser) and `axios`(for node)"
},
{
"path": "packages/axios-helper/index.js",
"chars": 720,
"preview": "import axios from 'redaxios'\n\nconst axiosFetch = (config = {}) => {\n let axiosConfig = {\n timeout: 30000, //TO"
},
{
"path": "packages/axios-helper/index.node.js",
"chars": 2435,
"preview": "import axios from 'axios'\nimport { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'\nimport { Agent as httpsAgent } from "
},
{
"path": "packages/axios-helper/package.json",
"chars": 459,
"preview": "{\n \"name\": \"axios-helper\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"main\": \"index.js\",\n \"exports\": {\n "
},
{
"path": "packages/crypto-helper/index.js",
"chars": 56,
"preview": "const cryptoHandle = crypto\nexport default cryptoHandle\n"
},
{
"path": "packages/crypto-helper/index.node.js",
"chars": 94,
"preview": "import { webcrypto } from 'crypto'\nconst cryptoHandle = webcrypto\nexport default cryptoHandle\n"
},
{
"path": "packages/crypto-helper/package.json",
"chars": 497,
"preview": "{\n \"name\": \"crypto-helper\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"main\": \"index.node.js\",\n \"types\": \"i"
},
{
"path": "packages/get-mime/index.js",
"chars": 2784,
"preview": "//jpeg FF D8 FF image/jpeg\n//png 89 50 4E 47 0D 0A 1A 0A\n//webp 52 49 46 46 ?? ?? ?? ?? 57 45 42 50 image/webp\n//gif87a "
},
{
"path": "packages/get-mime/package.json",
"chars": 290,
"preview": "{\n \"name\": \"get-mime\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"type\": \"module\",\n \"main\": \"index.js\",\n "
},
{
"path": "tests/backend.online.test.js",
"chars": 16435,
"preview": "/*\n Mock express.js Node.js 18.x required\n `nvm use 18`\n Twitter Monitor v3 test\n @BANKA2017 && NEST.MOE\n*/\nimport {"
},
{
"path": "tests/core.fetch.android.test.js",
"chars": 6782,
"preview": "import { describe, expect, test } from 'vitest'\nimport { GuestToken } from '../libs/core/Core.function.mjs'\nimport {\n "
},
{
"path": "tests/core.fetch.anonymous.test.js",
"chars": 10765,
"preview": "import { describe, expect, it, test } from 'vitest'\nimport { GuestToken } from '../libs/core/Core.function.mjs'\nimport {"
},
{
"path": "tests/mock/express.js",
"chars": 2977,
"preview": "/*\n Mock express.js Node.js 18.x required\n Twitter Monitor v3 test\n @BANKA2017 && NEST.MOE\n*/\n\nimport { GuestToken } "
},
{
"path": "tests/mock.express.test.js",
"chars": 3170,
"preview": "/*\n Mock express.js Node.js 18.x required\n `nvm use 18`\n Twitter Monitor v3 test\n @BANKA2017 && NEST.MOE\n*/\n\nimport "
},
{
"path": "vitest.config.js",
"chars": 174,
"preview": "import { defineConfig } from 'vitest/config'\nexport default defineConfig({\n test: {\n include: ['tests/*.test.j"
}
]
About this extraction
This page contains the full source code of the BANKA2017/twitter-monitor GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 87 files (2.1 MB), approximately 542.9k tokens, and a symbol index with 149 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.