From 7cd4ef691f5bd4d6d9582876c3f89e2c04ff172c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Touati?= Date: Thu, 25 Apr 2024 09:32:26 +0200 Subject: [PATCH] Handle login + auto re-login when bot account gets logged out (#5) * Handle login and re-logins * Update README * Fix log being in the wrong place * Fix dry run case * Hide API key from logs * CS * Check that config is valid * Simplify condition * Switch back to base package --- 5m5v-bot.js | 67 ++++++++++++++++++++++++++++++---------- 5m5v-config.yaml.example | 6 ++-- README.md | 8 +++-- package.json | 2 +- yarn.lock | 32 +++++++++---------- 5 files changed, 78 insertions(+), 37 deletions(-) diff --git a/5m5v-bot.js b/5m5v-bot.js index 5a244e0..8ac06aa 100644 --- a/5m5v-bot.js +++ b/5m5v-bot.js @@ -2,7 +2,7 @@ const fs = require('fs'); const yaml = require('js-yaml'); -const { Rettiwt } = require('@jeto314/rettiwt-api'); +const { Rettiwt } = require('rettiwt-api'); const TweetFilter = require('./lib/filter'); const util = require('./lib/util'); @@ -13,34 +13,69 @@ try { throw `Unable to load config file: ${error.message}`; } +if ((config?.users || []).length === 0) { + throw 'No users defined in config file'; +} + +if (!config.users.every(user => ['language', 'email', 'username', 'password'].every(key => key in (user ?? [])))) { + throw 'At least one user is missing required fields in config file'; +} + const languages = config.users.map(user => user.language); const pollingIntervalMs = 40 * 1000; const retweetDelayMs = config.delaytime || 2 * 60 * 1000; const isDryRun = process.argv[2] === '--dry-run'; - const tweetFilter = new TweetFilter(config.exclude, languages); -const rettiwt = new Rettiwt({ apiKey: config.users[0].api_key }); -console.log(isDryRun ? 'Looking for new tweets (dry run)...' : 'Looking for new tweets...'); +async function getApiKey(user) { + if (!user.apiKey) { + console.log(`Logging in as ${user.username}...`); + + user.apiKey = await new Rettiwt().auth.login(user.email, user.username, user.password); + + console.log('Logged in!'); + } + + return user.apiKey; +} (async () => { - for await (const tweet of rettiwt.tweet.stream({ includeWords: [util.trackedTerms.map(term => `"${term}"`).join(' OR ')] }, pollingIntervalMs)) { - const matchingLanguages = tweetFilter.matches(tweet) || []; + while (true) { + const rettiwt = new Rettiwt({ apiKey: await getApiKey(config.users[0]) }); + + console.log(isDryRun ? 'Looking for new tweets (dry run)...' : 'Looking for new tweets...'); + + try { + for await (const tweet of rettiwt.tweet.stream({ includeWords: [util.trackedTerms.map(term => `"${term}"`).join(' OR ')] }, pollingIntervalMs)) { + const matchingLanguages = tweetFilter.matches(tweet) || []; - for (const language of matchingLanguages) { - await new Promise(resolve => setTimeout(resolve, retweetDelayMs)); + for (const language of matchingLanguages) { + await new Promise(resolve => setTimeout(resolve, retweetDelayMs)); - try { - if (!isDryRun) { - const apiKey = config.users.find(user => user.language === language)?.api_key ?? null; - const rettiwt = new Rettiwt({ apiKey }); + const user = config.users.find(user => user.language === language); - await rettiwt.tweet.retweet(tweet.id); + try { + if (!isDryRun) { + const rettiwt = new Rettiwt({ apiKey: await getApiKey(user) }); + + await rettiwt.tweet.retweet(tweet.id); + } + + console.log(`Retweeted tweet ${tweet.id} in ${language}:\n${tweet.fullText}`); + } catch (error) { + console.error(`Unable to retweet ${tweet.id} in ${language}: ${error.message}`); + + if (error.constructor.name === 'RettiwtError' && error.code === 32) { + user.apiKey = null; + } + } } + } + } catch (error) { + console.error(`Error while streaming tweets: ${error.message}`); - console.log(`Retweeted tweet ${tweet.id} in ${language}:\n${tweet.fullText}`); - } catch (error) { - console.error(`Unable to retweet ${tweet.id} in ${language}: ${error.message}`); + if (error.constructor.name === 'RettiwtError' && error.code === 32) { + config.users[0].apiKey = null; } } } diff --git a/5m5v-config.yaml.example b/5m5v-config.yaml.example index a90df7b..0cfd2b3 100644 --- a/5m5v-config.yaml.example +++ b/5m5v-config.yaml.example @@ -1,3 +1,5 @@ users: - - api_key: Aa0Aa0Aa0Aa0Aa0Aa0Aa0Aa0Aa0Aa0Aa0Aa0Aa0Aa0Aa0 - language: english + - language: 'english' + username: 'user1' + email: 'user1@domain.com' + password: 'password123#@!' diff --git a/README.md b/README.md index cead0be..d20953a 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,14 @@ The configuration is loaded from `5m5v-config.yaml` in the working directory, wh #### `users` A list containing all users to retweet with. The first user listed will be used to search tweets with. -#### `users.api_key` -To obtain the API key for a given Twitter user, follow the [instructions from the Rettiwt-API package](https://github.com/Rishikant181/Rettiwt-API#authentication). #### `users.language` The language that this user will be retweeting matches from. +#### `users.username` +The Twitter username of that user (without the @). +#### `users.email` +The email address of the Twitter account. +#### `users.password` +The password of the Twitter account. #### `exclude` *(optional)* A list of keywords that if found in a Tweet will exclude that Tweet from being retweeted. #### `delaytime` *(optional)* diff --git a/package.json b/package.json index c32e7c8..b28b958 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "5m5v-bot.js", "dependencies": { "js-yaml": "^3.13.1", - "@jeto314/rettiwt-api": "1.0.0" + "rettiwt-api": "2.7.1" }, "devDependencies": { "nodeunit": "^0.11.3" diff --git a/yarn.lock b/yarn.lock index 4737b26..d616600 100644 --- a/yarn.lock +++ b/yarn.lock @@ -105,18 +105,6 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" -"@jeto314/rettiwt-api@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@jeto314/rettiwt-api/-/rettiwt-api-1.0.0.tgz#bff3544713df4a5f1e4190ee7c8545ff80b81791" - integrity sha512-8efcjpB+wBXdOrWcBdz/m58LTYC8JQ5PQqPMGEoQ1z2CGNmr6YUP/+as8cauPHd131CdtIROzOtwhkg46mytNQ== - dependencies: - axios "1.6.3" - class-validator "0.14.1" - commander "11.1.0" - https-proxy-agent "7.0.2" - rettiwt-auth "2.1.0" - rettiwt-core "3.4.0-alpha.2" - "@jridgewell/gen-mapping@^0.3.2": version "0.3.3" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" @@ -1318,6 +1306,18 @@ resolve@^1.10.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +rettiwt-api@2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rettiwt-api/-/rettiwt-api-2.7.1.tgz#8721606d08222d7034652782f7a09be53d28ad49" + integrity sha512-Q0757mFJTY70jFCFjLjzy1Dz/DIPR+HhnQaFBxz9HUZ4Dp9CStC/4OvEq6I3P8UlXTA3HVexTlevxRp9PHrnqw== + dependencies: + axios "1.6.3" + class-validator "0.14.1" + commander "11.1.0" + https-proxy-agent "7.0.2" + rettiwt-auth "2.1.0" + rettiwt-core "3.4.0" + rettiwt-auth@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/rettiwt-auth/-/rettiwt-auth-2.1.0.tgz#43fedd40cab5b775b92a1bef740d7b2a36553463" @@ -1328,10 +1328,10 @@ rettiwt-auth@2.1.0: cookiejar "2.1.4" https-proxy-agent "7.0.2" -rettiwt-core@3.4.0-alpha.2: - version "3.4.0-alpha.2" - resolved "https://registry.yarnpkg.com/rettiwt-core/-/rettiwt-core-3.4.0-alpha.2.tgz#d3033d0db7a05f503e3b0aef5981d33897f0d08b" - integrity sha512-775cDTPWZXhY+znEOsjupIgj/Mmi8shKhLveShm1KnXEX1Qc5SjTEnpQv1dHmDaxiV9n6BIQ73E7K9AucBl+Mg== +rettiwt-core@3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/rettiwt-core/-/rettiwt-core-3.4.0.tgz#515aa168a5a64effa6cb2340debafebf5d4ee8a1" + integrity sha512-7N2wV+AB5fJVMzyE0s6FS9jZ/4z/eXIM1DlhOqeWous36PZSDPprINIcdPM8LVmfMEpq4aanplHkqCheNGLXBw== dependencies: axios "1.6.3" class-validator "0.14.1"