U
This commit is contained in:
@@ -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": {
|
||||||
|
|||||||
Generated
+125
-22
@@ -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
@@ -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
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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