This commit is contained in:
NAME
2026-05-11 08:02:34 +00:00
parent c72a38201a
commit b5cf2502be
13 changed files with 534 additions and 35 deletions
+3
View File
@@ -27,8 +27,10 @@
"@nestjs/config": "^4.0.4", "@nestjs/config": "^4.0.4",
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.4.2",
"@twitter-api-v2/plugin-token-refresher": "^1.0.0", "@twitter-api-v2/plugin-token-refresher": "^1.0.0",
"axios": "^1.16.0", "axios": "^1.16.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.15.1", "class-validator": "^0.15.1",
"cookie": "^1.1.1", "cookie": "^1.1.1",
"https-proxy-agent": "^9.0.0", "https-proxy-agent": "^9.0.0",
@@ -37,6 +39,7 @@
"playwright-extra": "^4.3.6", "playwright-extra": "^4.3.6",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"swagger-ui-express": "^5.0.1",
"twitter-api-v2": "^1.29.0" "twitter-api-v2": "^1.29.0"
}, },
"devDependencies": { "devDependencies": {
+125 -22
View File
@@ -16,25 +16,31 @@ importers:
version: 5.1.6(keyv@5.6.0) version: 5.1.6(keyv@5.6.0)
'@nestjs/cache-manager': '@nestjs/cache-manager':
specifier: ^3.1.2 specifier: ^3.1.2
version: 3.1.2(@nestjs/common@11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2) version: 3.1.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2)
'@nestjs/common': '@nestjs/common':
specifier: ^11.0.1 specifier: ^11.0.1
version: 11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) version: 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/config': '@nestjs/config':
specifier: ^4.0.4 specifier: ^4.0.4
version: 4.0.4(@nestjs/common@11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) version: 4.0.4(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)
'@nestjs/core': '@nestjs/core':
specifier: ^11.0.1 specifier: ^11.0.1
version: 11.1.19(@nestjs/common@11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) version: 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/platform-express': '@nestjs/platform-express':
specifier: ^11.0.1 specifier: ^11.0.1
version: 11.1.19(@nestjs/common@11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19) version: 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)
'@nestjs/swagger':
specifier: ^11.4.2
version: 11.4.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)
'@twitter-api-v2/plugin-token-refresher': '@twitter-api-v2/plugin-token-refresher':
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0(twitter-api-v2@1.29.0) version: 1.0.0(twitter-api-v2@1.29.0)
axios: axios:
specifier: ^1.16.0 specifier: ^1.16.0
version: 1.16.0 version: 1.16.0
class-transformer:
specifier: ^0.5.1
version: 0.5.1
class-validator: class-validator:
specifier: ^0.15.1 specifier: ^0.15.1
version: 0.15.1 version: 0.15.1
@@ -59,6 +65,9 @@ importers:
rxjs: rxjs:
specifier: ^7.8.1 specifier: ^7.8.1
version: 7.8.2 version: 7.8.2
swagger-ui-express:
specifier: ^5.0.1
version: 5.0.1(express@5.2.1)
twitter-api-v2: twitter-api-v2:
specifier: ^1.29.0 specifier: ^1.29.0
version: 1.29.0 version: 1.29.0
@@ -77,7 +86,7 @@ importers:
version: 11.1.0(chokidar@4.0.3)(prettier@3.8.3)(typescript@5.9.3) version: 11.1.0(chokidar@4.0.3)(prettier@3.8.3)(typescript@5.9.3)
'@nestjs/testing': '@nestjs/testing':
specifier: ^11.0.1 specifier: ^11.0.1
version: 11.1.19(@nestjs/common@11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/platform-express@11.1.19) version: 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/platform-express@11.1.19)
'@types/express': '@types/express':
specifier: ^5.0.0 specifier: ^5.0.0
version: 5.0.6 version: 5.0.6
@@ -800,6 +809,9 @@ packages:
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
engines: {node: '>=8'} engines: {node: '>=8'}
'@microsoft/tsdoc@0.16.0':
resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==}
'@napi-rs/wasm-runtime@0.2.12': '@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
@@ -862,6 +874,19 @@ packages:
'@nestjs/websockets': '@nestjs/websockets':
optional: true optional: true
'@nestjs/mapped-types@2.1.1':
resolution: {integrity: sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==}
peerDependencies:
'@nestjs/common': ^10.0.0 || ^11.0.0
class-transformer: ^0.4.0 || ^0.5.0
class-validator: ^0.13.0 || ^0.14.0 || ^0.15.0
reflect-metadata: ^0.1.12 || ^0.2.0
peerDependenciesMeta:
class-transformer:
optional: true
class-validator:
optional: true
'@nestjs/platform-express@11.1.19': '@nestjs/platform-express@11.1.19':
resolution: {integrity: sha512-Vpdv8jyCQdThfoTx+UTn+DRYr6H6X02YUqcpZ3qP6G3ZUwtVp7eS+hoQPGd4UuCnlnFG8Wqr2J9bGEzQdi1rIg==} resolution: {integrity: sha512-Vpdv8jyCQdThfoTx+UTn+DRYr6H6X02YUqcpZ3qP6G3ZUwtVp7eS+hoQPGd4UuCnlnFG8Wqr2J9bGEzQdi1rIg==}
peerDependencies: peerDependencies:
@@ -877,6 +902,23 @@ packages:
prettier: prettier:
optional: true optional: true
'@nestjs/swagger@11.4.2':
resolution: {integrity: sha512-aBihEogDMj/bLEcaqhkvyX/ZVWUw/bmnhKzR0zwUoyGJikvZyaq7rOPYl/H7Lxkkr3c90SJxyuv1AX2UT1WKlw==}
peerDependencies:
'@fastify/static': ^8.0.0 || ^9.0.0
'@nestjs/common': ^11.0.1
'@nestjs/core': ^11.0.1
class-transformer: '*'
class-validator: '*'
reflect-metadata: ^0.1.12 || ^0.2.0
peerDependenciesMeta:
'@fastify/static':
optional: true
class-transformer:
optional: true
class-validator:
optional: true
'@nestjs/testing@11.1.19': '@nestjs/testing@11.1.19':
resolution: {integrity: sha512-/UFNWXvPEdu4v4DlC5oWLbGKmD27LehLK06b8oLzs6D6lf4vAQTdST8LRAXBadyMUQnVEQWMuBo3CtAVtlfXtQ==} resolution: {integrity: sha512-/UFNWXvPEdu4v4DlC5oWLbGKmD27LehLK06b8oLzs6D6lf4vAQTdST8LRAXBadyMUQnVEQWMuBo3CtAVtlfXtQ==}
peerDependencies: peerDependencies:
@@ -925,6 +967,9 @@ packages:
'@opentelemetry/api': '@opentelemetry/api':
optional: true optional: true
'@scarf/scarf@1.4.0':
resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
'@sinclair/typebox@0.34.49': '@sinclair/typebox@0.34.49':
resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==} resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==}
@@ -1704,6 +1749,9 @@ packages:
cjs-module-lexer@2.2.0: cjs-module-lexer@2.2.0:
resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==}
class-transformer@0.5.1:
resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==}
class-validator@0.15.1: class-validator@0.15.1:
resolution: {integrity: sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==} resolution: {integrity: sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==}
@@ -3149,6 +3197,18 @@ packages:
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
swagger-ui-dist@5.32.4:
resolution: {integrity: sha512-0AADFFQNJzExEN49SrD/34Nn9cxNxVLiydYl2MBwSZFPVXNkVwC/EFAjoezGGqE8oDegiDC+p47t8lKObCinMQ==}
swagger-ui-dist@5.32.5:
resolution: {integrity: sha512-7/FQfWe9A4qoyYFdAwy0chD0uDYidDp/ZT9VQ9LZlgD4AnnHJk8/+ytAA1HkJYOPySmK6helPDdJQMlcumt7HA==}
swagger-ui-express@5.0.1:
resolution: {integrity: sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==}
engines: {node: '>= v0.10.32'}
peerDependencies:
express: '>=4.0.0 || >=5.0.0-beta'
symbol-observable@4.0.0: symbol-observable@4.0.0:
resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==}
engines: {node: '>=0.10'} engines: {node: '>=0.10'}
@@ -4517,6 +4577,8 @@ snapshots:
'@lukeed/csprng@1.1.0': {} '@lukeed/csprng@1.1.0': {}
'@microsoft/tsdoc@0.16.0': {}
'@napi-rs/wasm-runtime@0.2.12': '@napi-rs/wasm-runtime@0.2.12':
dependencies: dependencies:
'@emnapi/core': 1.10.0 '@emnapi/core': 1.10.0
@@ -4524,10 +4586,10 @@ snapshots:
'@tybys/wasm-util': 0.10.2 '@tybys/wasm-util': 0.10.2
optional: true optional: true
'@nestjs/cache-manager@3.1.2(@nestjs/common@11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2)': '@nestjs/cache-manager@3.1.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2)':
dependencies: dependencies:
'@nestjs/common': 11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2)
cache-manager: 7.2.8 cache-manager: 7.2.8
keyv: 5.6.0 keyv: 5.6.0
rxjs: 7.8.2 rxjs: 7.8.2
@@ -4559,7 +4621,7 @@ snapshots:
- uglify-js - uglify-js
- webpack-cli - webpack-cli
'@nestjs/common@11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': '@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)':
dependencies: dependencies:
file-type: 21.3.4 file-type: 21.3.4
iterare: 1.2.1 iterare: 1.2.1
@@ -4569,21 +4631,22 @@ snapshots:
tslib: 2.8.1 tslib: 2.8.1
uid: 2.0.2 uid: 2.0.2
optionalDependencies: optionalDependencies:
class-transformer: 0.5.1
class-validator: 0.15.1 class-validator: 0.15.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@nestjs/config@4.0.4(@nestjs/common@11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': '@nestjs/config@4.0.4(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)':
dependencies: dependencies:
'@nestjs/common': 11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
dotenv: 17.4.1 dotenv: 17.4.1
dotenv-expand: 12.0.3 dotenv-expand: 12.0.3
lodash: 4.18.1 lodash: 4.18.1
rxjs: 7.8.2 rxjs: 7.8.2
'@nestjs/core@11.1.19(@nestjs/common@11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2)': '@nestjs/core@11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2)':
dependencies: dependencies:
'@nestjs/common': 11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nuxt/opencollective': 0.4.1 '@nuxt/opencollective': 0.4.1
fast-safe-stringify: 2.1.1 fast-safe-stringify: 2.1.1
iterare: 1.2.1 iterare: 1.2.1
@@ -4593,12 +4656,20 @@ snapshots:
tslib: 2.8.1 tslib: 2.8.1
uid: 2.0.2 uid: 2.0.2
optionalDependencies: optionalDependencies:
'@nestjs/platform-express': 11.1.19(@nestjs/common@11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19) '@nestjs/platform-express': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)
'@nestjs/platform-express@11.1.19(@nestjs/common@11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)': '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)':
dependencies: dependencies:
'@nestjs/common': 11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2
optionalDependencies:
class-transformer: 0.5.1
class-validator: 0.15.1
'@nestjs/platform-express@11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)':
dependencies:
'@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2)
cors: 2.8.6 cors: 2.8.6
express: 5.2.1 express: 5.2.1
multer: 2.1.1 multer: 2.1.1
@@ -4620,13 +4691,28 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- chokidar - chokidar
'@nestjs/testing@11.1.19(@nestjs/common@11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/platform-express@11.1.19)': '@nestjs/swagger@11.4.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)':
dependencies: dependencies:
'@nestjs/common': 11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@microsoft/tsdoc': 0.16.0
'@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/mapped-types': 2.1.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)
js-yaml: 4.1.1
lodash: 4.18.1
path-to-regexp: 8.4.2
reflect-metadata: 0.2.2
swagger-ui-dist: 5.32.4
optionalDependencies:
class-transformer: 0.5.1
class-validator: 0.15.1
'@nestjs/testing@11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/platform-express@11.1.19)':
dependencies:
'@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2)
tslib: 2.8.1 tslib: 2.8.1
optionalDependencies: optionalDependencies:
'@nestjs/platform-express': 11.1.19(@nestjs/common@11.1.19(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19) '@nestjs/platform-express': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)
'@noble/hashes@1.8.0': {} '@noble/hashes@1.8.0': {}
@@ -4649,6 +4735,8 @@ snapshots:
dependencies: dependencies:
cluster-key-slot: 1.1.2 cluster-key-slot: 1.1.2
'@scarf/scarf@1.4.0': {}
'@sinclair/typebox@0.34.49': {} '@sinclair/typebox@0.34.49': {}
'@sinonjs/commons@3.0.1': '@sinonjs/commons@3.0.1':
@@ -5588,6 +5676,8 @@ snapshots:
cjs-module-lexer@2.2.0: {} cjs-module-lexer@2.2.0: {}
class-transformer@0.5.1: {}
class-validator@0.15.1: class-validator@0.15.1:
dependencies: dependencies:
'@types/validator': 13.15.10 '@types/validator': 13.15.10
@@ -7167,6 +7257,19 @@ snapshots:
dependencies: dependencies:
has-flag: 4.0.0 has-flag: 4.0.0
swagger-ui-dist@5.32.4:
dependencies:
'@scarf/scarf': 1.4.0
swagger-ui-dist@5.32.5:
dependencies:
'@scarf/scarf': 1.4.0
swagger-ui-express@5.0.1(express@5.2.1):
dependencies:
express: 5.2.1
swagger-ui-dist: 5.32.5
symbol-observable@4.0.0: {} symbol-observable@4.0.0: {}
synckit@0.11.12: synckit@0.11.12:
+3 -1
View File
@@ -6,6 +6,7 @@ import {XPosterModule} from "./x-poster/x-poster.module";
import {ConfigModule} from "@nestjs/config"; import {ConfigModule} from "@nestjs/config";
import {CacheModule} from "@nestjs/cache-manager"; import {CacheModule} from "@nestjs/cache-manager";
import KeyvRedis from "@keyv/redis"; import KeyvRedis from "@keyv/redis";
import {XbotFollowModule} from "./xbot-follow/xbot-follow.module";
@Module({ @Module({
imports: [ imports: [
@@ -24,7 +25,8 @@ import KeyvRedis from "@keyv/redis";
}), }),
}), }),
SqsModule, SqsModule,
XPosterModule XPosterModule,
XbotFollowModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],
+23 -2
View File
@@ -1,12 +1,33 @@
import {NestFactory} from '@nestjs/core'; import {NestFactory} from '@nestjs/core';
import {AppModule} from './app.module'; import {AppModule} from './app.module';
import {SqsPosterWorker} from "./sqs-module/sqs.poster.worker"; import {SqsPosterWorker} from "./sqs-module/sqs.poster.worker";
import {DocumentBuilder, SwaggerModule} from "@nestjs/swagger";
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
const port = process.env.PORT || 3000; // Cấu hình Swagger
await app.listen(port, () => console.log(`Listening on port ${port}`)); const config = new DocumentBuilder()
.setTitle('X Poster API')
.setDescription('Mô tả chi tiết về các endpoint API')
.setVersion('1.0')
.addTag('users') // Phân nhóm API (tùy chọn)
.build();
const document = SwaggerModule.createDocument(app, config);
// Thiết lập đường dẫn truy cập tài liệu (ví dụ: http://localhost:3000/api)
SwaggerModule.setup('api', app, document);
const port = process.env.PORT || 3003;
await app.listen(port, () =>
console.log(`
🔥 X-Poster is running!
📡 API: http://localhost:${port}
📖 Swagger: http://localhost:${port}/docs
`)
);
await app.get(SqsPosterWorker).start(); await app.get(SqsPosterWorker).start();
+3 -9
View File
@@ -1,12 +1,6 @@
// src/modules/x-browser/x-browser.service.ts // src/modules/x-browser/x-browser.service.ts
import { import {HttpException, Injectable, Logger, OnModuleDestroy, OnModuleInit,} from '@nestjs/common';
Injectable, import {Browser, BrowserContext, chromium, Page} from 'playwright';
Logger,
OnModuleInit,
OnModuleDestroy, HttpException,
} from '@nestjs/common';
import {chromium, Browser, BrowserContext, Page} from 'playwright';
import {buildXCookies} from "./utils/x-headers.util";
import {rand} from "../helper"; import {rand} from "../helper";
export interface BrowserAccount { export interface BrowserAccount {
@@ -91,7 +85,7 @@ export class XBrowserService implements OnModuleInit, OnModuleDestroy {
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
'(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', '(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
viewport: {width: 1366, height: 768}, viewport: {width: 1366, height: 768},
locale: 'en-US', locale: process.env.BROWSER_LOCALE || 'en-US',
proxy: account.proxy ? {server: account.proxy} : undefined, proxy: account.proxy ? {server: account.proxy} : undefined,
}); });
console.log('getOrCreateContext:5') console.log('getOrCreateContext:5')
+1 -1
View File
@@ -77,7 +77,7 @@ export class XPosterRouterService {
}, },
], ],
proxy: '', proxy: '',
userAgent: '' userAgent: process.env.BROWSER_USER_AGENT || '',
}, },
}; };
@@ -0,0 +1,18 @@
import { IsString, IsOptional, IsInt, Min, Max } from 'class-validator';
import { Type } from 'class-transformer';
export class FollowFollowersDto {
@IsString()
targetUsername: string; // follow những ai đang follow người này
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
limit?: number = 10;
@IsOptional()
@Type(() => Boolean)
skipVerified?: boolean = false;
}
+7
View File
@@ -0,0 +1,7 @@
import { IsString, Length } from 'class-validator';
export class FollowOneDto {
@IsString()
@Length(1, 50)
username: string; // không có @, ví dụ: "elonmusk"
}
+88
View File
@@ -0,0 +1,88 @@
import {Injectable, Logger, OnModuleDestroy, OnModuleInit} from '@nestjs/common';
import {ConfigService} from '@nestjs/config';
import {Browser, BrowserContext, chromium, Page} from 'playwright';
@Injectable()
export class PlaywrightXService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(PlaywrightXService.name);
private browser: Browser;
private context: BrowserContext;
constructor(private readonly config: ConfigService) {
}
async onModuleInit() {
// Launch với stealth args để giảm bị detect
this.browser = await chromium.launch({
headless: false, // Để true khi đã test ổn định
args: [
'--disable-blink-features=AutomationControlled',
'--disable-features=IsolateOrigins,site-per-process',
'--disable-infobars',
'--no-sandbox',
'--window-size=1366,768',
],
});
this.context = await this.browser.newContext({
viewport: {width: 1366, height: 768},
userAgent: process.env.BROWSER_USER_AGENT ||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
'(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
locale: process.env.BROWSER_LOCALE || 'en-US',
permissions: ['notifications'],
});
// Pre-warm: set cookie rồi vào X kiểm tra login
await this.restoreSession();
}
async onModuleDestroy() {
await this.context?.close();
await this.browser?.close();
}
/** Tạo page mới từ context đã login */
async newPage(): Promise<Page> {
return this.context.newPage();
}
/** Set cookie auth_token, ct0, kdt vào context */
private async restoreSession() {
const authToken = this.config.get<string>('X_COOKIE_AUTH_TOKEN');
const ct0 = this.config.get<string>('X_COOKIE_CT0');
const kdt = this.config.get<string>('X_COOKIE_KDT') || '';
if (!authToken || !ct0) {
this.logger.warn('🚨 Thiếu TWITTER_AUTH_TOKEN hoặc CT0 trong .env');
return;
}
await this.context.addCookies([
{
name: 'auth_token',
value: authToken,
domain: '.x.com',
path: '/',
httpOnly: true,
secure: true,
sameSite: 'None'
},
{name: 'ct0', value: ct0, domain: '.x.com', path: '/', secure: true, sameSite: 'Lax'},
{name: 'kdt', value: kdt, domain: '.x.com', path: '/', secure: true, sameSite: 'Lax'},
]);
const page = await this.context.newPage();
await page.goto('https://x.com/home', {waitUntil: 'domcontentloaded', timeout: 30000});
await page.waitForTimeout(3000);
const isLoggedIn = await page
.locator('[data-testid="SideNav_AccountSwitcher_Button"], [data-testid="AppTabBar_Home_Link"]')
.first()
.isVisible()
.catch(() => false);
this.logger.log(`🔐 Session restore: ${isLoggedIn ? 'LOGGED IN' : 'GUEST (cookie có thể expired)'}`);
await page.close();
}
}
+21
View File
@@ -0,0 +1,21 @@
export interface FollowOneResult {
username: string;
success: boolean;
alreadyFollowing: boolean;
error?: string;
}
export interface FollowBatchResult {
targetSource: string;
totalScanned: number;
followed: string[]; // username đã follow thành công
skipped: Array<{ username: string; reason: string }>;
failed: Array<{ username: string; error: string }>;
}
export interface FollowFollowersOptions {
limit?: number; // mặc định 10
skipVerified?: boolean; // bỏ qua tick xanh (nếu detect được)
skipIfBioEmpty?: boolean; // chưa implement trong script cơ bản, để mở rộng
delayRange?: [number, number]; // [min, max] ms giữa các lần follow
}
+26
View File
@@ -0,0 +1,26 @@
import {Body, Controller, Get, Param, Post} from '@nestjs/common';
import {FollowFollowersDto} from './dto/follow-followers.dto';
import {XbotFollowService} from "./xbot-follow.service";
@Controller('x-auto')
export class XbotFollowController {
constructor(private readonly followService: XbotFollowService) {
}
/** GET /x-auto/follow { "username": "billgates" } */
@Get('follow/:username')
async followOne(@Param('username') username: string) {
const res = await this.followService.followOne(username);
return res;
}
/** POST /x-auto/follow/followers-of { "targetUsername": "elonmusk", "limit": 5 } */
@Post('follow/followers-of')
async followFollowers(@Body() dto: FollowFollowersDto) {
const res = await this.followService.followFollowersOf(dto.targetUsername, {
limit: dto.limit,
skipVerified: dto.skipVerified,
});
return res;
}
}
+16
View File
@@ -0,0 +1,16 @@
import {Global, Module} from "@nestjs/common";
import {XbotFollowController} from "./xbot-follow.controller";
import {XbotFollowService} from "./xbot-follow.service";
import {PlaywrightXService} from "./playwright-x.service";
@Global()
@Module({
imports: [],
providers: [
PlaywrightXService,
XbotFollowService
],
controllers: [XbotFollowController],
exports: [XbotFollowService],
})
export class XbotFollowModule {}
+200
View File
@@ -0,0 +1,200 @@
import {Injectable, Logger} from '@nestjs/common';
import {Page} from 'playwright';
import {PlaywrightXService} from './playwright-x.service';
import {FollowBatchResult, FollowFollowersOptions, FollowOneResult} from './types';
@Injectable()
export class XbotFollowService {
private readonly logger = new Logger(XbotFollowService.name);
private readonly sessionFollowed = new Set<string>(); // cache tránh follow lại trong cùng phiên
constructor(
private readonly pwService: PlaywrightXService,
// private readonly config: ConfigService,
) {}
// =================== 1. FOLLOW TRỰC TIẾP ===================
async followOne(username: string, page?: Page): Promise<FollowOneResult> {
const p = page ?? (await this.pwService.newPage());
const shouldClose = !page;
const target = username.replace(/^@/, '').toLowerCase();
this.logger.log(`➡️ Đang follow @${target}...`);
try {
await p.goto(`https://x.com/${target}`, {
waitUntil: 'domcontentloaded',
timeout: 30000,
});
await p.waitForLoadState('networkidle');
await this.humanDelay(1500, 3000);
// Check account tồn tại
if (await this.isTextPresent(p, /doesn\'t exist|Coundn.t be found/)) {
return { username: target, success: false, alreadyFollowing: false, error: 'Account not found' };
}
// Nếu đã follow rồi -> nút sẽ là "Following" (unfollowButton)
if (await this.isVisible(p, 'button[data-testid="unfollowButton"]')) {
this.logger.log(` → Already following @${target}`);
return { username: target, success: true, alreadyFollowing: true };
}
// Click Follow
const followBtn = p.locator('button[data-testid="followButton"]:not([disabled])').first();
if (await followBtn.isVisible().catch(() => false)) {
await followBtn.scrollIntoViewIfNeeded();
await this.humanDelay(400, 900);
await followBtn.click();
await this.humanDelay(1200, 2500);
// Verify chuyển sang Following
const success = await this.isVisible(p, 'button[data-testid="unfollowButton"]');
if (success) this.sessionFollowed.add(target);
return {
username: target,
success,
alreadyFollowing: false,
error: success ? undefined : 'Clicked but state did not change',
};
}
return { username: target, success: false, alreadyFollowing: false, error: 'Follow button not found' };
} catch (err: any) {
this.logger.error(` ❌ Lỗi follow @${target}: ${err.message}`);
return { username: target, success: false, alreadyFollowing: false, error: err.message };
} finally {
if (shouldClose) await p.close();
}
}
// =================== 2. FOLLOW FOLLOWERS CỦA USER CHỈ ĐỊNH ===================
async followFollowersOf(
targetUsername: string,
options: FollowFollowersOptions = {},
): Promise<FollowBatchResult> {
const { limit = 5, delayRange = [2000, 5000] } = options;
const source = targetUsername.replace(/^@/, '').toLowerCase();
const result: FollowBatchResult = {
targetSource: source,
totalScanned: 0,
followed: [],
skipped: [],
failed: [],
};
const page = await this.pwService.newPage();
this.logger.log(`🔍 Mở danh sách followers của @${source} (target: ${limit})`);
try {
await page.goto(`https://x.com/${source}/followers`, {
waitUntil: 'domcontentloaded',
timeout: 30000,
});
await page.waitForLoadState('networkidle');
await this.humanDelay(2500, 4000);
let lastHeight = -1;
let stagnant = 0;
while (result.followed.length < limit && stagnant < 4) {
// Lấy các cell user đang hiển thị
const cells = await page.locator('div[data-testid="cellInnerDiv"]').all();
for (const cell of cells) {
result.totalScanned++;
// Lấy username từ link đầu tiên trong cell
const link = cell.locator('a[href^="/"][role="link"]').first();
const href = await link.getAttribute('href').catch(() => null);
if (!href) continue;
const user = href.replace('/', '').split('?')[0].toLowerCase();
if (!user || user === source) continue; // Bỏ qua chính chủ
if (this.sessionFollowed.has(user)) {
result.skipped.push({ username: user, reason: 'already-in-session' });
continue;
}
// Đã follow chưa?
const isAlreadyFollowing = await cell
.locator('button[data-testid="unfollowButton"]')
.isVisible()
.catch(() => false);
if (isAlreadyFollowing) {
result.skipped.push({ username: user, reason: 'already-following' });
continue;
}
// Click follow trong cell
const btn = cell.locator('button[data-testid="followButton"]:not([disabled])').first();
if (await btn.isVisible().catch(() => false)) {
await btn.scrollIntoViewIfNeeded();
await this.humanDelay(500, 1200);
await btn.click();
await this.humanDelay(delayRange[0], delayRange[1]);
// Verify
const ok = await cell
.locator('button[data-testid="unfollowButton"]')
.isVisible()
.catch(() => false);
if (ok) {
result.followed.push(user);
this.sessionFollowed.add(user);
this.logger.log(`${result.followed.length}/${limit} | @${user}`);
} else {
result.failed.push({ username: user, error: 'State unchanged after click' });
}
}
if (result.followed.length >= limit) break;
}
// Scroll để load thêm
await page.evaluate(() => window.scrollBy(0, Math.floor(500 + Math.random() * 500)));
await this.humanDelay(1500, 3000);
const currentHeight = await page.evaluate(() => document.body.scrollHeight);
if (currentHeight === lastHeight) {
stagnant++;
} else {
stagnant = 0;
lastHeight = currentHeight;
}
}
} catch (err: any) {
this.logger.error(`❌ Batch follow lỗi: ${err.message}`);
} finally {
await page.close();
}
this.logger.log(
`🏁 Kết quả: Followed ${result.followed.length}, Skipped ${result.skipped.length}, Failed ${result.failed.length}`,
);
return result;
}
// =================== HELPERS ===================
private async humanDelay(min: number, max: number) {
const ms = Math.floor(min + Math.random() * (max - min));
await new Promise((r) => setTimeout(r, ms));
}
private async isVisible(page: Page, selector: string): Promise<boolean> {
return page.locator(selector).first().isVisible().catch(() => false);
}
private async isTextPresent(page: Page, regex: RegExp): Promise<boolean> {
const text = await page.locator('body').innerText().catch(() => '');
return regex.test(text);
}
}