U
This commit is contained in:
@@ -27,8 +27,10 @@
|
||||
"@nestjs/config": "^4.0.4",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/swagger": "^11.4.2",
|
||||
"@twitter-api-v2/plugin-token-refresher": "^1.0.0",
|
||||
"axios": "^1.16.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.15.1",
|
||||
"cookie": "^1.1.1",
|
||||
"https-proxy-agent": "^9.0.0",
|
||||
@@ -37,6 +39,7 @@
|
||||
"playwright-extra": "^4.3.6",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"twitter-api-v2": "^1.29.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Generated
+125
-22
@@ -16,25 +16,31 @@ importers:
|
||||
version: 5.1.6(keyv@5.6.0)
|
||||
'@nestjs/cache-manager':
|
||||
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':
|
||||
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':
|
||||
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':
|
||||
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':
|
||||
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':
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0(twitter-api-v2@1.29.0)
|
||||
axios:
|
||||
specifier: ^1.16.0
|
||||
version: 1.16.0
|
||||
class-transformer:
|
||||
specifier: ^0.5.1
|
||||
version: 0.5.1
|
||||
class-validator:
|
||||
specifier: ^0.15.1
|
||||
version: 0.15.1
|
||||
@@ -59,6 +65,9 @@ importers:
|
||||
rxjs:
|
||||
specifier: ^7.8.1
|
||||
version: 7.8.2
|
||||
swagger-ui-express:
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1(express@5.2.1)
|
||||
twitter-api-v2:
|
||||
specifier: ^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)
|
||||
'@nestjs/testing':
|
||||
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':
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.6
|
||||
@@ -800,6 +809,9 @@ packages:
|
||||
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
'@microsoft/tsdoc@0.16.0':
|
||||
resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==}
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||
|
||||
@@ -862,6 +874,19 @@ packages:
|
||||
'@nestjs/websockets':
|
||||
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':
|
||||
resolution: {integrity: sha512-Vpdv8jyCQdThfoTx+UTn+DRYr6H6X02YUqcpZ3qP6G3ZUwtVp7eS+hoQPGd4UuCnlnFG8Wqr2J9bGEzQdi1rIg==}
|
||||
peerDependencies:
|
||||
@@ -877,6 +902,23 @@ packages:
|
||||
prettier:
|
||||
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':
|
||||
resolution: {integrity: sha512-/UFNWXvPEdu4v4DlC5oWLbGKmD27LehLK06b8oLzs6D6lf4vAQTdST8LRAXBadyMUQnVEQWMuBo3CtAVtlfXtQ==}
|
||||
peerDependencies:
|
||||
@@ -925,6 +967,9 @@ packages:
|
||||
'@opentelemetry/api':
|
||||
optional: true
|
||||
|
||||
'@scarf/scarf@1.4.0':
|
||||
resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
|
||||
|
||||
'@sinclair/typebox@0.34.49':
|
||||
resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==}
|
||||
|
||||
@@ -1704,6 +1749,9 @@ packages:
|
||||
cjs-module-lexer@2.2.0:
|
||||
resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==}
|
||||
|
||||
class-transformer@0.5.1:
|
||||
resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==}
|
||||
|
||||
class-validator@0.15.1:
|
||||
resolution: {integrity: sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==}
|
||||
|
||||
@@ -3149,6 +3197,18 @@ packages:
|
||||
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
|
||||
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:
|
||||
resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==}
|
||||
engines: {node: '>=0.10'}
|
||||
@@ -4517,6 +4577,8 @@ snapshots:
|
||||
|
||||
'@lukeed/csprng@1.1.0': {}
|
||||
|
||||
'@microsoft/tsdoc@0.16.0': {}
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.10.0
|
||||
@@ -4524,10 +4586,10 @@ snapshots:
|
||||
'@tybys/wasm-util': 0.10.2
|
||||
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:
|
||||
'@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/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)
|
||||
cache-manager: 7.2.8
|
||||
keyv: 5.6.0
|
||||
rxjs: 7.8.2
|
||||
@@ -4559,7 +4621,7 @@ snapshots:
|
||||
- uglify-js
|
||||
- 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:
|
||||
file-type: 21.3.4
|
||||
iterare: 1.2.1
|
||||
@@ -4569,21 +4631,22 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
uid: 2.0.2
|
||||
optionalDependencies:
|
||||
class-transformer: 0.5.1
|
||||
class-validator: 0.15.1
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@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-expand: 12.0.3
|
||||
lodash: 4.18.1
|
||||
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:
|
||||
'@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
|
||||
fast-safe-stringify: 2.1.1
|
||||
iterare: 1.2.1
|
||||
@@ -4593,12 +4656,20 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
uid: 2.0.2
|
||||
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:
|
||||
'@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/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)
|
||||
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
|
||||
express: 5.2.1
|
||||
multer: 2.1.1
|
||||
@@ -4620,13 +4691,28 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@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/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)
|
||||
'@microsoft/tsdoc': 0.16.0
|
||||
'@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
|
||||
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': {}
|
||||
|
||||
@@ -4649,6 +4735,8 @@ snapshots:
|
||||
dependencies:
|
||||
cluster-key-slot: 1.1.2
|
||||
|
||||
'@scarf/scarf@1.4.0': {}
|
||||
|
||||
'@sinclair/typebox@0.34.49': {}
|
||||
|
||||
'@sinonjs/commons@3.0.1':
|
||||
@@ -5588,6 +5676,8 @@ snapshots:
|
||||
|
||||
cjs-module-lexer@2.2.0: {}
|
||||
|
||||
class-transformer@0.5.1: {}
|
||||
|
||||
class-validator@0.15.1:
|
||||
dependencies:
|
||||
'@types/validator': 13.15.10
|
||||
@@ -7167,6 +7257,19 @@ snapshots:
|
||||
dependencies:
|
||||
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: {}
|
||||
|
||||
synckit@0.11.12:
|
||||
|
||||
+3
-1
@@ -6,6 +6,7 @@ import {XPosterModule} from "./x-poster/x-poster.module";
|
||||
import {ConfigModule} from "@nestjs/config";
|
||||
import {CacheModule} from "@nestjs/cache-manager";
|
||||
import KeyvRedis from "@keyv/redis";
|
||||
import {XbotFollowModule} from "./xbot-follow/xbot-follow.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -24,7 +25,8 @@ import KeyvRedis from "@keyv/redis";
|
||||
}),
|
||||
}),
|
||||
SqsModule,
|
||||
XPosterModule
|
||||
XPosterModule,
|
||||
XbotFollowModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
||||
+23
-2
@@ -1,12 +1,33 @@
|
||||
import {NestFactory} from '@nestjs/core';
|
||||
import {AppModule} from './app.module';
|
||||
import {SqsPosterWorker} from "./sqs-module/sqs.poster.worker";
|
||||
import {DocumentBuilder, SwaggerModule} from "@nestjs/swagger";
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
await app.listen(port, () => console.log(`Listening on port ${port}`));
|
||||
// Cấu hình Swagger
|
||||
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();
|
||||
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
// src/modules/x-browser/x-browser.service.ts
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
OnModuleInit,
|
||||
OnModuleDestroy, HttpException,
|
||||
} from '@nestjs/common';
|
||||
import {chromium, Browser, BrowserContext, Page} from 'playwright';
|
||||
import {buildXCookies} from "./utils/x-headers.util";
|
||||
import {HttpException, Injectable, Logger, OnModuleDestroy, OnModuleInit,} from '@nestjs/common';
|
||||
import {Browser, BrowserContext, chromium, Page} from 'playwright';
|
||||
import {rand} from "../helper";
|
||||
|
||||
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 ' +
|
||||
'(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
||||
viewport: {width: 1366, height: 768},
|
||||
locale: 'en-US',
|
||||
locale: process.env.BROWSER_LOCALE || 'en-US',
|
||||
proxy: account.proxy ? {server: account.proxy} : undefined,
|
||||
});
|
||||
console.log('getOrCreateContext:5')
|
||||
|
||||
@@ -77,7 +77,7 @@ export class XPosterRouterService {
|
||||
},
|
||||
],
|
||||
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;
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user