diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..f559b54c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,6 @@ +# Contributing to the project + +You would like to contribute to the project itself? +Great! No matter if it's fixing a few typos or adding a whole feature - every contribution is welcome. + +The contributing page is located in the project's wiki, please [click here to get redirected](./docs/wiki/contributing.md)! \ No newline at end of file diff --git a/README.md b/README.md index 0840d60f..c911d52e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- Request tons of comments, favs & likes by texting a bot network! + Manage accounts and request tons of comments, favs, likes & follows by texting a bot network!
See how to set up the bot and customize it below.

@@ -31,12 +31,14 @@ ## **Introduction** -* Request many profile comments directly from the steam chat -* Easily host multiple steam accounts and control them from **one** console and chat with this bot cluster -* Send comments to other steam profiles -* Apply cooldowns & customize nearly any value -* Advertise your group & automatically invite users to it -* Use proxies and requests comments via URL in your browser +* Request profile, group, screenshot, artwork, guide or discussion comments directly from the Steam Chat +* Manage hundreds of Steam accounts with ease and control them from **one** terminal and Steam Chat +* No need of having a Steam Client installed - perfect for hosting on a server +* Apply cooldowns to users and customize various settings (including multi language support!) +* Advertise your Steam group and automatically invite users to it +* Use proxies to spread requests over multiple IPs +* Completely VAC safe as it does not directly interact with any games +* Extend the bot's functionality with plugins You can see and test out my 24/7 hosted comment bot in action [by clicking here!](https://steamcommunity.com/id/3urobeatscommentbot) @@ -70,9 +72,15 @@ You'll find pages on how to add proxies to drastically increase the amount of po #### **Questions, Bugs, Issues & Betas** If you have any questions, please feel free to open a [Q&A discussion](https://github.com/3urobeat/steam-comment-service-bot/discussions/new?category=q-a)! -If you encountered a **bug** or wish a feature to be added, please open an [**issue!**](https://github.com/3urobeat/steam-comment-service-bot/issues/new/choose) -If you are interested in beta builds of this project, visit the [beta-testing branch.](https://github.com/3urobeat/steam-comment-service-bot/tree/beta-testing) -If you are interested in the active development progress, visit the [projects section.](https://github.com/3urobeat/steam-comment-service-bot/projects) +If you encountered a bug or wish a feature to be added, please open an [issue](https://github.com/3urobeat/steam-comment-service-bot/issues/new/choose)! +If you are interested in beta builds of this project, visit the [beta-testing branch](https://github.com/3urobeat/steam-comment-service-bot/tree/beta-testing). +If you are interested in the active development progress, visit the [projects section](https://github.com/3urobeat/steam-comment-service-bot/projects). + +#### **Contributing** +You would like to contribute to the project itself? +Great! No matter if it's fixing a few typos or adding a whole feature - every contribution is welcome. + +The contributing page is located in the project's wiki, please [click here to get redirected](./docs/wiki/contributing.md)! #### **License** This project and all code included is distributed under the GPL-3.0 license. diff --git a/advancedconfig.json b/advancedconfig.json index 3ca44e4a..3874d057 100644 --- a/advancedconfig.json +++ b/advancedconfig.json @@ -2,20 +2,25 @@ "_disclaimer_": "This file includes only advanced settings! Please only use the 'config.json' for setup and ignore this file if you are new!", "_help_": "Read the docs: https://github.com/3urobeat/steam-comment-service-bot/blob/master/docs/wiki/advancedconfig_doc.md", "disableAutoUpdate": false, + "dummy0": "------------------- Login Settings: -------------------", "loginDelay": 2500, "loginTimeout": 60000, - "relogTimeout": 30000, + "loginRetryTimeout": 30000, + "relogTimeout": 900000, "maxLogOnRetries": 1, "useLocalIP": true, + "dummy1": "------------------- General Settings: -------------------", "acceptFriendRequests": true, "forceFriendlistSpaceTime": 4, "setPrimaryGroup": false, + "dummy2": "------------------- Request Settings: -------------------", "commandCooldown": 12000, "restrictAdditionalCommandsToOwners": [], "retryFailedComments": false, "retryFailedCommentsDelay": 300000, "retryFailedCommentsAttempts": 1, "lastQuotesSize": 5, + "dummy3": "------------------- Debug Settings: -------------------", "enableevalcmd": false, "printDebug": false, "steamUserDebug": false, diff --git a/config.json b/config.json index eef10589..9fc90237 100644 --- a/config.json +++ b/config.json @@ -1,11 +1,12 @@ { - "commentdelay": 15000, "_help_": "Read the docs: https://github.com/3urobeat/steam-comment-service-bot/blob/master/docs/wiki/config_doc.md", + "defaultLanguage": "english", + "requestDelay": 15000, "skipSteamGuard": false, - "commentcooldown": 5, + "requestCooldown": 5, "botaccountcooldown": 10, - "maxComments": 0, - "maxOwnerComments": 0, + "maxRequests": 0, + "maxOwnerRequests": 5, "randomizeAccounts": false, "unfriendtime": 31, "playinggames": ["!help | 3urobeat", 440, 730], diff --git a/customlang.json b/customlang.json index a2624f23..cc0151cf 100644 --- a/customlang.json +++ b/customlang.json @@ -1,3 +1,5 @@ { - "note": "Please read here on how to use this file: https://github.com/3urobeat/steam-comment-service-bot/blob/master/docs/wiki/customlang_doc.md" + "english": { + "note": "Please read here on how to use this file: https://github.com/3urobeat/steam-comment-service-bot/blob/master/docs/wiki/customlang_doc.md" + } } \ No newline at end of file diff --git a/docs/dev/README.md b/docs/dev/README.md new file mode 100644 index 00000000..ea669e5a --- /dev/null +++ b/docs/dev/README.md @@ -0,0 +1,40 @@ +# Welcome to the steam-comment-service-bot developer documentation! +[⬅️ Go back to the main page](../..#readme) + +The developer documentation includes pages for every module and some useful information about it. +This should help you figuring out how to implement parts of the bot into your plugin or when working on the project itself! + +It makes sense to first learn how this project works and is built before diving in further. The [introduction](./introduction.md) page should get you started quickly. + +Are you rather searching for the wiki? [Click here to get redirected](../wiki#readme) + +  + +## Table of Contents +This documentation uses the same structure as the `src` folder. Each top-level module listed here contains further links to its submodules. +The order of this list represents the order in which they are instantiated when the application is started. + +- [Introduction](./introduction.md) +- Parent Process: + - [Starter](./starter.md) +- Child Process: + - [Controller](./controller/controller.md) + - [DataManager](./dataManager/dataManager.md) + - [Updater](./updater/updater.md) + - [Bot](./bot/bot.md) + - [sessionHandler](./sessionHandler/sessionHandler.md) + - [CommandHandler](./commands/commandHandler.md) + - [PluginSystem](./pluginSystem/pluginSystem.md) + +  + +## Contact +Steam profile: https://steamcommunity.com/id/3urobeat +Steam group: https://steamcommunity.com/groups/3urobeatGroup +Comment Bot: https://steamcommunity.com/id/3urobeatscommentbot +YouTube Tutorial: https://youtu.be/8J78rC9Z28U +Discord: @3urobeat + +  + +> Steam Comment Service Bot - Created with ❤️ and a lot of time. \ No newline at end of file diff --git a/docs/dev/bot/bot.md b/docs/dev/bot/bot.md new file mode 100644 index 00000000..bf9fab54 --- /dev/null +++ b/docs/dev/bot/bot.md @@ -0,0 +1,13 @@ +# Bot +[⬅️ Go back to dev home](../#readme) + +  + +When logging in, the controller creates a bot object for every Steam account the user has provided. +It creates a SteamUser and SteamCommunity instance, which allow the Controller to use this bot account to interact with Steam. +The bot object itself handles events for this specific account (e.g. chat messages), informs the Controller about connection losses, etc. + +  + +Every function and object property is documented with JsDocs in the implementation file. +Please check them out using your IntelliSense or by clicking the button in the top right corner of this page. \ No newline at end of file diff --git a/docs/dev/bot/events.md b/docs/dev/bot/events.md new file mode 100644 index 00000000..94156bef --- /dev/null +++ b/docs/dev/bot/events.md @@ -0,0 +1,49 @@ +# Bot Events +[⬅️ Go back to Bot](./bot.md) + +  + +Each bot object handles their own [SteamUser](https://github.com/DoctorMcKay/node-steam-user) events. +These event handlers are located inside the bot events folder and contain each a prototype function for attaching themselves. +These functions follow the naming scheme `_attachSteamEventNameEvent` and are being called by the Bot constructor. + +  + +## Table of Contents +- [debug](#debug--debug-verbose-) +- [disconnected](#disconnected-) +- [error](#error-) +- [friendMessage](#friendMessage-) +- [loggedOn](#loggedOn-) +- [relationship](#relationship-) +- [webSession](#webSession-) + +Please use your browser's search function Ctrl+F to find a specific event using its name on this page. + +  + +## debug & debug-verbose +The content of these events is logged to the terminal when `steamUserDebug` and `steamUserDebugVerbose` are set to `true` in the `advancedconfig.json`. + +## disconnected +Handles a disconnect by logging to the terminal, updating its status and trying to relog itself, unless it is an intentional log off. + +## error +Handles login errors by logging to the terminal, updating its status and either retrying the login or skipping the account. + +## friendMessage +Handles Steam Chat messages to this account. +If this is the main account it will instruct the [CommandHandler](../commandHandler/commandHandler.md) to run the command or apply a cooldown if the user is spamming. +If this is a child account, it will respond with a message pointing to the main account. + +## loggedOn +Logs a message, sets the online status and increments the progress bar (on initial login) when this bot account establishes a connection to Steam. + +## relationship +Handles an incoming friend request or group invite by adding the user to the `lastcomment.db` database and inviting them to the group set in `config.json`. + +## webSession +Handles setting cookies, accepting friend requests & group invites while the bot was offline, updates the bot's status, performs a few checks and starts playing games. +This event is fired after loggedOn when this bot account establishes a connection to Steam. + +After this event was handled, a bot account is considered to be online and ready to be used. \ No newline at end of file diff --git a/docs/dev/commands/commandHandler.md b/docs/dev/commands/commandHandler.md new file mode 100644 index 00000000..acbd9c54 --- /dev/null +++ b/docs/dev/commands/commandHandler.md @@ -0,0 +1,26 @@ +# CommandHandler +[⬅️ Go back to dev home](../#readme) + +  + +The CommandHandler manages the Bot's functionality which is accessible by the user, for example through the Steam Chat. +On startup, it loads a bunch of core commands shipped with the application and exposes functions to un-/register commands at runtime, for example using plugins. +Lastly, the module contains a helper folder which stores various functions used by commands, like getting all available bot accounts or parsing command arguments. + +By default, the bot comes with one command handler which implements this module: The Bot's [friendMessage](../bot/events.md#friendmessage-) event. +This adds support for running commands and receiving answers through the Steam Chat. + +  + +Every function and object property is documented with JsDocs in the implementation file. +Please check them out using your IntelliSense or by clicking the button in the top right corner of this page. + +  + + +### resInfo +Object representing the default/commonly used content the resInfo object can/should contain. +The resInfo object is passed to every command and contains valuable information about the command handler which executes this command (plugins implementing other chat platforms need to implement their own command handler of course), +about the user who ran the command, which userIDs have owner rights, etc. +The commandHandler of e.g. a plugin can add more information to this object as they please. This can be useful to pass more information to their commands, through the commandHandler. + diff --git a/docs/dev/controller/controller.md b/docs/dev/controller/controller.md new file mode 100644 index 00000000..f430ee9e --- /dev/null +++ b/docs/dev/controller/controller.md @@ -0,0 +1,20 @@ +# Controller +[⬅️ Go back to dev home](../#readme) + +  + +The Controller is the center piece of the application. +It is the entry point of the child process, which gets spawned by the [parent process](../starter.md). +It stores references to all the other modules, does first checks (e.g. internet connection, unsupported nodejs version) and handles the startup. + +The parent process forks a child that loads `controller.js` into memory and sets a timestamp of startup as `process.argv[3]`. +Any data which should be passed through a restart will be set as a stringified object at `process.argv[4]`. +If the mentioned timestamp is within the last 2.5 seconds, `controller.js` will create a new Controller object and call `_start()` to start the application. + +Most functions are prototype functions linked to the existing Controller object at runtime, some may however still be normal exported functions. +Please check the [Helpers](./helpers.md) page of this module to see functions which still belong to this module but may not be listed directly on this page. + +  + +Every function and object property is documented with JsDocs in the implementation file. +Please check them out using your IntelliSense or by clicking the button in the top right corner of this page. \ No newline at end of file diff --git a/docs/dev/controller/events.md b/docs/dev/controller/events.md new file mode 100644 index 00000000..2d8d697e --- /dev/null +++ b/docs/dev/controller/events.md @@ -0,0 +1,51 @@ +# Controller Events +[⬅️ Go back to Controller](./controller.md) + +  + +The events folder contains functions for events which the Controller supports. +These functions are called by the Controller, which then in turn actually emit the event using the `controller.events` EventEmitter. +This allows for running internal code (e.g. ready event) before an external module receives them. + +  + +## Table of Contents +- [ready](#ready-) +- [statusUpdate](#statusUpdate-) +- [steamGuardInput](#steamGuardInput-) + +Please use your browser's search function Ctrl+F to find a specific event using its name on this page. + +  + +## ready +This event is emitted when the bot finished logging in all bot accounts for the first time since the last start/restart. + +Before emitting the event, the bot will +- ...log the ready messages containing a variety of useful information +- ...instruct the [DataManager](../dataManager/dataManager.md) to refresh the `cache.json` backups of all config files +- ...log held back log messages from during the startup +- ...perform various checks and display warnings if for example the friendlist space is running low +- ...and update the total login time in data.json. + +No arguments. + +## statusUpdate +This event is emitted when any bot account changes their online status. + +Before emitting the event, the bot will update the `status` property of the affected bot account to the new status. + +The event is emitted with the parameters +- `bot` ([Bot](../bot/bot.md)) - Bot instance of the affected account +- `newStatus` (Bot.[EStatus](/src/bot/EStatus.js)) - The new status of this bot + +## steamGuardInput +This event is emitted when any bot account requires a Steam Guard Code to be submitted before it can continue logging in. + +The event is emitted with the parameters +- `bot` ([Bot](../bot/bot.md)) - Bot instance of the affected account +- `submitCode` (function(string): void) - Function to submit a code. Pass an empty string to skip the account. + +The `submitCode` function allows users to implement accepting Steam Guard Codes from users into their plugins. This is very cool. +Check out how the [template plugin](https://github.com/3urobeat/steam-comment-bot-template-plugin/blob/main/plugin.js) implements the +`steamGuardInput` event function (which is called by the PluginSystem when the event is emitted, instead of listening directly to it). diff --git a/docs/dev/controller/helpers.md b/docs/dev/controller/helpers.md new file mode 100644 index 00000000..8c909ee1 --- /dev/null +++ b/docs/dev/controller/helpers.md @@ -0,0 +1,25 @@ +# Controller Helpers +[⬅️ Go back to Controller](./controller.md) + +  + +The helpers folder contains functions which are regularly used, often by multiple files, also from other modules. +Each module has their own helpers folder, containing helper functions which fit the best to that specific module, to keep the project structure organized. + +All prototype functions which are directly accessible from the active Controller object at runtime are already listed in the [Controller](./controller.md) docs page. +This page only includes functions which are directly exported, meaning to use them you need to import that specific helper file in your code. + +  + +## Table of Contents +- [misc.js](#miscjs-) +- [npminteraction.js](#npminteractionjs-) + +  + +## misc.js +Special Case: The functions in this helper are directly accessible from the Controller object to make using them easier. You can access them through the Controller `misc` object. + +  + +## npminteraction.js diff --git a/docs/dev/dataManager/dataManager.md b/docs/dev/dataManager/dataManager.md new file mode 100644 index 00000000..97ea3814 --- /dev/null +++ b/docs/dev/dataManager/dataManager.md @@ -0,0 +1,36 @@ +# DataManager +[⬅️ Go back to dev home](../#readme) + +  + +The DataManager system imports, checks, handles errors and provides a file updating service for all config & source code files. +It is the central point for holding and managing any data which the application stores on the filesystem. + +Use the data and functions exposed by this module whenever you need to e.g. read and write to a config file. + +  + +Every function and object property is documented with JsDocs in the implementation file. +Please check them out using your IntelliSense or by clicking the button in the top right corner of this page. + +  + +### lang +Object storing all supported languages and their strings used for responding to a user. + +It loads the files of `src/data/lang/` and overwrites all keys with the corresponding values from `customlang.json` (does not overwrite the file on the disk). +You can see the default lang content by clicking [here](/src/data/lang/english.json) and your customlang file by clicking [here](/customlang.json). + +Use the function `getLang()` to access the languages stored in this object. + +### cachefile +Object storing IDs from config files converted at runtime and backups for all config & data files. + +If you need the steamID64 of any owner or bot account or the groupID64 of the botsgroup or configgroup, read them from here. +The bot accepts various inputs in the config for setting owner IDs and converts them to steamID64s at startup. +These are stored in this object and should always be used instead of reading from the config directly. + +At every startup (when the Controller ready event fires) the bot writes a backup of all config and data files to this object as well. +This content is written to `src/data/cache.json` in order to restore previous config settings should the user make a syntax mistake or the updater break. + +You can see its content by clicking [here](/src/data/cache.json), however it is empty if you have never started the bot before. \ No newline at end of file diff --git a/docs/dev/introduction.md b/docs/dev/introduction.md new file mode 100644 index 00000000..9e8201ed --- /dev/null +++ b/docs/dev/introduction.md @@ -0,0 +1,35 @@ +# Introduction +[⬅️ Go back to dev home](./#readme) + +  + +Hey, welcome to the `steam-comment-service-bot` project! +As the name suggests, this started as a small and simple bot cluster solely for commenting on profiles. +As of now it however is \*way\* more than that and a name along the lines of `steam-bot-network-manager` would be better suited. + +This project now predominantly focuses on managing user data, handling issues and keeping Steam accounts logged in. +Various core commands like commenting, voting & favorizing then provide the user the functionality to command all accounts at once. + +  + +## Structure +This application consists of a few main parts, seperated into "modules". All of these run in a child process (more about that in a minute): +- **Controller** - Entry point of the child process, initializes everything and holds references to everything. This is the core part of the entire application. +- **Updater** - Handles updating an existing installation on a user's machine automatically when a new version was found on GitHub. It also creates a backup and is able to recover from it, should the installation fail. +- **Bot** - Controls the connection for one Steam account and handles events (e.g. chat messages, connection loss, ...). The Controller creates a Bot object for every account the user provided. +- **DataManager** - Handles reading and writing all config and data files from and to the disk, handles errors & and restores files either from a backup or from GitHub, displays setting recommendations and more. +- **PluginSystem** - Loads plugins installed by the user, calls their event handlers, manages their data and exposes the Controller to them. + +The `src` directory also contains a folder for library patches I've made, the commands folder holding the actual features like commenting, voting, etc. visible to the user, a session manager for logging in a bot account into Steam and the data folder, storing internal data (e.g the installed version, cooldowns, ...) and config backups. + +  + +## Start and Restart process +As mentioned above, the bot runs inside a child process, controlled by a parent process, which is the one you are actually starting when running the command `node start`. +This two process architecture allows me to completely restart the bot itself without any user interaction at all and without leaving any old data in the memory behind. This was quite revolutionary for me to figure out back when I built the updater :'D + +The startup procedure looks like follows: +`node start` **->** start.js (cannot be reloaded) **->** src/starter.js (can only be hot-reloaded) **->** src/controller.js (child process, can be fully reloaded) +The Controller now inits the DataManager, then performs a few checks (e.g. internet connection), then inits the Updater and then starts spawning Bot objects to log in all accounts (if no update was found). + +The starter process dev documentation page can be found [here](./starter.md). \ No newline at end of file diff --git a/docs/dev/pluginSystem/pluginSystem.md b/docs/dev/pluginSystem/pluginSystem.md new file mode 100644 index 00000000..71571234 --- /dev/null +++ b/docs/dev/pluginSystem/pluginSystem.md @@ -0,0 +1,14 @@ +# PluginSystem +[⬅️ Go back to dev home](../#readme) + +  + +The plugin system loads plugin packages installed by the user, exposes the Controller and provides functions for managing plugin data. +Plugins must be installed npm packages with the name prefix `steam-comment-bot-` to be recognized. + +You can read more about plugin requirements and their files on the [Creating Plugins wiki page](../../wiki/creating_plugins.md). + +  + +Every function and object property is documented with JsDocs in the implementation file. +Please check them out using your IntelliSense or by clicking the button in the top right corner of this page. \ No newline at end of file diff --git a/docs/dev/sessionHandler/sessionHandler.md b/docs/dev/sessionHandler/sessionHandler.md new file mode 100644 index 00000000..dadd6a27 --- /dev/null +++ b/docs/dev/sessionHandler/sessionHandler.md @@ -0,0 +1,15 @@ +# SessionHandler +[⬅️ Go back to dev home](../#readme) + +  + +Every [Bot](../bot/bot.md) object creates its own sessionHandler object. +The sessionHandler handles getting a refreshToken to login a bot account into Steam. +To do so, it either uses an existing refreshToken from the [tokens.db](../dataManager/dataManager.md#tokensdb) database or uses the user provided login credentials to create a new session by retrieving a Steam Guard Code. + +The sessionHandler module also periodically checks for tokens which expire soon and provides various functions for interacting with the [tokens.db](../dataManager/dataManager.md#tokensdb) database. + +  + +Every function and object property is documented with JsDocs in the implementation file. +Please check them out using your IntelliSense or by clicking the button in the top right corner of this page. \ No newline at end of file diff --git a/docs/dev/starter.md b/docs/dev/starter.md new file mode 100644 index 00000000..c618135b --- /dev/null +++ b/docs/dev/starter.md @@ -0,0 +1,61 @@ +# Starter Process +[⬅️ Go back to dev home](./#readme) + +  + +As mentioned in the [introduction](./introduction.md), the application operates within two processes. +When executing the application using the terminal command `npm start`, you execute the file `start.js`, which in turn instructs `starter.js` to spawn a new child process with the entry file `controller.js`. + +This architecture allows the application to completely restart itself by instructing the parent process to kill and respawn its child. +This leaves no old data behind in the memory, which is especially important for the automatic updater. + +  + +## Table of Contents +- [start.js](#startjs-) +- [starter.js](#starterjs-) + +  + +## start.js + +This file cannot be updated! + +This is the entry point of the parent process. +If `src/data/data.json` exists, it reads the next file to start from it. If not, it defaults to `./src/starter.js`. +This allows the parent process to be semi-updateable, should the file structure change. +Should this file be missing, it is able to fetch it from GitHub, allowing the bot to restore itself just from this single file. + +Following up, it executes the `run()` function which `starter.js` exposes. + +  + +Every function and object property is documented with JsDocs in the implementation file. +Please check them out using your IntelliSense or by clicking the button in the top right corner of this paragraph. + +  + +## starter.js + +This file can be semi-updated. When restarting after an update, start.js clears the cache of this file which will load changes but can leave old references behind in the memory. + +The job of this file is to install dependencies and handle starting & restarting the child process. +To do so, it attaches various event listeners to the parent process: +- `handleUnhandledRejection`: Catches generic Unhandled Rejection errors. Should never fire and is only included to prevent a possible crash +- `handleUncaughtException`: Catches errors which have not been handled properly, mainly "Module Not Found" errors. This catch is used to install dependencies for first-time users. +- `exit`: Triggered when the parent process is about to stop. It stops the child process to avoid an orphan and logs to the output. + +These listeners can also be detached again when restarting. + +To receive restart and stop requests from the child process, as well as detecting an unexpected crash of it, it attaches various listeners to the child process as well: +- `message` - Handles the `process.send()` messages `restart()` and `stop()` by calling the matching function in `start.js`. +- `close` - Handles an unexpected exit of the child process and restarts the application. + +By default the child process is spawned with the flags `--max-old-space-size=2048` and `--optimize-for-size`. +You can add more by extending the `execArgs` array at the top of the file. +To enable the memory debugger you can for example pass the flag `--inspect`. + +  + +Every function and object property is documented with JsDocs in the implementation file. +Please check them out using your IntelliSense or by clicking the button in the top right corner of this paragraph. \ No newline at end of file diff --git a/docs/dev/updater/helpers.md b/docs/dev/updater/helpers.md new file mode 100644 index 00000000..2f6b7f1e --- /dev/null +++ b/docs/dev/updater/helpers.md @@ -0,0 +1,50 @@ +# Updater Helpers +[⬅️ Go back to Updater](./updater.md) + +  + +The Updater's collection of helper functions handle the downloading and installing of updates, as well as saving and recovering backups. +The `run()` function located in the module's main file, `updater.js`, calls these functions in the correct order to install an update. +The helper files & functions are listed on this page in the same order. + +  + +## Table of Contents +- [checkForUpdate.js](#checkForUpdatejs-) +- [prepareUpdate.js](#prepareupdatejs-) +- [createBackup.js](#createBackupjs-) +- [downloadUpdate.js](#downloadupdatejs-) +- [customUpdateRules.js](#customupdaterulesjs-) +- [restoreBackup.js](#restorebackupjs-) + +  + +## checkForUpdate.js + +  + +## prepareUpdate.js + +  + +## createBackup.js + +  + +## downloadUpdate.js + +  + +## customUpdateRules.js +Some files, for example the config.json, can't just be overwritten like any of the source code files. +Instead we need to apply custom update rules to e.g. carry over existing user settings. + +This file is loaded into memory *after* updating but is called with the existing function signature from *before* the update. +This means we cannot change the existing parameter structure, leading to a few unused legacy parameters. +This is a minor quirk that must be kept in mind. + +This file is not called by `updater.js` but by `downloadUpdate.js`. + +  + +## restoreBackup.js diff --git a/docs/dev/updater/updater.md b/docs/dev/updater/updater.md new file mode 100644 index 00000000..0aa44568 --- /dev/null +++ b/docs/dev/updater/updater.md @@ -0,0 +1,17 @@ +# Updater +[⬅️ Go back to dev home](../#readme) + +  + +The Updater is able to fully update the application on a user's machine, without any manual interaction. +It does so by periodically checking the GitHub repository for a new version, pulling the codebase as a zip, creating a backup, replacing the files and restarting the bot. + +To recover from a failed update, the bot creates a backup of the current codebase before updating. +This backup will then be recovered, the bot will restart and skip any updates for some time. + +Updater's `run()` function calls [helper functions](./helpers.md) in the correct order which do the actual downloading, installing and backup handling. + +  + +Every function and object property is documented with JsDocs in the implementation file. +Please check them out using your IntelliSense or by clicking the button in the top right corner of this page. \ No newline at end of file diff --git a/docs/wiki/README.md b/docs/wiki/README.md index 0566d81f..47e530e3 100644 --- a/docs/wiki/README.md +++ b/docs/wiki/README.md @@ -3,6 +3,8 @@ You can find setup instructions, documentation for all commands, config files, error messages and much more here! +Are you rather searching for the developer documentation? [Click here to get redirected](../dev#readme) +   ## Table of Contents @@ -17,6 +19,7 @@ You can find setup instructions, documentation for all commands, config files, e - [Steam limitations](./steam_limitations.md) - [Integrating into your own application](./integrating_into_your_app.md) - [Creating plugins](./creating_plugins.md) +- [Contributing to the project](./contributing.md) - [Version Changelogs](./version_changelogs.md)   diff --git a/docs/wiki/advancedconfig_doc.md b/docs/wiki/advancedconfig_doc.md index 5fa1bafc..edf3704a 100644 --- a/docs/wiki/advancedconfig_doc.md +++ b/docs/wiki/advancedconfig_doc.md @@ -14,25 +14,33 @@ This is the full documentation to customize your `advancedconfig.json`. | \_disclaimer\_ | String | No functionality. Just a comment pointing to the normal config. | | \_help\_ | String | No functionality. Links directly to here to provide easily accessible explanations. | | disableautoupdate | true or false | Disables auto updates. **Setting to true is not recommended!** Default: false | -| loginDelay | Number in ms | Time the bot will wait between logging in each account to prevent an IP ban. Default: 2500 | -| loginTimeout | Number in ms | Time after which an active login attempt will be considered as timed out and failed. It will be retried or skipped when maxLogOnRetries is exceeded. Set to 0 to disable. Default: 60000 | -| relogTimeout | Number in ms | Time the bot will wait after loosing connection to Steam before trying to check if Steam/your internet is up again. Default: 30000 | +|   | | | +| loginDelay | Number in ms | Time the bot will wait between logging in each account to prevent an IP ban. Default: 2500 (2.5 seconds) | +| loginTimeout | Number in ms | Time after which an active login attempt will be considered as timed out and failed. It will be retried or skipped when maxLogOnRetries is exceeded. Set to 0 to disable. Default: 60000 (60 seconds) | +| loginRetryTimeout | Number in ms | Time the bot will wait after loosing connection to Steam before attempting to log in again. Default: 30000 (30 seconds) | +| relogTimeout | Number in ms | Time the bot will wait after failing all reconnect attempts before trying again. Default: 900000 (15 minutes) | | maxLogOnRetries | Number | Amount of times the bot will retry logging in to an account if the first try fails. Default: 1 | | useLocalIP | true or false | If the bot should use your real IP as well when using proxies. Default: true | +|   | | | | acceptFriendRequests | true or false | If the bot should accept friend requests. Default: true | -| forceFriendlistSpaceTime | Number in days | Amount of days a user hasn't requested comments to get unfriended if only one friend slot is left. Set to 0 to disable. Default: 4 | -| setPrimaryGroup | true or false | If the bot should set `yourgroup` in `config.json` as the primary group of each bot. **Does currently not work because of node-steamcommunity!** Default: false | -| commandCooldown | Number in ms | Timeframe in which a user is allowed to use 5 commands before it is considered as spamming and the user gets blocked for 90 seconds. Default: 12000 | +| forceFriendlistSpaceTime | Number in days | Amount of days a user hasn't requested something to get unfriended if only one friend slot is left. Set to 0 to disable. Default: 4 | +| setPrimaryGroup | true or false | If the bot should set `yourgroup` in `config.json` as the primary group of each bot. Default: false | +|   | | | +| commandCooldown | Number in ms | Timeframe in which a user is allowed to use 5 commands before it is considered as spamming and the user gets blocked for 90 seconds. Default: 12000 (12 seconds) | | restrictAdditionalCommandsToOwners | Array with cmd names as strings | Restricts more commands and their aliases to owners only. Default: [] | | retryFailedComments | true or false | If the bot should retry comments that failed in a comment request. Default: false | -| retryFailedCommentsDelay | Number in ms | Time the bot will wait before retrying the failed comments. Default: 300000 | +| retryFailedCommentsDelay | Number in ms | Time the bot will wait before retrying the failed comments. Default: 300000 (5 minutes) | | retryFailedCommentsAttempts | Number | How often the bot should retry a failed comment. Default: 1 | | lastQuotesSize | Number | Amount (minus 1) of different quotes that need to be selected in between before a quote can be used again. Default: 5 | +|   | | | | enableevalcmd | true or false | The eval command allows the botowner to run javascript code from the steam chat. **Warning: This can harm your machine! Leave it to false if you don't know what you are doing!** Default: false | -| enableurltocomment | true or false | Enables or disables the webserver plugin to request comments via URL and to view the log from your browser. Default: false | | printDebug | true or false | Enables and logs debug messages of the bot. Default: false | | steamUserDebug | true or false | Enables and logs debug messages of the steam-user lib. Default: false | | steamUserDebugVerbose | true or false | Enables and logs debug-verbose messages of the steam-user lib. Default: false | | steamSessionDebug | true or false | Enabled and logs debug messages of the steam-session lib. Default: false | | logAnimationSpeed | Number in ms | Time the logging lib will wait between each frame of an animation. Default: 250 | +  + +The undocumented `dummy` keys are there to group certain settings together to improve visibility. +They serve no other purpose and can be ignored. \ No newline at end of file diff --git a/docs/wiki/changelogs/CHANGELOG_v2.14.md b/docs/wiki/changelogs/CHANGELOG_v2.14.md new file mode 100644 index 00000000..804b1a16 --- /dev/null +++ b/docs/wiki/changelogs/CHANGELOG_v2.14.md @@ -0,0 +1,228 @@ +# Version 2.14.x Changelog +[⬅️ Go back to version overview](../version_changelogs.md) + +  + +**Current** +- [2.14.0](#2.14.0) + +  + + + +## **2023-10-21, Version 2.14.0** +**Changes of note (TL;DR):** +- Added support for commenting in discussions using !comment +- Added support for following & unfollowing users/workshops/curators using !follow & !unfollow +- Added support for setting specific games for specific accounts +- Added a language system which currently supports english & russian. Each user can set their lang using !lang + - Reworked `customlang.json` structure to work with the new language system. Please read the updated [customlang wiki page](/docs/wiki/customlang_doc.md)! +- Added a relogging handler which also attempts to switch out broken proxies - you no longer need to manually intervene to get accounts back online! + - The bot now longer stops itself when the main account looses connection +- Renamed `config.json` keys `commentdelay`, `commentcooldown`, `maxComments` & `maxOwnerComments` to `requestDelay`, `requestCooldown`, `maxRequests`, `maxOwnerRequests` to apply to all request types +- Renamed advancedconfig.json key `relogTimeout` to `loginRetryTimeout` +- Fixed a lot of bugs + +If you are using a `customlang.json`, make sure to read the language string changes at the end and update your file. + +  +  + +**Additions:** +- Added new commands: !follow, !unfollow +- Added support for commenting in discussions by updating !comment! Suggestion in [#128](https://github.com/3urobeat/steam-comment-service-bot/issues/128) + - Added a library patch to load my changes until [my PR to the SteamCommunity library gets accepted](https://github.com/DoctorMcKay/node-steamcommunity/pull/319) +- Added support for following & unfollowing users/workshops and curators by adding two new commands as mentioned above! Suggestion in [#163](https://github.com/3urobeat/steam-comment-service-bot/issues/163) & [#207](https://github.com/3urobeat/steam-comment-service-bot/issues/207) + - This feature was added to the SteamCommunity library [in my PR #320](https://github.com/DoctorMcKay/node-steamcommunity/pull/320) +- Added a language system + - Added a `!lang` command to see all supported languages and to update your chosen one + - Added a userSettings database to save language settings for every user who adds the bot + - Added a defaultLanguage setting to `config.json` + - Added a `getLang()` function to the DataManager to get a language string + - Supports replacing language string variables for you + - Automatically fetches the correct language for the user when a userID is provided + - Added russian translation [@Blueberryy](https://github.com/Blueberryy) [#186](https://github.com/3urobeat/steam-comment-service-bot/pull/186), updated by [@sashascurtu](https://github.com/sashascurtu) [#212](https://github.com/3urobeat/steam-comment-service-bot/pull/212) + - Added an unsupported language check to DataManager's dataCheck +- Added automatic renewal of refreshTokens that expire soon + - Enabled automatic renewal in steam-user options + - Added a `attemptTokenRenew()` function to the sessionHandler and call it from the handleExpiringTokens.js helper (this was done before steam-user added support, it now acts as a backup) +- Added a relogging system to attempt to recover failed logins after 15 minutes + - Supports switching out broken proxies - you no longer need to manually intervene to get accounts back online! + - Added a Controller `getBotsPerProxy()` function to enable finding least used proxies + - Added the advancedconfig `relogTimeout` setting to customize the 15 minutes default setting + - Added proxy support to the Controller `checkConnection()` helper and added a `splitProxyString()` helper to Controller.misc + - Added DataManager `checkProxy()` and `checkAllProxies()` helper functions to update `isOnline` for every proxy + - Added a Bot `switchProxy()` function to relog a bot account with a different proxy without needing a restart +- Added a (stripped down for now) developer wiki +- Added a dataIntegrity check to the DataManager to automatically recover corrupted source files by checking their checksum + - The bot can now recover itself from only the initial `start.js` file. Impressive, right? +- Added support for setting specific games for specific accounts. Suggestion in [#193](https://github.com/3urobeat/steam-comment-service-bot/issues/193) +- Added a scripts directory + - Added the langStringsChangeDetector script to generate the lang keys updated list for each changelog + - Added the generateFileStructure script to update `/src/data/fileStructure.json` + - Added the checkTranslationKeys script to find missing or misnamed lang keys in translations +- Added a contributing wiki page +- Added 351 more quotes to default quotes.txt file [@8C](https://github.com/8C) [#210](https://github.com/3urobeat/steam-comment-service-bot/pull/210) +- Added compatibility feature for update from 2.13 to 2.14 + +  + +**Reworks:** +- Reworked `customlang.json` structure to work with the new language system. Please read the updated [customlang wiki page](/docs/wiki/customlang_doc.md)! +- Reworked how variables are set in language strings to easily distinguish them from normal text. They now follow this syntax: `${variableName}` +- Reworked how proxies are loaded and stored in the DataManager to store connection status information + - They are now stored in an array of objects instead of a string array and contain the properties `proxy`, `proxyIndex`, `isOnline` & `lastOnlineCheck` +- Reworked how the logininfo is stored in the DataManager to fix an invalid account order when a username consisting of only numbers was provided + - The accounts are now stored in an array of objects instead of an object with the username as key +- Reworked bot accounts password protection in `!eval` +- Reworked `advancedconfig.json` by adding dummy values that act as separators to group certain settings together +- Reworked Updater's `customUpdateRules()` to carry removed config & advancedconfig values through an update + - The corresponding compatiblity feature must handle the processing & removal of these values +- Replaced all writeFile() calls with DataManager write helper calls +- Replaced every lang usage with `data.getLang()` +- Improved `!settings` command array & object conversion +- Improved log for first time user when installing dependencies +- Improved creating plugins, accounts setup and config setup guide +- The `controller.restart()` function now automatically sets default params if undefined to simplify usage +- DataManager's dataCheck now returns a string containing information when a config value has been reset to default + - The `!settings` command now handles this setting change rejection by informing the user +- Generalized a few lang strings to make translation easier + +  + +**Fixes:** +- Fixed parent process not setting process title when restarting after automatic dependency installation +- Fixed `checkAndGetFile()` failing if npminteraction.js helper is missing +- Fixed npminteraction helper failing if package.json is missing +- Fixed dataManager failing if helpers are missing +- Fixed dataCheck failing if DataManager helpers were not replaced quick enough +- Fixed compability feature check failing if folder is missing +- Fixed handleErrors.js failing if npminteraction.js is helper is missing +- Fixed `dataIntegrity()` resolving too fast when restart is needed +- Fixed weird infinite loop crash in `syncLoop()` when calling `next()` too fast +- Fixed missing game licenses check not working when cache.json is empty +- Fixed dataCheck not resetting change of setting which triggered a promise rejection +- Fixed up-/downvote error detection in sharedfiles libraryPatch for `!vote` & `!downvote` commands +- Fixed sharedfile comment error detection in sharedfiles libraryPatch +- Fixed compatibility check not finding anything due to typo +- Fixed undefined playing status in ready message when config.playingGames = [] +- Fixed the connection check in the Controller not being awaited properly on startup +- Fixed handleMissingGameLicenses only filtering the main account +- Fixed `getBots()` not supporting OFFLINE filter +- Fixed invalid account order when a username consisting only numbers was provided by changing how the logininfo is stored (see above) + +  + +**Changes:** +- Removed library patch for re-enabling primaryGroup profile setting [#287](https://github.com/DoctorMcKay/node-steamcommunity/pull/287) & [#307](https://github.com/DoctorMcKay/node-steamcommunity/pull/307) as the PR was merged +- Removed machineName from logOnOptions. The bot will no longer identify itself when logging into an account +- The bot now longer stops itself when the main account looses connection as the relogging helper takes over +- The bot now only runs botsgroup and missing game licenses checks on the intial login of a bot account, no longer also on relogs +- Create accounts.txt file in dataImport if it is missing +- Miscellaneous log improvements (e.g. less newlines, less messages without dates) +- Renamed `config.json` keys `commentdelay`, `commentcooldown`, `maxComments` & `maxOwnerComments` to `requestDelay`, `requestCooldown`, `maxRequests`, `maxOwnerRequests` to apply to all request types +- Renamed `advancedconfig.json` key `relogTimeout` to `loginRetryTimeout`. `relogTimeout` is now used in `handleRelog`. +- Renamed `defaultlang.json` in `src/data/lang/` to `english.json` +- Updated dataCheck to support the new language system +- Updated `!help` command response to include voting, favorizing and following request types +- Updated wiki pages related to new or changed features +- Updated dependencies +- Minor other changes + +
+ A lot of language strings have changed because the variable syntax has been improved. This list is long, to see it click me + + - These language keys have been added: + - langname + - commentunsupportedtype + - genericnoaccounts + - genericrequestless + - genericnotenoughavailableaccs + - followprocessstarted + - followsuccess + - helpcommentowner + - helpcommentuser + - helpvote + - helpfavorite + - helpfollow + - langcmdsupported + - langcmdnotsupported + - langcmdsuccess + - settingscmdcouldnotconvert + - settingscmdvaluereset +- These language keys have been removed: + - votenoaccounts + - voterequestless + - votenotenoughavailableaccs + - favoritenoaccounts + - favoriterequestless + - favoritenotenoughavailableaccs + - helpcommentowner1 + - helpcommentowner2 + - helpcommentuser1 + - helpcommentuser2 + - helpping + - helpjoingroup +- These language key's values have changed: + - updaterautoupdatedisabled + - commentcmdusageowner + - commentcmdusageowner2 + - commentcmdusage + - commentcmdusage2 + - commentrequesttoohigh + - commentinvalidid + - commentmissingnumberofcomments + - commentzeroavailableaccs + - commentnotenoughavailableaccs + - commentnoaccounts + - commentnounlimitedaccs + - commentprocessstarted + - commentfailedcmdreference + - comment429stop + - commentretrying + - commentsuccess + - voteprocessstarted + - votesuccess + - favoriteprocessstarted + - favoritesuccess + - useradded + - userunfriend + - userforceunfriend + - commandnotfound + - invalidnumber + - invalidprofileid + - invalidsharedfileid + - idoncooldown + - requestaborted + - helpcommandlist + - helpinfo + - helpabort + - helpabout + - helpowner + - helpreadothercmdshere + - pingcmdmessage + - ownercmdmsg + - abortcmdnoprocess + - abortcmdsuccess + - resetcooldowncmdsuccess + - settingscmdsamevalue + - settingscmdvaluechanged + - failedcmdnothingfound + - failedcmdmsg + - sessionscmdmsg + - addfriendcmdacclimited + - addfriendcmdsuccess + - unfriendidcmdsuccess + - unfriendallcmdpending + - joingroupcmdsuccess + - leavegroupcmdsuccess + - leaveallgroupscmdpending + - blockcmdsuccess + - unblockcmdsuccess + - childbotmessage + + This list was generated using my [langStringsChangeDetector.js](/scripts/langStringsChangeDetector.js) script. + +
+ +  \ No newline at end of file diff --git a/docs/wiki/commands_doc.md b/docs/wiki/commands_doc.md index 60c34ba0..e969e2c1 100644 --- a/docs/wiki/commands_doc.md +++ b/docs/wiki/commands_doc.md @@ -11,11 +11,13 @@ This is the full documentation of all commands. Most commands have aliases but s | Command | Usage/Arguments | Description | | ------------- | ---------------- | ------------ | | !help | No arguments | Returns a list of commands available to you and a link to this page. | -| !comment | User: `amount`

Owner: `amount ID [custom quotes]` | Request comments from all available bot accounts. Max amount can be defined in `config.json`.

Owner specific: Provide an ID to send comments to a specific profile, group or sharedfile. You must always provide `amount` when providing `ID`.
A botowner can also provide a custom quote selection in the form of an array [quote1, quote2, ...]. You need to provide all previous arguments.

When no `ID` has been provided the bot will always use the profile of the requesting user. (You) | +| !comment | User: `amount`

Owner: `amount ID [custom quotes]` | Request comments from all available bot accounts. Max amount can be defined in `config.json`.

Owner specific: Provide an ID/url to send comments to a specific profile, group, sharedfile (screenshot, artwork, guide) or discussion (you must provide a full url for discussions). You must always provide `amount` when providing `ID`.
A botowner can also provide a custom quote selection in the form of an array [quote1, quote2, ...]. You need to provide all previous arguments.

When no `ID` has been provided the bot will always use the profile of the requesting user. (You) | | !upvote | `amount ID` | Upvotes a sharedfile with all bot accounts that haven't yet voted on that item. Requires unlimited accounts! | | !downvote | `amount ID` | Downvotes a sharedfile with all bot accounts that haven't yet voted on that item. Requires unlimited accounts! (Owner only.) | | !favorite | `amount ID` | Favorizes a sharedfile with all bot accounts that haven't yet favorized that item. | | !unfavorite | `amount ID` | Unfavorizes a sharedfile with all bot accounts that have favorized that item. (Owners only.) | +| !follow | `amount ID` | Follows a user's workshop or a curator (you must provide a full url for curators) with all bot accounts that haven't yet done so.
Providing an ID/url is owner only, normal users can only request follows for themselves.
When no `ID` has been provided the bot will always use the profile of the requesting user. (You) | +| !unfollow | `amount ID` | Unfollows a user's workshop or a curator (you must provide a full url for curators) with all bot accounts that have done so.
Providing an ID/url is owner only, normal users can only request unfollows for themselves.
When no `ID` has been provided the bot will always use the profile of the requesting user. (You) | | !ping | No arguments | Returns ping in ms to Steam's servers. Can be used to check if the bot is responsive | | !info | No arguments | Returns useful information and statistics about the bot and you. | | !owner | No arguments | Returns a link to the owner's profile set in the config.json. | @@ -23,6 +25,7 @@ This is the full documentation of all commands. Most commands have aliases but s | !abort | `ID` | Abort your own comment process or one on another ID you have started. Owners can also abort requests started by other users. | | !resetcooldown | `profileid` or `global` | Clear your, the profileid's or the comment cooldown of all bot accounts (global). Alias: !rc (Owner only.) | | !settings | `config key` `new value` | Change a value in the config. (Owner only.) | +| !lang | `language` | Set a language which the bot will use to respond to you. This setting is per-user. Provide no argument to get a list of all supported languages. | | !failed | `ID` | See the exact errors of the last comment request on your profile or provide an ID to see the errors of the last request you started. Owners can also view errors for requests started by other users. | | !sessions | No arguments | Displays all active requests. (Owner only.) | | !mysessions | No arguments | Displays all active requests that you have started. | @@ -42,9 +45,10 @@ This is the full documentation of all commands. Most commands have aliases but s | !log | No arguments | Shows the last 15 lines of the log. (Owner only.) | | !eval | `javascript code` | Disabled by default, needs to be toggled on with `enableevalcmd` in config.json.

**Warning!** This will run any javascript code that was provided. It is strongly advised to leave this feature off unless you know exactly what this means! If you have multiple owners configured they can also run code on **your** machine!

(Owner only.) | -

+  + To get more information about responses in form of an error that one of these commands could return, visit the `Errors & FAQ` page in this wiki. -Note about voting & favorizing commands: -The bot only knows about accounts which have already voted/favorized an item for requests that have been made through the bot. +**Note about voting, favorizing & follow commands:** +The bot only knows about accounts which have already voted/favorized/followed an item for requests that have been made through the bot. This is because all requests are stored in a database and we cannot ask Steam for every account on every request as this would spam the heck out of them. \ No newline at end of file diff --git a/docs/wiki/config_doc.md b/docs/wiki/config_doc.md index aff865e0..9b97cbea 100644 --- a/docs/wiki/config_doc.md +++ b/docs/wiki/config_doc.md @@ -12,16 +12,17 @@ This is the full documentation to customize your `config.json`. | Key | Usage | Description | | ------------- | ---------------- | ------------ | | \_help\_ | String | No functionality. Links directly to here to provide easily accessible explanations. | -| commentdelay | Number in ms | Adds a delay between each comment to prevent a cooldown from steam. Default: 7500 +| defaultLanguage | String | Default language to use for new users adding your bot account. [List of supported languages](/src/data/lang/) - Default: "english" | +| requestDelay | Number in ms | Adds a delay between each comment to prevent a cooldown from steam. Default: 15000 (15 seconds) | | skipSteamGuard | true or false | When true, the bot will skip all accounts that require a steamGuard to be typed in when logging in. Default: false | -| commentcooldown | Number in min | Applies this cooldown in minutes to every user after they requested comments. Set to 0 to disable. Default: 5 -| botaccountcooldown | Number in min | Applies this cooldown to every bot account used in a comment request to prevent getting a cooldown from steam. Set to 0 to disable. Default: 10 | -| maxComments | Number | Defines how many comments a normal user can request from your bot. Will automatically use accounts multiple times if it is greater than the amount of accounts logged in. | -| maxOwnerComments | Number | Defines how many comments owners can request (every user in the ownerid array). Will automatically use accounts multiple times if it is greater than the amount of accounts logged in. | -| randomizeAccounts | true or false | Defines if the order of accounts used to comment should be random. Default: false | +| requestCooldown | Number in min | Applies this cooldown in minutes to every user after they started a request. Set to 0 to disable. Default: 5 | +| botaccountcooldown | Number in min | Applies this cooldown to every bot account used in a request to prevent getting a cooldown from Steam. Set to 0 to disable. Default: 10 | +| maxRequests | Number | Defines how many comments/likes/favs a normal user can request from your bot. Will automatically use accounts multiple times if it is greater than the amount of accounts logged in. | +| maxOwnerRequests | Number | Defines how many comments/likes/favs owners can request (every user in the ownerid list below). Will automatically use accounts multiple times if it is greater than the amount of accounts logged in. | +| randomizeAccounts | true or false | Defines if the order of accounts used to comment should be randomized. Default: false | | unfriendtime | Days | Number of days the bot will wait before unfriending someone who hasn't requested a comment in that time period except the owner. Set to 0 to disable. | -| playinggames | ["custom game", game id, game id, ...] | This custom text will be shown on your profile as the name of a game you are playing. The bot will play the set game ids. Don't provide a string to disable the custom game text. | -| childaccplayinggames | ["custom game", game id, game id] | Same behaviour as playinggames but sets the status and games for all child accounts. | +| playinggames | `["custom game", game id, game id, ...]` | This custom text will be shown on your profile as the name of a game you are playing. The bot will play the set game ids. Don't provide a string to disable the custom game text. | +| childaccplayinggames | `["custom game", game id, game id, ...]`

or

`[{ "myacc1": ["Specific Game", 730], "myacc25": [] }, "General Game", 440]` | Same behaviour as playinggames but sets the status and games for all child accounts.
Use the second syntax to set specific games for specific child accounts.
Replace "myacc1" etc. with the username of the corresponding account.

This example will display "Specific Game" game & idle CS2 only for account "myacc1", idle nothing for account "myacc25" and display "General Game" & idle TF2 for all other accounts. | | yourgroup | "url to group" | Advertise your group with the !group command. Leave it empty (like this: "") to disable the command. | | botsgroupid | "url to group" | All bot accounts will join this group. Disable this feature by leaving the brackets empty (like this: ""). | | acceptgroupinvites | true or false | Defines if the bots will accept group invites from other users. A group invite from the main bot will always be accepted. | diff --git a/docs/wiki/contributing.md b/docs/wiki/contributing.md new file mode 100644 index 00000000..38fc6bc3 --- /dev/null +++ b/docs/wiki/contributing.md @@ -0,0 +1,111 @@ +# Contributing to the project +[⬅️ Go back to wiki home](./#readme) + +  + +You would like to contribute to the project itself? +Great! No matter if it's fixing a few typos or adding a whole feature - every contribution is welcome. + +You can see a list of all features that still need to be worked on, are in progress or are finished in the repo's [projects](https://github.com/3urobeat/steam-comment-service-bot/projects) section. + +Please read this page *before* diving in, it contains a few **very** important points! (I'm serious) + +  + +## Table Of Contents +- [Reporting an issue](#reporting-an-issue) +- [How to fork and open pull requests](#how-to-fork-and-open-pull-requests) +- [Translating](#translating) +- [Styling Guidelines](#styling-guidelines) +- [Starting the bot](#starting-the-bot) + +  + +## Reporting an Issue +Found a bug? +Please report it by creating an [issue](https://github.com/3urobeat/steam-comment-service-bot/issues/new/choose) so that I am able to fix it! +If you've got a feature request instead, you can choose the "Feature request" template instead. + +If you have any questions, please open a [Q&A discussion](https://github.com/3urobeat/steam-comment-service-bot/discussions/new?category=q-a) instead! + +  + +## How to fork and open pull requests +To contribute code to the project, you first need to fork this repository. Go to the main page of this repository and click on the "Fork" button in the top right. +Before clicking the "Create fork" button in the next menu, make sure the checkmark at "Copy the `master` branch only" is **unchecked**! +After waiting a few seconds you should now have a *copy* of the repository on your account. + +Go into a folder on your computer where the project should be stored, open a terminal and run the command +`git clone https://github.com/your-username/steam-comment-service-bot` or use any other Git Client of your choice. + +Once the repository has been cloned, switch to the `beta-testing` branch using the command `git checkout beta-testing`. +This branch contains the latest changes and must be the one you base your changes off of. The `master` branch contains the latest release. + +You can now create your own branch using `git checkout -b "branchname"`, make changes and commit them to it. +It makes sense to give the branch a sensible name based on what your changes will be, but no pressure. + +The setup of your dev bot is very similar to the [normal setup](./setup_guide.md), however make sure to run `npm install` manually. This will install all dev dependencies, which are omitted in the normal installation. +It is probably also a good idea to enable `printDebug` in `advancedconfig.json` to see a more detailed log output. + +Once you have made your changes and verified they are working as expected, you need to open a Pull Request to merge them into this repository. +[Click here](https://github.com/3urobeat/steam-comment-service-bot/compare/), click on "Compare across forks" at the top and select `base: beta-testing` on the left side. +Then, choose your fork on the right at `head repository:`, your branch at `compare:` and click on "Create Pull Request". + +Give your pull request a fitting title which describes your changes in a few words and put a more in depth explanation below in the description. +Once you are satisfied, hit the "Create pull request" button below to submit. +I'll take a look at it and perhaps suggest or make some minor changes in the following few days. + +  + +## Translating +You know an unsupported language and would like to contribute a translation? Cool! + +Create a new `.json` file in the `src/data/lang/` directory with the name of the language in English (e.g. "german" instead of "deutsch"). +Please also make sure the filename is lowercase, like the other ones. + +Open the file, copy the content of `english.json` into your file and start translating the value of every key (except the key `langname`, it must be the same as the filename). + +Some language strings contain variables which are replaced by the bot with corresponding values at runtime. +These are marked with `${variablename}` and must occur like that in your translated string as well. + +Some strings also contain command syntax information which must not be translated, like for example in the key `updaterautoupdatedisabled` at the very end: "update true" +These are sadly not 100% obvious but you should be able to recognize them fairly easily. + +When you are done, open a PR like explained above in [How to fork and open pull requests](#how-to-fork-and-open-pull-requests). + +Should you want to test your translation, please make sure to read [Starting the bot](#starting-the-bot) below. Your changes may otherwise get lost! +Should you get an error while starting, make sure your syntax is correct. Common mistakes are for example missing quotation marks `"` or missing commas `,` at line ends. + +  + +## Styling Guidelines +Please make sure your code is somewhat good looking, is easy to read and is properly documented. +Take a look at any of the other source code files in the project to see how I style my code. + +The project includes an [eslint config](/.eslintrc.json) to enforce the project's styling rules, so please make sure your eslint installation works. +It should be included as a dev dependency when setting up the project on your machine. +While working on your code, eslint should automatically display warnings or errors for parts of your code if you are using an IDE. +To run the linter manually, you can execute the command `npx eslint .` in the project folder. +Please make sure to fix all eslint errors and warnings before submitting a pull request. + +In short, the main styling rules are: +- Spaces for indentation with a size of `4` +- camelCase for variables and functions +- Opening braces on the same line as the if/for/while statement +- Do not omit semicolons at line ends +- Do not use `var` but `let` & `const` instead +- Provide JsDocs for your functions, these are also used to generate typescript bindings later on + +  + +## Starting the bot +When starting the bot during development, it is crucial to execute the [generateFileStructure](/scripts/generateFileStructure.js) script before starting the bot, on every change. +You can do this easily by using a simple chained command: +`node scripts/generateFileStructure.js && node start.js` + +This ensures that [fileStructure.json](/src/data/fileStructure.json), which contains checksums for every file, gets updated. +The application will otherwise recognize your changed file as broken and will restore it with the default one. + +  + +Thanks for taking your time! \ No newline at end of file diff --git a/docs/wiki/creating_plugins.md b/docs/wiki/creating_plugins.md index 22448e9e..adb27556 100644 --- a/docs/wiki/creating_plugins.md +++ b/docs/wiki/creating_plugins.md @@ -15,17 +15,16 @@ You should definitely take a look at the developer documentation though, it expl   ## Table Of Contents - -- [Getting started](#getting-started) -- [The filestructure](#filestructure) -- [Exposed functions and events](#functions) -- [Logging messages](#logging) -- [Plugin System Interface](#pluginsystem) -- [Controller](#controller) -- [Command System](#commandhandler) -- [Typescript](#typescript) -- [Packing and installing your plugin using npm](#npm) -- [Additional information](#additional-info) +- [Getting started](#getting-started) +- [The filestructure](#filestructure) +- [Exposed functions and events](#functions) +- [Logging messages](#logging) +- [Plugin System Interface](#pluginsystem) +- [Controller](#controller) +- [Command System](#commandhandler) +- [Typescript](#typescript) +- [Packing and installing your plugin using npm](#npm) +- [Additional information](#additional-info)   @@ -49,7 +48,9 @@ Populate description, author and version as well. The plugin will be packed into Open the entry file `plugin.js` and edit the PluginSystem import file path at the top. It should point to your `steam-comment-service-bot` installation. This makes sure your code editor's IntelliSense will work. If your plugin folder is right beside the bot folder, the default path should already be correct. -**Important:** This path will cause errors when the plugin is being loaded. Uncomment the import before packing & publishing your plugin. + +**Important:** +When packing your plugin using npm you **need to comment this path out**. This path can only be left in while developing and debugging the plugin using `npm link` (explained at [Additional information](#additional-info)) as the relative path will change.   @@ -59,9 +60,9 @@ If your plugin folder is right beside the bot folder, the default path should al Each plugin consists of three important files. -- `plugin.js` - The entry file of your plugin. This one will be loaded by the bot and contains all the functions exposed by your plugin. It must contain an exposed constructor and load function. -- `config.json` - The default configuration file of your plugin. This one will be copied into the plugin config folder by the bot the first time your plugin gets loaded. It must contain the parameter "enabled", everything else is up to you. -- `package.json` - The NPM package config file of your plugin. This one will be read by NPM to package and install your plugin. The bot will go through all installed npm packages with the `steam-comment-bot-` name prefix and attempt to load their `plugin.js` file. +- `plugin.js` - The entry file of your plugin. This one will be loaded by the bot and contains all the functions exposed by your plugin. It must contain an exposed constructor and load function. +- `config.json` - The default configuration file of your plugin. This one will be copied into the plugin config folder by the bot the first time your plugin gets loaded. It must contain the parameter "enabled", everything else is up to you. +- `package.json` - The NPM package config file of your plugin. This one will be read by NPM to package and install your plugin. The bot will go through all installed npm packages with the `steam-comment-bot-` name prefix and attempt to load their `plugin.js` file. You can of course add more files and folders as you like and load them from the `plugin.js` file. @@ -103,13 +104,13 @@ Please do not use any `console.log` calls in your plugins (unless maybe for debu Here is the parameter structure, first to last: -- One of these types: 'debug', 'info', 'warn', 'error'. Debug mesages are only logged if `printDebug` is set to true in `advancedconfig.json` -- The message you want to log. If not of datatype string, the library will attempt to colorize the data, just like console.log does. -- Optional - nodate: true if the message should not have a date -- Optional - remove: true if the next message should overwrite this one -- Optional - animation: An array containing strings. If this is specified, it will display each element of this array after another in the front of the message as an animation. The logger library has some default animations, check them out using your IntelliSense at: `logger.animations` or [here](https://github.com/3urobeat/output-logger/blob/master/lib/data/animations.json) -- Optional - printNow: true to force print this message now. This will skip the log hold back system explained below -- Optional - cutToWidth: true to force cut this message to the current width of the terminal +- One of these types: 'debug', 'info', 'warn', 'error'. Debug mesages are only logged if `printDebug` is set to true in `advancedconfig.json` +- The message you want to log. If not of datatype string, the library will attempt to colorize the data, just like console.log does. +- Optional - nodate: true if the message should not have a date +- Optional - remove: true if the next message should overwrite this one +- Optional - animation: An array containing strings. If this is specified, it will display each element of this array after another in the front of the message as an animation. The logger library has some default animations, check them out using your IntelliSense at: `logger.animations` or [here](https://github.com/3urobeat/output-logger/blob/master/lib/data/animations.json) +- Optional - printNow: true to force print this message now. This will skip the log hold back system explained below +- Optional - cutToWidth: true to force cut this message to the current width of the terminal Check out the JsDoc of the logger function directly: [controller logger.js](../../src/controller/helpers/logger.js) @@ -129,7 +130,7 @@ The Plugin System is responsible for loading, checking and calling plugin functi It also stores references to the controller, giving you access to every part of the bot. The Plugin System is probably the most important part for you when developing a plugin. -Check it out using your IntelliSense and take a look at the developer documentation (TODO). +Check it out using your IntelliSense and take a look at the [developer documentation](../dev/pluginSystem/pluginSystem.md).   @@ -143,10 +144,10 @@ It is responsible for loading all modules on start, storing references to them, You can access it from your plugin through the Plugin System: `sys.controller` From there, every other module of the bot is accessible. Worth noting: -- Functions to restart/stop the application, resolving steamIDs, getting bot accounts, etc. -- `sys.controller.data` - The DataManager object which contains every loaded datafile (e.g. logininfo, config, quotes, proxies, etc.) -- `sys.controller.bots` - Object which holds references to all Bot objects, mapped to their account names. Access any bot account that is in use right now from there. I recommend using the `getBots()` function instead of accessing this object directly. -- `sys.controller.commandHandler` - The CommandHandler object, read more [below](#commandhandler). +- Functions to restart/stop the application, resolving steamIDs, getting bot accounts, etc. +- `sys.controller.data` - The DataManager object which contains every loaded datafile (e.g. logininfo, config, quotes, proxies, etc.) +- `sys.controller.bots` - Object which holds references to all Bot objects, mapped to their account names. Access any bot account that is in use right now from there. I recommend using the `getBots()` function instead of accessing this object directly. +- `sys.controller.commandHandler` - The CommandHandler object, read more [below](#commandhandler). ...and much more. Check it out using your code editor's IntelliSense. @@ -171,8 +172,8 @@ this.plugin.commandHandler.runCommand( ["5", "3urobeat"], // Arguments Array (x, y, msg) => { logger("info", "Comment Command said: " + msg) }, // Response Function this, // The current context - { cmdprefix: "/", userID: "12345", ownerIDs: ["12345"] // The resInfo object -}) + { cmdprefix: "/", userID: "12345", ownerIDs: ["12345"] } // The resInfo object +) ``` This would cause the comment command to send 5 comments to the Steam Profile "3urobeat". @@ -190,13 +191,11 @@ This is also where the `ownerIDs` array comes into play: It allows you to overwr -## **Typescript:** - -For the best development environment you can utilize our typing file in TS -just take the [typing file](https://github.com/3urobeat/steam-comment-service-bot/blob/beta-testing/types/types.d.ts) and copy it to your project +## **Typescript** -there you can point your ts compiler to the file by adding the following to your tsconfig.json +You can also write your plugins in TS by utilizing the project's [typing file](https://github.com/3urobeat/steam-comment-service-bot/blob/beta-testing/types/types.d.ts). +Copy it over to your plugin project and point your TS compiler to it by adding the following to your `tsconfig.json`: ```json { "compilerOptions": { @@ -205,7 +204,7 @@ there you can point your ts compiler to the file by adding the following to your } ``` -The [web server](https://github.com/DerDeathraven/steam-comment-bot-rest-api) plugin is written in TS and can be used as a example +The official [REST API](https://github.com/DerDeathraven/steam-comment-bot-rest-api) plugin is written in TS and can be used as an example.   diff --git a/docs/wiki/customlang_doc.md b/docs/wiki/customlang_doc.md index 18877ecd..f03ae727 100644 --- a/docs/wiki/customlang_doc.md +++ b/docs/wiki/customlang_doc.md @@ -7,21 +7,32 @@ This page will instruct you on how to modify the messages the bot will send to a This includes nearly all messages and isn't difficult! Please open the file `customlang.json` which is located in the bot's folder. -If this file doesn't exist for you (which will be the case if you updated to `2.10`) then please create it and copy&paste [this](https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/customlang.json) into it. -Every message that you are able to change is included [here](https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/master/src/data/lang/defaultlang.json). +This file allows you to change all supported language keys for every supported language. +Every message that you are able to change is included [here](https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/master/src/data/lang/english.json) and every supported language [here](https://github.com/3urobeat/steam-comment-service-bot/tree/master/src/data/lang). -To change a message add a comma to the end of the previous line and add the key of the message you want to change to your `customlang.json` **with the same syntax** you saw in the list with all messages. -After you have done that add a colon behind your key and write your message in the brackets. +  -Example of how your `customlang.json` could look like after changing the `useradded` message: -``` +Take a look at the example below to see how you can modify the `useradded` message for english and the `pingcmdmessage` message for russian: +```json { - "note": "Please read here on how to use this file: https://github.com/3urobeat/steam-comment-service-bot/blob/master/docs/wiki/customlang_doc.md", - "useradded": "You will recieve this message if you add me and it was modified using the customlang file!", + "english": { + "note": "Please read here on how to use this file: https://github.com/3urobeat/steam-comment-service-bot/blob/master/docs/wiki/customlang_doc.md", + "useradded": "You will recieve this message if you add me and it was modified using the customlang file!" + }, + "russian": { + "pingcmdmessage": "Imagine this is russian and your ping took ${pingtime}ms." + } } -``` +``` +The `note` key is being ignored by the bot, it is only there to conveniently link you to this article. + +Please make sure that you follow the JSON syntax closely, the file will otherwise fail to load. +Common mistakes are missing colons at line ends (except the last one) or missing brackets. + +  -Notes: -- Some messages may have words in them that will be replaced by the bot when sending the message. You should be able to spot them easily like for example `steamID64` in the `failedcmdmsg` message. You should include them in your custom message as well. -- Don't hardcode command prefixes and instead use the keyword `cmdprefix`. It will be replaced by messages that support it by default. \ No newline at end of file +Some questions and notes: +- Some messages include variables. These are recognizeable by the syntax `${someVariableName}`. You must include them using the same syntax in your message so the bot can correctly replace them before sending. +- You cannot add new languages here. If you want to add a translation, please [read the howto article here](./contributing.md#translating). +- Don't hardcode command prefixes and instead use the keyword `${cmdprefix}`. It will be replaced by messages that support it by default. \ No newline at end of file diff --git a/docs/wiki/errors_doc.md b/docs/wiki/errors_doc.md index c8c2c1bb..2e399173 100644 --- a/docs/wiki/errors_doc.md +++ b/docs/wiki/errors_doc.md @@ -32,7 +32,7 @@ Please don't take all error descriptions for granted. Steam sometimes seems to t | HTTP Errors | | ----- | `Error: HTTP Error 403`: Steam denied your request. Why? I don't know for sure. -`Error: HTTP Error 429`: Your IP has made too many requests to Steam and got a cooldown. Wait a few minutes and try again. You can increase the commentdelay or reduce the amount of accounts using one IP (for example with proxies) to combat this. +`Error: HTTP Error 429`: Your IP has made too many requests to Steam and got a cooldown. Wait a few minutes and try again. You can increase the requestDelay or reduce the amount of accounts using one IP (for example with proxies) to combat this. `Error: HTTP Error 500`: The steam servers seem to have a problem/are down. Check [steam server status.](https://steamstat.us) `Error: HTTP Error 502`: The steam servers seem to have a problem/are down. Check [steam server status.](https://steamstat.us) `Error: HTTP Error 504`: The steam servers are slow atm/are down. Check [steam server status.](https://steamstat.us) diff --git a/docs/wiki/setup_guide.md b/docs/wiki/setup_guide.md index 6e3dc475..15d7dbc2 100644 --- a/docs/wiki/setup_guide.md +++ b/docs/wiki/setup_guide.md @@ -31,25 +31,30 @@ Click here: [Download](https://github.com/3urobeat/steam-comment-service-bot/arc Extract the zip and open the `steam-comment-service-bot` folder. You need to have at least node.js version 14.15.0 installed: [Download](https://nodejs.org) -To get your version number type `node --version` in your console or terminal. +If you already have node installed, check the version number by running `node --version` in your console or terminal. If you need a tutorial for this specific node part, [click here.](https://youtu.be/8J78rC9Z28U?t=60)   ## Setup & Configuration: #### Accounts: -Open the `accounts.txt` file and provide your accounts in the `username:password:shared_secret` format, one account per line. +The bot needs at least a few Steam Accounts configured to be effective. +These accounts are used to do the interactions in the SteamCommunity which you request (e.g. commenting, voting, favorizing, ...). +Creating a few accounts manually shouldn't take long. Make sure to give them a username and profile picture so they don't *instantly* look like random Bot accounts. + +Open the `accounts.txt` file with a text editor and provide your accounts in the `username:password:shared_secret` format, one account per line. If you don't want to use a shared_secret just leave it out and only provide the account in the `username:password` format. - -Please make sure you know about limited/unlimited accounts. Your accounts also need to have E-Mail Steam Guard active. -You can read a detailed explanation [here in the wiki](./steam_limitations.md). +The first account which you provide in this file will be the one you interact with to run commands to request comments, see info, etc. + +Make sure your accounts have E-Mail Steam Guard activated! This is a requirement from Steam to be able to comment at all! +I highly recommend that you take a quick look at the [Steam Limitations wiki page](./steam_limitations.md) to learn more about what you can and cannot do with your accounts.
Another, optional method (not recommended anymore): If you'd rather like to provide your accounts in an object notation (JSON), then empty the accounts.txt file and create a `logininfo.json` file. Fill out the usernames and passwords of each bot account you want to use, following this object notation format: - ``` + ```json { "bot0": ["username0", "password0", "shared_secret"], "bot1": ["username1", "password1", "shared_secret"], @@ -60,19 +65,26 @@ You can read a detailed explanation [here in the wiki](./steam_limitations.md). You can add more accounts by extending the list ("bot4": ["username4", "password4", "shared_secret"], etc...). Make sure to **NOT** forget a comma after each line, **ONLY** the last line **MUST NOT** have a comma! (ignoring this will cause errors!) + + This was the method of providing login credentials back in the day and is kept for backwards compatiblity. + It is not recommended anymore as the chance of making a syntax mistake is way higher and requires more effort to extend for lots of accounts.
  #### Config: -Open `config.json` with a text editor. -You need to provide the link to your steam profile at "owner" and the link or your steam64id of your profile at "ownerid", following the existing template. -Make sure to put your link and or ID inside the brackets, just like the template shows. - -Set an amount of comments a normal user and the amount an owner is allowed to request from the bot. -This largely depends on how many accounts you use, the commentdelay set and if you use proxies. -I would recommend max 2 comments per account if you use no proxies and default settings, so if you use 5 accounts, try setting maxComments and maxOwnerComments to 10. +Open the `config.json` file with a text editor of your choice. +We need to configure just a couple of things - your profile link, your ID and max comments - the rest can be left at default for now. + +- **First,** provide the link to your steam profile at "owner". Example: `"owner": "https://steamcommunity.com/id/3urobeat",` +- **Second,** provide the same link, just the vanity or steamID64 inside the "ownerid" array. This will give yourself owner rights, giving you access to more features and certain owner only commands. If you want to set multiple owners, check out the [config documentation](./config_doc.md). +Example (using the vanity): `"ownerid": ["3urobeat"]` +- **Third,** set an amount a normal user and an owner is allowed to request from the bot at once. This largely depends on how many accounts you use, the delay set and if you use proxies. +For now, I would recommend max 2 comments per account if you use no proxies and default settings. So if you use 5 accounts, try setting "maxRequests" and "maxOwnerRequests" to `10` and leave the requestDelay at default. +Example: `"maxRequests": 10,` & `"maxOwnerRequests": 10,` +Make sure your formatting follows the default `config.json` exactly (especially the commas at the end of every line and quotation marks)! + For now you can ignore all the other settings, however if you'd like to customize more values later on then check out the [complete config documentation](./config_doc.md).   @@ -96,7 +108,7 @@ For now you can ignore all the other settings, however if you'd like to customiz   -The bot is now ready! Do not modify any of the other files. +The bot is now ready to be started! Do not modify any of the other files.   @@ -105,7 +117,7 @@ Open up a power shell/terminal in this folder and type `node start.js`. > **Important Disclaimer:** Do not start the bot with a tool that restarts on changes (like nodemon etc)! Only use normal `node`. -Head over to your Steam client, add the main bot (the first account in your accounts.txt) as friend and send him the message `!help`. +Head over to your Steam client, add the main bot (the first account in your accounts.txt) as friend and send him the chat message `!help`. It should respond with a list of commands available to you. To request a comment, simply type `!comment 1`! @@ -117,4 +129,4 @@ You can see all commands and their usage [here in the wiki](./commands_doc.md). ## That's it! 🎉 Congrats, you've successfully set up the bot! -Head back to the README by [clicking here](../..#setup--config-guide)! \ No newline at end of file +Head back to the README by [clicking here](../..#setup--config-guide)! diff --git a/docs/wiki/version_changelogs.md b/docs/wiki/version_changelogs.md index c4e8837c..440041a0 100644 --- a/docs/wiki/version_changelogs.md +++ b/docs/wiki/version_changelogs.md @@ -26,3 +26,4 @@ Every page contains a 'Go back' link at the top to get redirected back to this o - [2.11.x](./changelogs/CHANGELOG_v2.11.md) - [2.12.x](./changelogs/CHANGELOG_v2.12.md) - [2.13.x](./changelogs/CHANGELOG_v2.13.md) +- [2.14.x](./changelogs/CHANGELOG_v2.14.md) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b9c7abb2..81eba3c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "steam-comment-service-bot", - "version": "2.13.6", + "version": "2.14.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "steam-comment-service-bot", - "version": "2.13.6", + "version": "2.14.0", "license": "GPL-3.0", "dependencies": { "@seald-io/nedb": "^4.0.2", - "@types/tail": "^2.2.1", + "@types/tail": "^2.2.2", "download": "^8.0.0", "htmlparser2": "^9.0.0", "https": "^1.0.0", @@ -18,15 +18,15 @@ "request": "^2.88.2", "steam-comment-bot-rest": "^1.1.0", "steam-comment-bot-webserver": "file:plugins/steam-comment-bot-webserver-1.0.0.tgz", - "steam-session": "^1.3.0", - "steam-user": "^4.29.1", - "steamcommunity": "^3.46.1", + "steam-session": "^1.6.0", + "steam-user": "^5.0.1", + "steamcommunity": "^3.47.1", "steamid": "^2.0.0", - "steamid-resolver": "^1.3.3" + "steamid-resolver": "^1.3.4" }, "devDependencies": { - "eslint": "^8.47.0", - "eslint-plugin-jsdoc": "^46.4.6", + "eslint": "^8.52.0", + "eslint-plugin-jsdoc": "^46.8.2", "tsd-jsdoc": "^2.5.0" } }, @@ -40,9 +40,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.10.tgz", - "integrity": "sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true, "peer": true, "bin": { @@ -74,9 +74,9 @@ } }, "node_modules/@doctormckay/stdlib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@doctormckay/stdlib/-/stdlib-2.7.0.tgz", - "integrity": "sha512-mNHFwj/U5kBxsh00vuqILEw95keMAsbxIIewkSZ9ORUoOxMIaA4WWlbqx3BFjU7aF+0/QLuA4ey9y4MFw4cj0w==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@doctormckay/stdlib/-/stdlib-2.9.0.tgz", + "integrity": "sha512-5lG6MYx749UgPpsCJ9zceERU/Bd7847Tpsy03gdrBI7FLLkrHLYT1dh8QQNz7OLLfxnN0xgAzZXu0nZhwsiVZg==", "dependencies": { "psl": "^1.9.0" }, @@ -89,6 +89,11 @@ "resolved": "https://registry.npmjs.org/@doctormckay/steam-crypto/-/steam-crypto-1.2.0.tgz", "integrity": "sha512-lsxgLw640gEdZBOXpVIcYWcYD+V+QbtEsMPzRvjmjz2XXKc7QeEMyHL07yOFRmay+cUwO4ObKTJO0dSInEuq5g==" }, + "node_modules/@doctormckay/user-agents": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@doctormckay/user-agents/-/user-agents-1.0.0.tgz", + "integrity": "sha512-F+sL1YmebZTY2CnjoR9BXFEULpq7y8dxyLx48LZVa0BSDseXdLG/DtPISfM1iNv1XKCeiBzVNfAT/MOQ69v1Zw==" + }, "node_modules/@es-joy/jsdoccomment": { "version": "0.40.1", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.40.1.tgz", @@ -119,9 +124,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", - "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", + "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -151,21 +156,21 @@ } }, "node_modules/@eslint/js": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.47.0.tgz", - "integrity": "sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", + "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, @@ -187,9 +192,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "node_modules/@nodelib/fs.scandir": { @@ -319,32 +324,32 @@ } }, "node_modules/@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.4.tgz", + "integrity": "sha512-N7UDG0/xiPQa2D/XrVJXjkWbpqHCd2sBaB32ggRF2l83RhPfamgKGF8gwwqyksS95qUS5ZYF9aF+lLPRlwI2UA==", "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "node_modules/@types/bytebuffer": { - "version": "5.0.44", - "resolved": "https://registry.npmjs.org/@types/bytebuffer/-/bytebuffer-5.0.44.tgz", - "integrity": "sha512-k1qonHga/SfQT02NF633i+7tIfKd+cfC/8pjnedcfuXJNMWooss/FkCgRMSnLf2WorLjbuH4bfgAZEbtyHBDoQ==", + "version": "5.0.46", + "resolved": "https://registry.npmjs.org/@types/bytebuffer/-/bytebuffer-5.0.46.tgz", + "integrity": "sha512-QxINdj2nX5ITZfRk4fOZza9IGVJ0QLBeVIuUcEEcmTm1MyPn0PRiK+tf8K7XXwJ+fjT9S5aKt+z78NlCm2RNtA==", "dependencies": { "@types/long": "^3.0.0", "@types/node": "*" } }, "node_modules/@types/caseless": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.4.tgz", + "integrity": "sha512-2in/lrHRNmDvHPgyormtEralhPcN3An1gLjJzj2Bw145VBxkQ75JEXW6CTdMAwShiHQcYsl2d10IjQSdJSJz4g==" }, "node_modules/@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "version": "3.4.37", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.37.tgz", + "integrity": "sha512-zBUSRqkfZ59OcwXon4HVxhx5oWCJmc0OtBTK05M+p0dYjgN6iTwIL2T/WbsQZrEsdnwaF9cWQ+azOnpPvIqY3Q==", "dependencies": { "@types/node": "*" } @@ -355,17 +360,17 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, "node_modules/@types/cors": { - "version": "2.8.13", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", - "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "version": "2.8.15", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.15.tgz", + "integrity": "sha512-n91JxbNLD8eQIuXDIChAN1tCKNWCEgpceU9b7ZMbFA+P+Q4yIeh80jizFLEvolRPc1ES0VdwFlGv+kJTSirogw==", "dependencies": { "@types/node": "*" } }, "node_modules/@types/express": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", - "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.20.tgz", + "integrity": "sha512-rOaqlkgEvOW495xErXMsmyX3WKBInbhG5eqojXYi3cGUaLoRDlXa5d52fkfWZT963AZ3v2eZ4MbKE6WpDAGVsw==", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -374,9 +379,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.35", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz", - "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==", + "version": "4.17.39", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.39.tgz", + "integrity": "sha512-BiEUfAiGCOllomsRAZOiMFP7LAnrifHpt56pc4Z7l9K6ACyN06Ns1JLMBxwkfLOjJRlSf06NwWsT7yzfpaVpyQ==", "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -385,22 +390,22 @@ } }, "node_modules/@types/file-manager": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/file-manager/-/file-manager-2.0.0.tgz", - "integrity": "sha512-XEEsKApHT68NqUxDscnw/tAizJS07aKs20RXDkepfdx4n8M+bvPGO+Ty1whiLhhyLPw1y/6QnyVliu65ySYlcg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/file-manager/-/file-manager-2.0.2.tgz", + "integrity": "sha512-G4MGSeQ0tOuK5f5ApSb9ft5z0hpLaVP4yOzDspNPjyVmNSKpFWGb1vVCurJ83X+ydUpZd3yR/hie9xFJLbgCeg==", "dependencies": { "@types/node": "*" } }, "node_modules/@types/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.3.tgz", + "integrity": "sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA==" }, "node_modules/@types/linkify-it": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", - "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.4.tgz", + "integrity": "sha512-hPpIeeHb/2UuCw06kSNAOVWgehBLXEo0/fUs0mw3W2qhqX89PI2yvok83MnuctYGCPrabGIoi0fFso4DQ+sNUQ==", "dev": true, "peer": true }, @@ -421,36 +426,39 @@ } }, "node_modules/@types/mdurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", - "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.4.tgz", + "integrity": "sha512-ARVxjAEX5TARFRzpDRVC6cEk0hUIXCCwaMhz8y7S1/PxU6zZS1UMjyobz7q4w/D/R552r4++EhwmXK1N2rAy0A==", "dev": true, "peer": true }, "node_modules/@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.4.tgz", + "integrity": "sha512-1Gjee59G25MrQGk8bsNvC6fxNiRgUlGn2wlhGf95a59DrprnnHk80FIMMFG9XHMdrfsuA119ht06QPDXA1Z7tw==" }, "node_modules/@types/node": { - "version": "20.4.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.9.tgz", - "integrity": "sha512-8e2HYcg7ohnTUbHk8focoklEQYvemQmu9M/f43DZVx43kHn0tE3BY/6gSDxS7k0SprtS0NHvj+L80cGLnoOUcQ==" + "version": "20.8.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz", + "integrity": "sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==", + "dependencies": { + "undici-types": "~5.25.1" + } }, "node_modules/@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + "version": "6.9.9", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.9.tgz", + "integrity": "sha512-wYLxw35euwqGvTDx6zfY1vokBFnsK0HNrzc6xNHchxfO2hpuRg74GbkEW7e3sSmPvj0TjCDT1VCa6OtHXnubsg==" }, "node_modules/@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.6.tgz", + "integrity": "sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA==" }, "node_modules/@types/request": { - "version": "2.48.8", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.8.tgz", - "integrity": "sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==", + "version": "2.48.11", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.11.tgz", + "integrity": "sha512-HuihY1+Vss5RS9ZHzRyTGIzwPTdrJBkCm/mAeLRYrOQu/MGqyezKXWOK1VhCnR+SDbp9G2mRUP+OVEqCrzpcfA==", "dependencies": { "@types/caseless": "*", "@types/node": "*", @@ -472,18 +480,18 @@ } }, "node_modules/@types/send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz", - "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==", + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.3.tgz", + "integrity": "sha512-/7fKxvKUoETxjFUsuFlPB9YndePpxxRAOfGC/yJdc9kTjTeP5kRCTzfnE8kPUKCeyiyIZu0YQ76s50hCedI1ug==", "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "node_modules/@types/serve-static": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.2.tgz", - "integrity": "sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.4.tgz", + "integrity": "sha512-aqqNfs1XTF0HDrFdlY//+SGUxmdSUbjeRXb5iaZc3x0/vMbYmdw9qvOgHWOyyLFxSSRnUuP5+724zBgfw8/WAw==", "dependencies": { "@types/http-errors": "*", "@types/mime": "*", @@ -491,9 +499,9 @@ } }, "node_modules/@types/steam-user": { - "version": "4.26.4", - "resolved": "https://registry.npmjs.org/@types/steam-user/-/steam-user-4.26.4.tgz", - "integrity": "sha512-Lpmz0gHz4T1Wle00+zwV1OK72U5TW0utlTga8aac0R3vt9k/iRuXsL5mdnv5v8O6mwY6YGectPgRop5DZlqUcA==", + "version": "4.26.6", + "resolved": "https://registry.npmjs.org/@types/steam-user/-/steam-user-4.26.6.tgz", + "integrity": "sha512-+3gCVc8zsCMt9IRbSS1bhBBsTyzYX1GxreiqBx2Uz+MO3YDxx47E+3aTafqsFWXGOy+jbu6q+kAqVrPtHpaQYg==", "dependencies": { "@types/bytebuffer": "*", "@types/file-manager": "*", @@ -502,9 +510,9 @@ } }, "node_modules/@types/steamcommunity": { - "version": "3.43.2", - "resolved": "https://registry.npmjs.org/@types/steamcommunity/-/steamcommunity-3.43.2.tgz", - "integrity": "sha512-p4K6cfqusXkhvdHDLCMW2t0EG+EVlrE4i7LbFEdSjCvsp9hGYX/Q0ym9Tw2v6DOumnHbtDzKBvefW6Zv1Y2oVA==", + "version": "3.43.4", + "resolved": "https://registry.npmjs.org/@types/steamcommunity/-/steamcommunity-3.43.4.tgz", + "integrity": "sha512-wmLGf5GfhKs/tWr3jppppiOmbzdYsDucnmS1lCcDmvXysiMIEhIL60Y8g7BU337dKyBzyotlWRMjae0UU4cGvA==", "dependencies": { "@types/node": "*", "@types/request": "*", @@ -512,19 +520,25 @@ } }, "node_modules/@types/steamid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/steamid/-/steamid-2.0.1.tgz", - "integrity": "sha512-B8sy9wfOOeh+yNrFFzyghCQ1n3rssYbTvZD2kXzuy3HSJEK7TVeYnrpXG/0dOKrq1VU8D7cWORgszsdqKBZnVg==" + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/steamid/-/steamid-2.0.2.tgz", + "integrity": "sha512-dCXjh122ilAmmpmiCNfxzjXpT/V9dpGhekXa24+EUX0vOxaSapW3UDEV7K8QrfZtzxGUFvxt515KJkdneZEMLQ==" }, "node_modules/@types/tail": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@types/tail/-/tail-2.2.1.tgz", - "integrity": "sha512-j75Gs5MiIpNR14wztQ4vtViUqxZi+lcgflyXC7P9iMgNnMab7XcV5p+2590IO3njsWWn5l8C+55ILk2CDDyaHg==" + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@types/tail/-/tail-2.2.2.tgz", + "integrity": "sha512-+CjjgMFjIVgTYsJXWNpAKVRerFWc9c+GTMzY/336fSW6BhY5TJwo2CNYJiNq7mO9rBHmtmpceKf2DnkrnaR3Vg==" }, "node_modules/@types/tough-cookie": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", - "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==" + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.4.tgz", + "integrity": "sha512-95Sfz4nvMAb0Nl9DTxN3j64adfwfbBPEYq14VN7zT5J5O2M9V6iZMIIQU1U+pJyl9agHYHNCqhCXgyEtIRRa5A==" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true }, "node_modules/accepts": { "version": "1.3.8", @@ -710,9 +724,9 @@ "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" }, "node_modules/axios": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", + "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -984,6 +998,19 @@ "node": ">=4" } }, + "node_modules/cacheable-request/node_modules/json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==" + }, + "node_modules/cacheable-request/node_modules/keyv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", + "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", + "dependencies": { + "json-buffer": "3.0.0" + } + }, "node_modules/cacheable-request/node_modules/lowercase-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", @@ -993,12 +1020,13 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1513,6 +1541,19 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1663,9 +1704,9 @@ } }, "node_modules/engine.io": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.2.tgz", - "integrity": "sha512-IXsMcGpw/xRfjra46sVZVHiSWo/nJ/3g1337q9KNXtS6YRzbW5yIzTCb9DjhrBe7r3GZQR0I4+nq+4ODk5g/cA==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.3.tgz", + "integrity": "sha512-IML/R4eG/pUS5w7OfcDE0jKrljWS9nwnEfsxWCIJF5eO6AHo6+Hlv+lQbdlAYsiJPHzUthLm1RUjnBzWOs45cw==", "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -1727,18 +1768,19 @@ } }, "node_modules/eslint": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.47.0.tgz", - "integrity": "sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", + "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "^8.47.0", - "@humanwhocodes/config-array": "^0.11.10", + "@eslint/js": "8.52.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -1781,9 +1823,9 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "46.4.6", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.4.6.tgz", - "integrity": "sha512-z4SWYnJfOqftZI+b3RM9AtWL1vF/sLWE/LlO9yOKDof9yN2+n3zOdOJTGX/pRE/xnPsooOLG2Rq6e4d+XW3lNw==", + "version": "46.8.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.8.2.tgz", + "integrity": "sha512-5TSnD018f3tUJNne4s4gDWQflbsgOycIKEUBoCLn6XtBMgNHxQFmV8vVxUtiPxAQq8lrX85OaSG/2gnctxw9uQ==", "dev": true, "dependencies": { "@es-joy/jsdoccomment": "~0.40.1", @@ -2070,17 +2112,17 @@ "dev": true }, "node_modules/fast-xml-parser": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.7.tgz", - "integrity": "sha512-J8r6BriSLO1uj2miOk1NW0YVm8AGOOu3Si2HQp/cSmo6EA4m3fcwu2WKjJ4RK9wMLBtg69y1kS8baDiQBR41Ig==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.2.tgz", + "integrity": "sha512-rmrXUXwbJedoXkStenj1kkljNF7ugn5ZjR9FJcwmCfcCbtOMDghPajbc+Tck6vE6F5XsDmx+Pr2le9fw8+pXBg==", "funding": [ - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" - }, { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" } ], "dependencies": { @@ -2131,9 +2173,9 @@ } }, "node_modules/file-manager/node_modules/@doctormckay/stdlib": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/@doctormckay/stdlib/-/stdlib-1.16.0.tgz", - "integrity": "sha512-mObNOnuEgEb+hZKkd6mY2PoB+9gLfuYkmv8ggN/R3JFjaRIWDOR1QlBV3psQZs7TqGpe3ZFB6bgddQGE02psOA==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@doctormckay/stdlib/-/stdlib-1.16.1.tgz", + "integrity": "sha512-XhuUOzElz6fnNdt70IYNKqhPAEpGaL4JHOhAvklRh0hAhVPW+/wLxaWT3DWUbaG5Dta5YvIp7+cZK3GhIpAuug==", "engines": { "node": ">=6.0.0" } @@ -2214,28 +2256,29 @@ } }, "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", "dev": true, "dependencies": { - "flatted": "^3.1.0", + "flatted": "^3.2.9", + "keyv": "^4.5.3", "rimraf": "^3.0.2" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=12.0.0" } }, "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", "funding": [ { "type": "individual", @@ -2327,19 +2370,22 @@ "dev": true }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2397,9 +2443,9 @@ } }, "node_modules/globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -2497,17 +2543,6 @@ "node": ">=6" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2517,6 +2552,17 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", @@ -2572,6 +2618,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/htmlparser2": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.0.0.tgz", @@ -2997,9 +3054,10 @@ } }, "node_modules/json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true }, "node_modules/json-schema": { "version": "0.4.0", @@ -3037,11 +3095,12 @@ } }, "node_modules/keyv": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", - "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, "dependencies": { - "json-buffer": "3.0.0" + "json-buffer": "3.0.1" } }, "node_modules/klaw": { @@ -3451,9 +3510,9 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3709,9 +3768,9 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/protobufjs": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", - "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", + "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", @@ -4010,9 +4069,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" }, "node_modules/seek-bzip": { "version": "1.0.6", @@ -4096,6 +4155,20 @@ "node": ">= 0.8.0" } }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -4255,9 +4328,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", - "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", "dev": true }, "node_modules/split": { @@ -4272,9 +4345,9 @@ } }, "node_modules/sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -4319,9 +4392,9 @@ } }, "node_modules/steam-appticket/node_modules/@doctormckay/stdlib": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/@doctormckay/stdlib/-/stdlib-1.16.0.tgz", - "integrity": "sha512-mObNOnuEgEb+hZKkd6mY2PoB+9gLfuYkmv8ggN/R3JFjaRIWDOR1QlBV3psQZs7TqGpe3ZFB6bgddQGE02psOA==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@doctormckay/stdlib/-/stdlib-1.16.1.tgz", + "integrity": "sha512-XhuUOzElz6fnNdt70IYNKqhPAEpGaL4JHOhAvklRh0hAhVPW+/wLxaWT3DWUbaG5Dta5YvIp7+cZK3GhIpAuug==", "engines": { "node": ">=6.0.0" } @@ -4337,9 +4410,9 @@ "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, "node_modules/steam-appticket/node_modules/protobufjs": { - "version": "6.11.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", - "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", @@ -4400,11 +4473,12 @@ } }, "node_modules/steam-session": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/steam-session/-/steam-session-1.3.0.tgz", - "integrity": "sha512-s0gThNvX0TG3B5Po2hZQQZ2dr4OlTD+Lu6D2ucIssq6xUPdgEm7Ky2hVvOXLVYSWy7IdSMaq7lWzSI3s2lq81g==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/steam-session/-/steam-session-1.6.0.tgz", + "integrity": "sha512-28bTCqxP27hcVRSwMW60tVKQeosFJiNcjJYGJ3yb15IrGFWQAJqHFTRmVZIUYTTMxUiFgv+5ns9dpLXz+eA7DA==", "dependencies": { - "@doctormckay/stdlib": "^2.4.1", + "@doctormckay/stdlib": "^2.9.0", + "@doctormckay/user-agents": "^1.0.0", "debug": "^4.3.4", "kvparser": "^1.0.1", "node-bignumber": "^1.2.2", @@ -4412,7 +4486,7 @@ "socks-proxy-agent": "^7.0.0", "steamid": "^2.0.0", "tiny-typed-emitter": "^2.1.0", - "websocket13": "^3.0.1" + "websocket13": "^4.0.0" }, "engines": { "node": ">=12.22.0" @@ -4427,12 +4501,12 @@ } }, "node_modules/steam-user": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/steam-user/-/steam-user-4.29.1.tgz", - "integrity": "sha512-J4Z3QZRvB6ETRMe/oSiW3byLdqixUBv2UrBJ9cGeyFCuxl0z69zuarK/IbCFxDeCM2t8zLQwY9l0z94ofzF15w==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/steam-user/-/steam-user-5.0.1.tgz", + "integrity": "sha512-kH0u0v4qobbnkxDcWyZNoAvOkbCJH9KBbRGRssSfkmAX4fZZQYlJfJwvojL0DIRJr6/3C4tycqXCHYW0+B1Xlg==", "dependencies": { "@bbob/parser": "^2.2.0", - "@doctormckay/stdlib": "^1.16.0", + "@doctormckay/stdlib": "^2.7.1", "@doctormckay/steam-crypto": "^1.2.0", "adm-zip": "^0.5.10", "binarykvparser": "^2.2.0", @@ -4440,82 +4514,32 @@ "file-manager": "^2.0.0", "kvparser": "^1.0.1", "lzma": "^2.3.2", - "protobufjs": "^6.11.3", + "protobufjs": "^7.2.4", "socks-proxy-agent": "^7.0.0", "steam-appticket": "^1.0.1", - "steam-session": "^1.2.5", + "steam-session": "^1.3.4", "steam-totp": "^2.0.1", - "steamid": "^1.1.0", - "websocket13": "^3.0.1" + "steamid": "^2.0.0", + "websocket13": "^4.0.0" }, "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/steam-user/node_modules/@doctormckay/stdlib": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/@doctormckay/stdlib/-/stdlib-1.16.0.tgz", - "integrity": "sha512-mObNOnuEgEb+hZKkd6mY2PoB+9gLfuYkmv8ggN/R3JFjaRIWDOR1QlBV3psQZs7TqGpe3ZFB6bgddQGE02psOA==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/steam-user/node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" - }, - "node_modules/steam-user/node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "node_modules/steam-user/node_modules/protobufjs": { - "version": "6.11.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", - "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", - "@types/node": ">=13.7.0", - "long": "^4.0.0" - }, - "bin": { - "pbjs": "bin/pbjs", - "pbts": "bin/pbts" - } - }, - "node_modules/steam-user/node_modules/steamid": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/steamid/-/steamid-1.1.3.tgz", - "integrity": "sha512-t86YjtP1LtPt8D+TaIARm6PtC9tBnF1FhxQeLFs6ohG7vDUfQuy/M8II14rx1TTUkVuYoWHP/7DlvTtoCGULcw==", - "dependencies": { - "cuint": "^0.2.1" + "node": ">=14.0.0" } }, "node_modules/steamcommunity": { - "version": "3.46.1", - "resolved": "https://registry.npmjs.org/steamcommunity/-/steamcommunity-3.46.1.tgz", - "integrity": "sha512-gze62L5K6TcIdrA0Rn7PNCnA9wRrQC9x79WzjiO09fvigl1fd1ZfFrT+Arje7YQsVDUVJISn4nh9FN5PjtX2iw==", + "version": "3.47.1", + "resolved": "https://registry.npmjs.org/steamcommunity/-/steamcommunity-3.47.1.tgz", + "integrity": "sha512-1XRfFunFnCxt3Ww5tvCKeZJtAMUmz4e8JJI1kC7BX2EN4wBhTNZCbzhz4ZvJxUw9WZMG47UQ2ajpmcditCCe8w==", "dependencies": { + "@doctormckay/user-agents": "^1.0.0", "async": "^2.6.3", "cheerio": "0.22.0", "image-size": "^0.8.2", - "node-bignumber": "^1.2.1", "request": "^2.88.0", + "steam-session": "^1.6.0", "steam-totp": "^1.5.0", "steamid": "^1.1.3", - "xml2js": "^0.4.22" + "xml2js": "^0.6.2" }, "engines": { "node": ">=4.0.0" @@ -4546,9 +4570,9 @@ } }, "node_modules/steamid-resolver": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/steamid-resolver/-/steamid-resolver-1.3.3.tgz", - "integrity": "sha512-Hc8RwEuYjwUZy4/WUKh5vVf/tev80QSmAV0mFi6ExIZ/4zPjJgAQRAjo3URyay/NguUzvElDypAkliRG7aN2aA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/steamid-resolver/-/steamid-resolver-1.3.4.tgz", + "integrity": "sha512-JCUs/n4+5DHoGxmtFbMzhglSKGFrAJoB1oMEkMN3RkStRLqSHVR8iLNJUAElQMbA0Q+J6zYDGkR/2F2sKQsEjQ==", "dependencies": { "https": "^1.0.0", "xml2js": "^0.5.0" @@ -4869,9 +4893,9 @@ } }, "node_modules/typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4903,6 +4927,11 @@ "dev": true, "peer": true }, + "node_modules/undici-types": { + "version": "5.25.3", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", + "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5007,26 +5036,18 @@ } }, "node_modules/websocket13": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/websocket13/-/websocket13-3.0.2.tgz", - "integrity": "sha512-OgzxBXT9eQ0eEapeK2OKFlgKpegQnjMweHLUwGmfJhm+IbgpZ+NPT2eAEP1UUUjBslbmKpZ47rFU6Dp6agfyGQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/websocket13/-/websocket13-4.0.0.tgz", + "integrity": "sha512-/ujP9ZfihyAZIXKGxcYpoe7Gj4r5o3WYSfP93o9lVNhhqoBtYba4m1s3mxdjKZu/HOhX5Mcqrt89dv/gC3b06A==", "dependencies": { - "@doctormckay/stdlib": "^1.8.0", + "@doctormckay/stdlib": "^2.7.1", "bytebuffer": "^5.0.1", "permessage-deflate": "^0.1.7", "tiny-typed-emitter": "^2.1.0", "websocket-extensions": "^0.1.4" }, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/websocket13/node_modules/@doctormckay/stdlib": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/@doctormckay/stdlib/-/stdlib-1.16.0.tgz", - "integrity": "sha512-mObNOnuEgEb+hZKkd6mY2PoB+9gLfuYkmv8ggN/R3JFjaRIWDOR1QlBV3psQZs7TqGpe3ZFB6bgddQGE02psOA==", - "engines": { - "node": ">=6.0.0" + "node": ">=12.22.0" } }, "node_modules/which": { @@ -5044,12 +5065,12 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", - "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", "dependencies": { "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "call-bind": "^1.0.4", "for-each": "^0.3.3", "gopd": "^1.0.1", "has-tostringtag": "^1.0.0" @@ -5087,9 +5108,9 @@ } }, "node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" diff --git a/package.json b/package.json index c0998dfc..29b24197 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "steam-comment-service-bot", - "version": "2.13.6", - "description": "Request a ton of steam profile/group comments from a bot network with just one command!", + "version": "2.14.0", + "description": "Steam Multi Account Manager with built-in comment, like & favorite commands and extensive plugin support.", "main": "start.js", "dependencies": { - "@types/tail": "^2.2.1", "@seald-io/nedb": "^4.0.2", + "@types/tail": "^2.2.2", "download": "^8.0.0", "htmlparser2": "^9.0.0", "https": "^1.0.0", @@ -13,14 +13,15 @@ "request": "^2.88.2", "steam-comment-bot-rest": "^1.1.0", "steam-comment-bot-webserver": "file:plugins/steam-comment-bot-webserver-1.0.0.tgz", - "steam-session": "^1.3.0", - "steam-user": "^4.29.1", - "steamcommunity": "^3.46.1", + "steam-session": "^1.6.0", + "steam-user": "^5.0.1", + "steamcommunity": "^3.47.1", "steamid": "^2.0.0", - "steamid-resolver": "^1.3.3" + "steamid-resolver": "^1.3.4" }, "scripts": { "start": "node start.js", + "dev": "npm run types ; node scripts/generateFileStructure.js && node start", "types": "jsdoc -t node_modules/tsd-jsdoc/dist -r src/. -d types" }, "author": "3urobeat", @@ -31,8 +32,8 @@ "homepage": "https://github.com/3urobeat", "repository": "https://github.com/3urobeat/steam-comment-service-bot", "devDependencies": { - "eslint": "^8.47.0", - "eslint-plugin-jsdoc": "^46.4.6", + "eslint": "^8.52.0", + "eslint-plugin-jsdoc": "^46.8.2", "tsd-jsdoc": "^2.5.0" }, "types": "./types/types.d.ts" diff --git a/quotes.txt b/quotes.txt index 9a4dbe45..11e4b9b5 100644 --- a/quotes.txt +++ b/quotes.txt @@ -40,7 +40,7 @@ Sick looking profile you got there (☞゚ヮ゚)☞ ☜(゚ヮ゚☜) Enjoy the day! ^_^ +rep always learning new things -+rep cs:go god ++rep cs2 god +rep so cool person spread happy vibes! stay safe! 😷 @@ -77,4 +77,353 @@ Hey, what are you currently doing? Enjoy the day with a nice cup of tea! I wish you the best! Sign my profile please ❤ -⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⣿⣿⠿⠛⣉⣥⣼⣿⣿\n⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣿⣿⣿⣿⠿⠿⣿⣿⡟⠛⢿⣿⣿⠇⡀⢹⣆⠰⠿⠛⣉⣻⣿⣿\n⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢹⣿⠿⠛⢉⣡⣽⡿⠋⣠⣿⣿⠟⠁⣠⣶⡀⢻⡇⠀⠈⢿⠏⢠⣿⡀⢻⡄⣶⡿⠟⢻⣿⣿\n⣿⣿⣿⣿⠉⢿⣏⢻⣿⣿⣿⣿⢸⡀⠴⠞⠛⢙⣿⣀⣈⡙⠛⠿⠀⢸⣿⠿⠁⠘⣃⢀⠂⠀⣰⣿⣿⣇⣀⣧⣤⣴⣾⣿⣿⣿\n⣿⣿⣿⡿⠰⠘⢿⣆⢻⡇⠈⠏⢸⣷⠠⣶⡿⠟⢿⣿⣿⠿⠟⢂⣴⡤⣿⣿⣿⣿⣿⣿⣿⣿⣿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿\n⣿⣿⣿⠁⣴⡆⢻⣿⡄⠁⣼⣄⣼⣿⣄⣠⣴⣾⣿⣿⣿⣷⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿\n⣿⣿⣿⣠⣿⣷⣾⣿⣿⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ \ No newline at end of file +⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⣿⣿⠿⠛⣉⣥⣼⣿⣿\n⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣿⣿⣿⣿⠿⠿⣿⣿⡟⠛⢿⣿⣿⠇⡀⢹⣆⠰⠿⠛⣉⣻⣿⣿\n⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢹⣿⠿⠛⢉⣡⣽⡿⠋⣠⣿⣿⠟⠁⣠⣶⡀⢻⡇⠀⠈⢿⠏⢠⣿⡀⢻⡄⣶⡿⠟⢻⣿⣿\n⣿⣿⣿⣿⠉⢿⣏⢻⣿⣿⣿⣿⢸⡀⠴⠞⠛⢙⣿⣀⣈⡙⠛⠿⠀⢸⣿⠿⠁⠘⣃⢀⠂⠀⣰⣿⣿⣇⣀⣧⣤⣴⣾⣿⣿⣿\n⣿⣿⣿⡿⠰⠘⢿⣆⢻⡇⠈⠏⢸⣷⠠⣶⡿⠟⢿⣿⣿⠿⠟⢂⣴⡤⣿⣿⣿⣿⣿⣿⣿⣿⣿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿\n⣿⣿⣿⠁⣴⡆⢻⣿⡄⠁⣼⣄⣼⣿⣄⣠⣴⣾⣿⣿⣿⣷⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿\n⣿⣿⣿⣠⣿⣷⣾⣿⣿⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ +☕+rep friendly +🍵+rep Good Friend +😎+rep awesome guy +🔫+rep Nice AIM +👏+rep Good Job +👤+rep Great Man +👍+rep best gamer in CS2 +📢+rep great HIGHLY recommended +⚔+rep trusted CS2 player +📈+rep pro player +🔱+rep clutch KING +⚜+rep nice CLUTCH, wow ++rep nice player 👌 ++rep good player 👍 ++rep one tap 👌 ++rep 🎮 friendly person 🎮 ++rep good Ak-47 👌 ++rep Good M4A4 👌 ++rep 💢 ONE TAP MACHINE 💢 ++rep Amazing Tactics 👌 ++rep Top Player 🔝🔝 ++rep Thanks For Carry 👍 ++rep 🎮 Great Teammate 🎮 ++rep Friendly ✔️✔️ ++rep Killing Machine 😈 ++rep can you sign me?? ++rep very nice profile 👌 ++rep non-toxic player 👌 ++rep nice profile 💜💜 ++Rep ++rep Only Headshot ++rep Seviliyorsun ++rep Good ++rep Good SPREY ++rep Awp King ++rep Aimbot dedected ++rep Good Teammate ++rep Friendly Person💜 ++rep Headmachine ++rep ONE TAP MACHINE ++rep Good Player ++rep Best Player +-rep cheater ++rep Leader ++rep Good player ++rep Epic Clutch ++rep Clutchmeister ++rep 1Tap Only ++rep Insane Skills ++rep One shot man! ++rep Thx for carry ++rep Epic Comeback ++rep Good Teammate ++rep Friendly Person ++rep ONE TAP MACHINE ++rep AK GOD ++rep DEAGLE MASTER ++rep Amazing Tactics ++rep Killing Machine ++rep Nice to meet you! ++rep Really Good ++rep LEGIT Player ++rep AWP GOD ++rep Clutch King ++rep Nice Profile. ++rep communicates well ++rep amazing communication ++rep god player ++rep a skilled player at the game ++rep clutched a 1v2 ++rep clutched a 1v3 before switching sides ++rep clutched a 1v4 on match point ++rep a good leader for the team ++rep knows the game too well ++rep secured us the victory ++rep Legit Player ++rep Good Player ++rep Good Aim ++rep Top Player ++rep amazing tactics ++rep!! +Nice profile! 💖 +Wow, So many game hours? ✨ ++Rep, Best CS2 Player! 💎 ++Rep, Secured us the victory. 👑 +gg! +🖤666🖤 +🤍🤍🤍🤍🤍🤍🤍🤍🤍 +🔥🔥🔥🔥🔥 +🖤🧡❤️💚💙🤍❤️💜🧡🤍💚💛💙🖤💜❤️🧡💛 +Hey there, thanks for the friend invite ❤️ It's nice to meet you +Thanks for the friend invite ❤️ +Awesome! +Grove street leader and very good player +plus rep +SO GOOD ACC +You was my friend with my other acc :) +sigh pls💛 +best sausagesucker in the world <3 +🖤🧡❤️💚 Have a GOOD Day 💜❤️🧡💛 +nice akk +he is legit +Hey, can you like & comment on my screenshot? +GG +omg +Respect very good player +yo +nice bro +rep +in the future, my comments will be legends! +hi +what a gamer +eat me +sheeeeesh +О.о +Nice One! +rep my profile man =(( +hey nice account +Legend +lmao +666 +❤️69🔥 +duuuuuuuuuuuuuuuuuud +nice +ok +bruh +much love +crazy bro🔥 +sheesh bro +pog +Feel FREE to add me +nice lvl +yes +sussy +Hmmmmmmmm +hot +yeah +hola +damn cute +Look at me +Robotaim +Howw are you? +Have a nice day<3 +SIGN ME DADDY +<3 best +You could do so many different cool profiles but you keep this ugly one :D +ops +How was your weekend? +i love you +Holy cow +Beautiful profile! +YAY +nice profile, i dig it <3 +<3 +one love +I see you too. +what do you do for a living? +gaming +who ++_+ +thx :3 +OwO is good +so nice +so good player :0 +nice clutch man <3 +good sniper uwwu +god <333333 +100 skill 0 luck :0 +Kind person :) +#1 +GOD +Global +Global Elite +I like you +The best teammate +best teammate +11 lvl faceit +major player +secound s1mple +❤️❤️❤️ +🖤🖤🖤 +🖤 LOVE 🤍 +wooooooo nice profile +pls give me skin +wow nice skins +rich boyy +richh +best player in the world +the best player in the world +pls sign me daddy ❤️🔥 +play with me ❤️ +hard boy 🔥❤️ +hard +you're mine ❤️❤️ +hello everyone +the whole world sees it :D +Give a kiss ❤️ +💸💸💸 +🐐 +The 🐐 +😈😈😈 +😈❤️😈 +hell +hell 🖤 +the ruler of hell ++rep good aim ++rep never give up ++rep friendly ++rep good player ++rep good nades ++rep good teacher ++rep god ++rep very ood player ++rep best player in the world ++rep i love you ++rep L ❤️ you ++rep very good aim ++rep god aim ++rep good timing ++rep best timing ++rep good movment ++rep movment god ++rep playing like 11 lvl faceit ++rep legit ++rep legit player ++rep best mate ++rep best awp ++rep major player ++rep robotaim ++rep good ++rep god ++rep nice profile ++rep big namer +big namer ++rep thx for carry ++rep always carry ++rep thx for carry +40k god ++rep 40 kills ++rep ++rep accept my invite +accept my invite pls ++rep thx for win ++rep owo ++rep uwu +1tap ++rep onetap machine ++rep legend +legend +legend 🔥 +onetap machine +king ++rep king +deagle god ++rep deagle god +big +hot ++rep big ++rep hot +insane player +insane clutch ++rep insane player ++rep insane timing ++rep insane +insane +You are insane!!!! +good nades +OnetapKing +never lose ++rep never lose +110% +100% hs +100% HS GOD ++rep 80% hs +<33 +marry me +:b +:) ++rep :D +:D +veryy goodd +I L❤️VE YOU +🤍❤️ +HOT 🔥 +:0 +W gamer +wanna play game +W rizz ++rep gamer +TOP1 +meow +++++++rep +yoooooooooooooooooo man +China is a cool country imo +CS2 God +Legendary +1 tapper ++rep nice guy +pls accept friend request +he is insane +pew pew +1v9 +s1mple time? +the most amazing player I've ever seen ++REP +MACHINE +big boy +smurf +smurfing +god cs2 0_0 +boost me pls +how do you train your aim? +nice day +good day +rank_global_1 +mega reaction ++ rep good comms +good aim +rep +add me back +your crazy +W ++rep 666km/h peek +dope profile +rep +signed by me :) +Hey nice account +rep my profile man =(( +hell 🖤 ++rep, legend ESL player +Just give him AK and wait... XD +It's speed demon xD +Insane Skilss +That movement bro +rep +99% skill 1%lucky +rep thanks for win ;) +this guy is better than ScreaM ++rep best aimer +What a god +10/10 player +thanks for free carry +5 bullets - 5 kills :D +420/10 player ++rep Really Good ++rep clutched a 1v4 on match point ++rep clutched a 1v2 ++rep a skilled player at the game ++rep ak monster ++rep awp monster ++rep high skill ++rep 300 iq +teach me +good guy, + REP diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 00000000..7674b760 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,8 @@ +# steam-comment-service-bot Scripts +This directory holds various scripts that aid in developing and releasing updates. + +  + +- checkTranslationKeys: Finds misnamed, missing or obsolete language strings in all translations +- generateFileStructure: Updates "src/data/fileStructure.json" which is used to recover broken files. +- langStringsChangeDetector: Generates language file changes for the version changelog. \ No newline at end of file diff --git a/scripts/checkTranslationKeys.js b/scripts/checkTranslationKeys.js new file mode 100644 index 00000000..5b30787f --- /dev/null +++ b/scripts/checkTranslationKeys.js @@ -0,0 +1,60 @@ +/* + * File: checkTranslationKeys.js + * Project: steam-comment-service-bot + * Created Date: 13.09.2023 21:58:32 + * Author: 3urobeat + * + * Last Modified: 13.09.2023 22:30:34 + * Modified By: 3urobeat + * + * Copyright (c) 2023 3urobeat + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. If not, see . + */ + + +/* + This script compares all translations with the english one to detect misnamed, missing or obsolete language strings +*/ + + +const fs = require("fs"); +const eng = require("../src/data/lang/english.json"); + + +// Find all translations inside the same directory +let translations = fs.readdirSync("./src/data/lang/"); + +console.log(`Checking ${translations.length - 1} translation(s). If the script exits with no further messages, all translations contain the same keys.`); + + +// Iterate through all translations +translations.forEach((name) => { + if (name == "english.json") return; // Skip "original" language + let lang; + + // Attempt to load language + try { + lang = require("../src/data/lang/" + name); + } catch(err) { + console.log(`WARNING: Failed to load language '${name}': ${err}`); + } + + + // Get key arrays of both translations + let engKeys = Object.keys(eng); + let langKeys = Object.keys(lang); + + + // Check lang for missing keys + engKeys.forEach((e) => { + if (!langKeys.includes(e)) console.log(`Language '${name}' is missing language key '${e}'!`); + }); + + // Check lang for obselete/misnamed keys + langKeys.forEach((e) => { + if (!engKeys.includes(e)) console.log(`Language '${name}' contains obsolete/misnamed key '${e}'.`); + }); +}); \ No newline at end of file diff --git a/scripts/generateFileStructure.js b/scripts/generateFileStructure.js new file mode 100644 index 00000000..845ad231 --- /dev/null +++ b/scripts/generateFileStructure.js @@ -0,0 +1,88 @@ +/* + * File: generateFileStructure.js + * Project: steam-comment-service-bot + * Created Date: 02.09.2023 14:41:54 + * Author: 3urobeat + * + * Last Modified: 09.09.2023 15:15:20 + * Modified By: 3urobeat + * + * Copyright (c) 2023 3urobeat + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. If not, see . + */ + + +/* + This script generates a JSON file containing a path and corresponding GitHub URL for every file in the project. + This is used to check and recover missing files on startup. + The output is copied to "src/data/fileStructure.json". +*/ + + +const fs = require("fs"); +const path = require("path"); +const crypto = require("crypto"); + +const ignore = [ + ".git", "node_modules", "backup", "plugins", // Folders + "accounts.txt", "config.json", "proxies.txt", "quotes.txt", "advancedconfig.json", "config.json", "customlang.json", // Config files + "output.txt", "src/data/cache.json", "src/data/data.json", "src/data/fileStructure.json", "src/data/lastcomment.db", "src/data/ratingHistory.db", "src/data/tokens.db", "src/data/userSettings.db", // Files changing at runtime + "comment-service-bot.code-workspace" // Misc +]; + +const output = []; + + +/** + * Iterates through all files in the project and pushes them to the output + * @param {string} src Project root path + * @param {boolean} firstCall Set to `true` on first call, will be set to `false` on recursive call + */ +function searchFolderRecursiveSync(src, firstCall) { + let files = []; + + // Copy files or call function again if dir + if (fs.lstatSync(src).isDirectory()) { + files = fs.readdirSync(src); + + let targetFolder = path.join("./", src); + + files.forEach(async (file) => { + let filepath = targetFolder + "/" + file; + + // Check if the file resides in the project root and prevent path from resulting in .//start.js + if (targetFolder == "./") filepath = file; + + // Ignore this file/folder if name is in ignore array + if (ignore.includes(filepath)) return; + + let curSource = path.join(src, file); + + // Recursively call this function again if this is a dir + if (fs.lstatSync(curSource).isDirectory()) { + searchFolderRecursiveSync(curSource, false); + + } else { + + // Construct URL and calculate checksum + let fileurl = "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/" + filepath; + let filesum = crypto.createHash("md5").update(fs.readFileSync(filepath)).digest("hex"); + + // Add file to output array + output.push({ "path": filepath, "url": fileurl, "checksum": filesum }); + } + }); + } + + // Write output when we are finished and not in a deeper recursion level + if (firstCall) { + fs.writeFileSync("./src/data/fileStructure.json", JSON.stringify({ "files": output }, null, 4)); + + console.log(`Done! Found ${output.length} files and wrote them to 'src/data/fileStructure.json'.`); + } +} + +searchFolderRecursiveSync("./", true); \ No newline at end of file diff --git a/scripts/langStringsChangeDetector.js b/scripts/langStringsChangeDetector.js new file mode 100644 index 00000000..adf2bb88 --- /dev/null +++ b/scripts/langStringsChangeDetector.js @@ -0,0 +1,51 @@ +/* + * File: langStringsChangeDetector.js + * Project: steam-comment-service-bot + * Created Date: 05.06.2023 13:59:22 + * Author: 3urobeat + * + * Last Modified: 09.09.2023 12:47:40 + * Modified By: 3urobeat + * + * Copyright (c) 2023 3urobeat + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. If not, see . + */ + + +/* + This script compares two language files and generates an output to include in the changelog. +*/ + + +// Load files +const oldLang = require("./oldLang.json"); +const newLang = require("./newLang.json"); + + +// Check for additions +console.log("- These language keys have been added:"); + +Object.keys(newLang).forEach((e, i) => { + if (!oldLang[e]) console.log(` - ${e}`); + + // Check for removed + if (i + 1 == Object.keys(newLang).length) { + console.log("- These language keys have been removed:"); + + Object.keys(oldLang).forEach((f, j) => { + if (!newLang[f]) console.log(` - ${f}`); + + // Check for changed + if (j + 1 == Object.keys(oldLang).length) { + console.log("- These language key's values have changed:"); + + Object.keys(newLang).forEach((g) => { + if (oldLang[g] && oldLang[g] != newLang[g]) console.log(` - ${g}`); + }); + } + }); + } +}); \ No newline at end of file diff --git a/src/bot/bot.js b/src/bot/bot.js index 77ae7fbb..9d97cbc2 100644 --- a/src/bot/bot.js +++ b/src/bot/bot.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 08.07.2023 00:41:25 + * Last Modified: 21.10.2023 12:26:46 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -17,10 +17,11 @@ const SteamUser = require("steam-user"); const SteamCommunity = require("steamcommunity"); -const request = require("request"); // Yes I know, the library is deprecated but steamcommunity uses it as well so it is being used anyway +const request = require("request"); // Yes I know, the library is deprecated but we must wait for node-steamcommunity to drop the lib as well const EStatus = require("./EStatus.js"); const Controller = require("../controller/controller.js"); // eslint-disable-line +const DataManager = require("../dataManager/dataManager.js"); // eslint-disable-line const SessionHandler = require("../sessions/sessionHandler.js"); @@ -68,11 +69,20 @@ const Bot = function(controller, index) { * Additional login related information for this bot account */ this.loginData = { - logOnOptions: Object.values(controller.data.logininfo)[index], // TODO: This could be an issue later when the index could change at runtime + logOnOptions: controller.data.logininfo[index], // TODO: This could be an issue later when the index could change at runtime logOnTries: 0, + relogTries: 0, // Amount of times logOns have been retried after relogTimeout. handleRelog() attempts to cycle proxies after enough failures waitingFor2FA: false, // Set by sessionHandler's handle2FA helper to prevent handleLoginTimeout from triggering proxyIndex: proxyIndex, - proxy: controller.data.proxies[proxyIndex] + proxy: controller.data.proxies[proxyIndex].proxy + }; + + /** + * Stores the timestamp and reason of the last disconnect. This is used by handleRelog() to take proper action + */ + this.lastDisconnect = { + timestamp: 0, + reason: "" }; // Define the log message prefix of this account in order to @@ -91,6 +101,7 @@ const Bot = function(controller, index) { require("./helpers/checkMsgBlock.js"); require("./helpers/handleLoginTimeout.js"); require("./helpers/handleMissingGameLicenses.js"); + require("./helpers/handleRelog.js"); require("./helpers/steamChatInteraction.js"); // Create sessionHandler object for this account @@ -100,14 +111,15 @@ const Bot = function(controller, index) { logger("debug", `[${this.logPrefix}] Using proxy ${this.loginData.proxyIndex} "${this.loginData.proxy}" to log in to Steam and SteamCommunity...`); // Force protocol for now: https://dev.doctormckay.com/topic/4187-disconnect-due-to-encryption-error-causes-relog-to-break-error-already-logged-on/?do=findComment&comment=10917 - this.user = new SteamUser({ autoRelogin: false, httpProxy: this.loginData.proxy, protocol: SteamUser.EConnectionProtocol.WebSocket }); + this.user = new SteamUser({ autoRelogin: false, renewRefreshTokens: true, httpProxy: this.loginData.proxy, protocol: SteamUser.EConnectionProtocol.WebSocket }); this.community = new SteamCommunity({ request: request.defaults({ "proxy": this.loginData.proxy }) }); // Pass proxy to community library as well // Load my library patches require("../libraryPatches/CSteamSharedFile.js"); - require("../libraryPatches/profile.js"); require("../libraryPatches/sharedfiles.js"); require("../libraryPatches/helpers.js"); + require("../libraryPatches/CSteamDiscussion.js"); + require("../libraryPatches/discussions.js"); if (global.checkm8!="b754jfJNgZWGnzogvl e.proxy == this.user.options.httpProxy); + // Log login message for this account, with mentioning proxies or without - if (!this.loginData.proxy) logger("info", `[${this.logPrefix}] Trying to log in without proxy... (Attempt ${this.loginData.logOnTries}/${this.controller.data.advancedconfig.maxLogOnRetries + 1})`, false, true, logger.animation("loading")); - else logger("info", `[${this.logPrefix}] Trying to log in with proxy ${this.loginData.proxyIndex}... (Attempt ${this.loginData.logOnTries}/${this.controller.data.advancedconfig.maxLogOnRetries + 1})`, false, true, logger.animation("loading")); + if (!thisProxy.proxy) logger("info", `[${this.logPrefix}] Trying to log in without proxy... (Attempt ${this.loginData.logOnTries}/${this.controller.data.advancedconfig.maxLogOnRetries + 1})`, false, true, logger.animation("loading")); + else logger("info", `[${this.logPrefix}] Trying to log in with proxy ${thisProxy.proxyIndex}... (Attempt ${this.loginData.logOnTries}/${this.controller.data.advancedconfig.maxLogOnRetries + 1})`, false, true, logger.animation("loading")); // Attach loginTimeout handler this.handleLoginTimeout(); @@ -174,13 +189,53 @@ module.exports = Bot; /* -------- Register functions to let the IntelliSense know what's going on in helper files -------- */ +/** + * Handles the SteamUser debug events if enabled in advancedconfig + */ +Bot.prototype._attachSteamDebugEvent = function() {}; + +/** + * Handles the SteamUser disconnect event and tries to relog the account + */ +Bot.prototype._attachSteamDisconnectedEvent = function() {}; + +/** + * Handles the SteamUser error event + */ +Bot.prototype._attachSteamErrorEvent = function() {}; + +/** + * Handles messages, cooldowns and executes commands. + */ +Bot.prototype._attachSteamFriendMessageEvent = function() {}; + +/** + * Do some stuff when account is logged in + */ +Bot.prototype._attachSteamLoggedOnEvent = function() {}; + +/** + * Accepts a friend request, adds the user to the lastcomment.db database and invites him to your group + */ +Bot.prototype._attachSteamFriendRelationshipEvent = function() {}; + +/** + * Accepts a group invite if acceptgroupinvites in the config is true + */ +Bot.prototype._attachSteamGroupRelationshipEvent = function() {}; + +/** + * Handles setting cookies and accepting offline friend & group invites + */ +Bot.prototype._attachSteamWebSessionEvent = function() {}; + /** * Checks if user is blocked, has an active cooldown for spamming or isn't a friend * @param {object} steamID64 The steamID64 of the message sender * @param {string} message The message string provided by steam-user friendMessage event * @returns {boolean} `true` if friendMessage event shouldn't be handled, `false` if user is allowed to be handled */ -Bot.prototype.checkMsgBlock = function(steamID64, message) {}; // eslint-disable-line +Bot.prototype.checkMsgBlock = async function(steamID64, message) {}; // eslint-disable-line /** * Handles aborting a login attempt should an account get stuck to prevent the bot from softlocking (see issue #139) @@ -192,6 +247,23 @@ Bot.prototype.handleLoginTimeout = function() {}; */ Bot.prototype.handleMissingGameLicenses = function() {}; +/** + * Changes the proxy of this bot account and relogs it. + * @param {number} newProxyIndex Index of the new proxy inside the DataManager.proxies array. + */ +Bot.prototype.switchProxy = function(newProxyIndex) {}; // eslint-disable-line + +/** + * Checks host internet connection, updates the status of all proxies checked >2.5 min ago and switches the proxy of this bot account if necessary. + * @returns {Promise.} Resolves with a boolean indicating whether the proxy was switched when done. A relog is triggered when the proxy was switched. + */ +Bot.prototype.checkAndSwitchMyProxy = async function() {}; + +/** + * Attempts to get this account, after failing all logOnRetries, back online after some time. Does not apply to initial logins. + */ +Bot.prototype.handleRelog = function() {}; + /** * Our commandHandler respondModule implementation - Sends a message to a Steam user * @param {object} _this The Bot object context diff --git a/src/bot/events/disconnected.js b/src/bot/events/disconnected.js index ee36befa..383c5e5c 100644 --- a/src/bot/events/disconnected.js +++ b/src/bot/events/disconnected.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 29.06.2023 22:35:03 + * Last Modified: 15.10.2023 17:04:54 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -30,13 +30,17 @@ Bot.prototype._attachSteamDisconnectedEvent = function() { logger("info", `${logger.colors.fgred}[${this.logPrefix}] Lost connection to Steam. Message: ${msg} | Check: https://steamstat.us`); + // Store disconnect timestamp & reason + this.lastDisconnect.timestamp = Date.now(); + this.lastDisconnect.reason = msg; + this.controller._statusUpdateEvent(this, Bot.EStatus.OFFLINE); // Set status of this account to offline // Don't relog if account is in skippedaccounts array or if relogAfterDisconnect is false if (!this.controller.info.skippedaccounts.includes(this.loginData.logOnOptions.accountName) && this.controller.info.relogAfterDisconnect) { - logger("info", `${logger.colors.fggreen}[${this.logPrefix}] Initiating a relog in ${this.controller.data.advancedconfig.relogTimeout / 1000} seconds.`); // Announce relog + logger("info", `${logger.colors.fggreen}[${this.logPrefix}] Initiating a login retry in ${this.controller.data.advancedconfig.loginRetryTimeout / 1000} seconds.`); // Announce relog - setTimeout(() => this.controller.login(), this.controller.data.advancedconfig.relogTimeout); // Relog in relogTimeout ms + setTimeout(() => this.controller.login(), this.controller.data.advancedconfig.loginRetryTimeout); // Relog in loginRetryTimeout ms } else { logger("info", `[${this.logPrefix}] I won't queue myself for a relog because this account is either already being relogged, was skipped or this is an intended logOff.`); } diff --git a/src/bot/events/error.js b/src/bot/events/error.js index 514fcf47..3246a063 100644 --- a/src/bot/events/error.js +++ b/src/bot/events/error.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 29.06.2023 22:35:03 + * Last Modified: 20.10.2023 20:04:41 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -33,7 +33,7 @@ Bot.prototype._attachSteamErrorEvent = function() { logger("", "", true); logger("warn", `${logger.colors.fgred}[${this.logPrefix}] Lost connection to Steam! Reason: LogonSessionReplaced. I won't try to relog this account because someone else is using it now.`, false, false, null, true); // Force print this message now - // Abort or skip account + // Abort or skip account. No need to attach handleRelog() here if (this.index == 0) { logger("error", `${logger.colors.fgred}Failed account is bot0! Aborting...`, true); return this.controller.stop(); @@ -51,11 +51,15 @@ Bot.prototype._attachSteamErrorEvent = function() { logger("info", `${logger.colors.fgred}[${this.logPrefix}] Lost connection to Steam. Reason: ${err}`); this.controller._statusUpdateEvent(this, Bot.EStatus.OFFLINE); // Set status of this account to offline + // Store disconnect timestamp & reason + this.lastDisconnect.timestamp = Date.now(); + this.lastDisconnect.reason = err; + // Check if this is an intended logoff if (this.controller.info.relogAfterDisconnect && !this.controller.info.skippedaccounts.includes(this.loginData.logOnOptions.accountName)) { - logger("info", `${logger.colors.fggreen}[${this.logPrefix}] Initiating a relog in ${this.controller.data.advancedconfig.relogTimeout / 1000} seconds.`); // Announce relog + logger("info", `${logger.colors.fggreen}[${this.logPrefix}] Initiating a login retry in ${this.controller.data.advancedconfig.loginRetryTimeout / 1000} seconds.`); // Announce relog - setTimeout(() => this.controller.login(), this.controller.data.advancedconfig.relogTimeout); // Relog after waiting relogTimeout ms + setTimeout(() => this.controller.login(), this.controller.data.advancedconfig.loginRetryTimeout); // Relog after waiting loginRetryTimeout ms } else { logger("info", `[${this.logPrefix}] I won't queue myself for a relog because this account was skipped or this is an intended logOff.`); } @@ -66,23 +70,20 @@ Bot.prototype._attachSteamErrorEvent = function() { // Check if all logOnTries are used or if this is a fatal error if (this.loginData.logOnTries > this.controller.data.advancedconfig.maxLogOnRetries || blockedEnumsForRetries.includes(err.eresult)) { - logger("", "", true); - logger("error", `Couldn't log in bot${this.index} after ${this.loginData.logOnTries} attempt(s). ${err} (${err.eresult})`, true); - - // Add additional messages for specific errors to hopefully help the user diagnose the cause - if (this.loginData.proxy) logger("", ` Is your proxy ${this.proxyIndex} offline or maybe blocked by Steam?`, true); + logger("error", `Couldn't log in bot${this.index} after ${this.loginData.logOnTries} attempt(s). ${err} (${err.eresult})`); - // Abort execution if account is bot0 - if (this.index == 0) { + // Abort if bot0 failed on initial login or skip account for now + if (this.index == 0 && this.controller.info.readyAfter == 0) { logger("", "", true); logger("error", "Aborting because the first bot account always needs to be logged in!\nPlease correct what caused the error and try again.", true); return this.controller.stop(); - } else { // Skip account if not bot0 + } else { - logger("info", "Failed account is not bot0. Skipping account...", true); - this.controller._statusUpdateEvent(this, Bot.EStatus.SKIPPED); - this.controller.info.skippedaccounts.push(this.loginData.logOnOptions.accountName); + //logger("info", "Failed account is not bot0. Skipping account...", true); + //this.controller.info.skippedaccounts.push(this.loginData.logOnOptions.accountName); + this.controller._statusUpdateEvent(this, Bot.EStatus.ERROR); + this.handleRelog(); } } else { // Got retries left or it is a relog... diff --git a/src/bot/events/friendMessage.js b/src/bot/events/friendMessage.js index 569b5353..0c31f271 100644 --- a/src/bot/events/friendMessage.js +++ b/src/bot/events/friendMessage.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 10.07.2023 09:36:42 + * Last Modified: 19.10.2023 19:34:51 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -25,7 +25,7 @@ const Bot = require("../bot.js"); */ Bot.prototype._attachSteamFriendMessageEvent = function() { - this.user.chat.on("friendMessage", (msg) => { + this.user.chat.on("friendMessage", async (msg) => { let message = msg.message_no_bbcode; let steamID = msg.steamid_friend; @@ -36,7 +36,7 @@ Bot.prototype._attachSteamFriendMessageEvent = function() { if (this.friendMessageBlock.includes(steamID64)) return logger("debug", `[${this.logPrefix}] Ignoring friendMessage event from ${steamID64} as user is on friendMessageBlock list.`); // Check if this event should be handled or if user is blocked - let isBlocked = this.checkMsgBlock(steamID64, message); + let isBlocked = await this.checkMsgBlock(steamID64, message); if (isBlocked) return; // Stop right here if user is blocked, on cooldown or not a friend @@ -48,11 +48,11 @@ Bot.prototype._attachSteamFriendMessageEvent = function() { // Sort out any chat messages not sent to the main bot if (this.index !== 0) { switch(message.toLowerCase()) { - case `${resInfo.cmdprefix}about`: // Please don't change this message as it gives credit to me; the person who put really much of his free time into this project. The bot will still refer to you - the operator of this instance. + case `${resInfo.cmdprefix}about`: // Please don't change this message as it gives credit to me; the person who put really much of their free time into this project. The bot will still refer to you - the operator of this instance. this.sendChatMessage(this, resInfo, this.controller.data.datafile.aboutstr); break; default: - if (message.startsWith(resInfo.cmdprefix)) this.sendChatMessage(this, resInfo, `${this.controller.data.lang.childbotmessage.replace(/cmdprefix/g, resInfo.cmdprefix)}\nhttps://steamcommunity.com/profiles/${new SteamID(String(this.controller.main.user.steamID)).getSteamID64()}`); + if (message.startsWith(resInfo.cmdprefix)) this.sendChatMessage(this, resInfo, `${await this.controller.data.getLang("childbotmessage", { "cmdprefix": resInfo.cmdprefix }, steamID64)}\nhttps://steamcommunity.com/profiles/${new SteamID(String(this.controller.main.user.steamID)).getSteamID64()}`); else logger("debug", `[${this.logPrefix}] Chat message is not a command, ignoring message.`); } @@ -69,7 +69,7 @@ Bot.prototype._attachSteamFriendMessageEvent = function() { if (!doc) { // Add user to database if he/she is missing for some reason let lastcommentobj = { id: new SteamID(String(steamID)).getSteamID64(), - time: Date.now() - (this.data.config.commentcooldown * 60000) // Subtract commentcooldown so that the user is able to use the command instantly + time: Date.now() - (this.data.config.requestCooldown * 60000) // Subtract requestCooldown so that the user is able to use the command instantly }; this.controller.data.lastCommentDB.insert(lastcommentobj, (err) => { if (err) logger("error", "Error inserting new user into lastcomment.db database! Error: " + err); }); @@ -90,9 +90,9 @@ Bot.prototype._attachSteamFriendMessageEvent = function() { let cont = message.slice(1).split(" "); // Remove prefix and split let args = cont.slice(1); // Remove cmd name to only get arguments - let success = this.controller.commandHandler.runCommand(cont[0].toLowerCase(), args, this.sendChatMessage, this, resInfo); + let success = await this.controller.commandHandler.runCommand(cont[0].toLowerCase(), args, this.sendChatMessage, this, resInfo); // Don't listen to your IDE, this *await is necessary* - if (!success) this.sendChatMessage(this, resInfo, this.controller.data.lang.commandnotfound.replace(/cmdprefix/g, resInfo.cmdprefix)); // Send cmd not found msg if runCommand() returned false + if (!success) this.sendChatMessage(this, resInfo, await this.controller.data.getLang("commandnotfound", { "cmdprefix": resInfo.cmdprefix }, steamID64)); // Send cmd not found msg if runCommand() returned false }); }; \ No newline at end of file diff --git a/src/bot/events/loggedOn.js b/src/bot/events/loggedOn.js index 4fdc0d2f..2d000450 100644 --- a/src/bot/events/loggedOn.js +++ b/src/bot/events/loggedOn.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 29.06.2023 22:35:03 + * Last Modified: 21.10.2023 12:28:10 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -33,7 +33,7 @@ Bot.prototype._attachSteamLoggedOnEvent = function() { // Increase progress bar if one is active - if (logger.getProgressBar()) logger.increaseProgressBar((100 / Object.keys(this.data.logininfo).length) / 3); + if (logger.getProgressBar()) logger.increaseProgressBar((100 / this.data.logininfo.length) / 3); }); diff --git a/src/bot/events/relationship.js b/src/bot/events/relationship.js index 8f98e8aa..db9ee8cb 100644 --- a/src/bot/events/relationship.js +++ b/src/bot/events/relationship.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 10.07.2023 09:33:09 + * Last Modified: 19.10.2023 19:00:06 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -25,7 +25,7 @@ const Bot = require("../bot.js"); */ Bot.prototype._attachSteamFriendRelationshipEvent = function() { - this.user.on("friendRelationship", (steamID, relationship) => { + this.user.on("friendRelationship", async (steamID, relationship) => { if (relationship == 2) { let steamID64 = new SteamID(String(steamID)).getSteamID64(); @@ -39,13 +39,13 @@ Bot.prototype._attachSteamFriendRelationshipEvent = function() { // Log message and send welcome message logger("info", `[${this.logPrefix}] Added User: ` + steamID64); - if (this.index == 0) this.sendChatMessage(this, { userID: steamID64 }, this.controller.data.lang.useradded.replace(/cmdprefix/g, "!")); + if (this.index == 0) this.sendChatMessage(this, { userID: steamID64 }, await this.controller.data.getLang("useradded", { "cmdprefix": "!" }, steamID64)); // Add user to lastcomment database let lastcommentobj = { id: steamID64, - time: Date.now() - (this.controller.data.config.commentcooldown * 60000) // Subtract commentcooldown so that the user is able to use the command instantly + time: Date.now() - (this.controller.data.config.requestCooldown * 60000) // Subtract requestCooldown so that the user is able to use the command instantly }; this.controller.data.lastCommentDB.remove({ id: steamID64 }, {}, (err) => { if (err) logger("error", "Error removing duplicate steamid from lastcomment.db on friendRelationship! Error: " + err); }); // Remove any old entries diff --git a/src/bot/events/webSession.js b/src/bot/events/webSession.js index a74bd487..d4e1f859 100644 --- a/src/bot/events/webSession.js +++ b/src/bot/events/webSession.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 10.07.2023 09:33:16 + * Last Modified: 21.10.2023 13:13:22 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -28,7 +28,7 @@ Bot.prototype._attachSteamWebSessionEvent = function() { this.user.on("webSession", (sessionID, cookies) => { // Get websession (log in to chat) // Increase progress bar if one is active - if (logger.getProgressBar()) logger.increaseProgressBar((100 / Object.keys(this.data.logininfo).length) / 3); + if (logger.getProgressBar()) logger.increaseProgressBar((100 / this.data.logininfo.length) / 3); // Set cookies (otherwise the bot is unable to comment) @@ -36,6 +36,8 @@ Bot.prototype._attachSteamWebSessionEvent = function() { this.controller._statusUpdateEvent(this, Bot.EStatus.ONLINE); // Set status of this account to online + this.loginData.relogTries = 0; // Reset relogTries to indicate that this proxy is working should one of the next logOn retries fail + if (!this.controller.info.readyAfter) logger("info", `[${this.logPrefix}] Got websession and set cookies. Accepting offline friend & group invites...`, false, true, logger.animation("loading")); // Only print message with animation if the bot was not fully started yet else logger("info", `[${this.logPrefix}] Got websession and set cookies. Accepting offline friend & group invites...`, false, true); @@ -63,8 +65,8 @@ Bot.prototype._attachSteamWebSessionEvent = function() { // Log message and send welcome message. Delay msg to avoid AccessDenied and RateLimitExceeded errors logger("info", `[${this.logPrefix}] Added user while I was offline! User: ` + thisfriend); - setTimeout(() => { - if (this.index == 0) this.sendChatMessage(this, { userID: String(thisfriend) }, this.controller.data.lang.useradded.replace(/cmdprefix/g, "!")); + setTimeout(async () => { + if (this.index == 0) this.sendChatMessage(this, { userID: String(thisfriend) }, await this.controller.data.getLang("useradded", { "cmdprefix": "!" }, String(thisfriend))); else logger("debug", "Not sending useradded message because this isn't the main user..."); }, 1000 * processedFriendRequests); @@ -72,7 +74,7 @@ Bot.prototype._attachSteamWebSessionEvent = function() { // Add user to lastcomment database let lastcommentobj = { id: thisfriend, - time: Date.now() - (this.controller.data.config.commentcooldown * 60000) // Subtract commentcooldown so that the user is able to use the command instantly + time: Date.now() - (this.controller.data.config.requestCooldown * 60000) // Subtract requestCooldown so that the user is able to use the command instantly }; this.controller.data.lastCommentDB.remove({ id: thisfriend }, {}, (err) => { if (err) logger("error", "Error removing duplicate steamid from lastcomment.db on offline friend accept! Error: " + err); }); // Remove any old entries @@ -117,34 +119,40 @@ Bot.prototype._attachSteamWebSessionEvent = function() { } - /* ------------ Join botsgroup: ------------ */ - logger("debug", `[${this.logPrefix}] Checking if bot account is in botsgroup...`, false, true, logger.animation("loading")); + // Run the following only on initial login + if (this.lastDisconnect.timestamp == 0) { - if (this.controller.data.cachefile.botsgroupid && (!this.user.myGroups[this.controller.data.cachefile.botsgroupid] || this.user.myGroups[this.controller.data.cachefile.botsgroupid] != 3)) { // If botsgroupid is defined, not in myGroups or in it but not enum 3 - this.community.joinGroup(new SteamID(this.controller.data.cachefile.botsgroupid)); + /* ------------ Join botsgroup: ------------ */ + logger("debug", `[${this.logPrefix}] Checking if bot account is in botsgroup...`); - logger("info", `[${this.logPrefix}] Joined/Requested to join steam group that has been set as botsgroup.`); - } + if (this.controller.data.cachefile.botsgroupid && (!this.user.myGroups[this.controller.data.cachefile.botsgroupid] || this.user.myGroups[this.controller.data.cachefile.botsgroupid] != 3)) { // If botsgroupid is defined, not in myGroups or in it but not enum 3 + this.community.joinGroup(new SteamID(this.controller.data.cachefile.botsgroupid)); + logger("info", `[${this.logPrefix}] Joined/Requested to join steam group that has been set as botsgroup.`); + } - /* ------------ Set primary group: ------------ */ // TODO: Add further delays? https://github.com/3urobeat/steam-comment-service-bot/issues/165 - if (this.controller.data.advancedconfig.setPrimaryGroup && this.controller.data.cachefile.configgroup64id) { - logger("info", `[${this.logPrefix}] setPrimaryGroup is enabled and configgroup64id is set, setting ${this.controller.data.cachefile.configgroup64id} as primary group...`, false, true, logger.animation("loading")); - this.community.editProfile({ - primaryGroup: new SteamID(this.controller.data.cachefile.configgroup64id) - }, (err) => { - if (err) logger("err", `[${this.logPrefix}] Error setting primary group: ${err}`, true); - }); - } + /* ------------ Set primary group: ------------ */ // TODO: Add further delays? https://github.com/3urobeat/steam-comment-service-bot/issues/165 + if (this.controller.data.advancedconfig.setPrimaryGroup && this.controller.data.cachefile.configgroup64id) { + logger("debug", `[${this.logPrefix}] setPrimaryGroup is enabled and configgroup64id is set, setting '${this.controller.data.cachefile.configgroup64id}' as primary group...`); + + this.community.editProfile({ + primaryGroup: new SteamID(this.controller.data.cachefile.configgroup64id) + }, (err) => { + if (err) logger("err", `[${this.logPrefix}] Error setting primary group: ${err}`, false, false, null, true); + else logger("info", `[${this.logPrefix}] Successfully set '${this.controller.data.cachefile.configgroup64id}' as primary group...`, false, true, logger.animation("loading")); + }); + } - /* ------------ Check for missing game licenses and start playing: ------------ */ - this.handleMissingGameLicenses(); + /* ------------ Check for missing game licenses and start playing: ------------ */ + this.handleMissingGameLicenses(); + + } // Increase progress bar if one is active - if (logger.getProgressBar()) logger.increaseProgressBar((100 / Object.keys(this.data.logininfo).length) / 3); + if (logger.getProgressBar()) logger.increaseProgressBar((100 / this.data.logininfo.length) / 3); }); diff --git a/src/bot/helpers/checkMsgBlock.js b/src/bot/helpers/checkMsgBlock.js index eefb2eef..3aa6cbf9 100644 --- a/src/bot/helpers/checkMsgBlock.js +++ b/src/bot/helpers/checkMsgBlock.js @@ -4,7 +4,7 @@ * Created Date: 20.03.2023 12:46:47 * Author: 3urobeat * - * Last Modified: 10.07.2023 13:05:31 + * Last Modified: 10.09.2023 00:04:33 * Modified By: 3urobeat * * Copyright (c) 2023 3urobeat @@ -26,7 +26,7 @@ const lastmessage = {}; // Tracks the last cmd usage of a normal command to appl * @param {string} message The message string provided by steam-user friendMessage event * @returns {boolean} `true` if friendMessage event shouldn't be handled, `false` if user is allowed to be handled */ -Bot.prototype.checkMsgBlock = function(steamID64, message) { +Bot.prototype.checkMsgBlock = async function(steamID64, message) { // Check if user is blocked and ignore message if (this.user.myFriends[steamID64] == 1 || this.user.myFriends[steamID64] == 6) { @@ -40,7 +40,7 @@ Bot.prototype.checkMsgBlock = function(steamID64, message) { if (lastmessage[steamID64] && lastmessage[steamID64][0] + this.controller.data.advancedconfig.commandCooldown > Date.now() && lastmessage[steamID64][1] > 5) return true; // Just don't respond if (lastmessage[steamID64] && lastmessage[steamID64][0] + this.controller.data.advancedconfig.commandCooldown > Date.now() && lastmessage[steamID64][1] > 4) { // Inform the user about the cooldown - this.sendChatMessage({ userID: steamID64, prefix: "/me" }, this.controller.data.lang.userspamblock); + this.sendChatMessage({ userID: steamID64, prefix: "/me" }, await this.controller.data.getLang("userspamblock", null, steamID64)); logger("info", `${steamID64} has been blocked for 90 seconds for spamming.`); lastmessage[steamID64][0] += 90000; @@ -54,7 +54,7 @@ Bot.prototype.checkMsgBlock = function(steamID64, message) { // Deny non-friends the use of any command if (this.user.myFriends[steamID64] != 3) { - this.sendChatMessage({ userID: steamID64, prefix: "/me" }, this.controller.data.lang.usernotfriend); + this.sendChatMessage({ userID: steamID64, prefix: "/me" }, await this.controller.data.getLang("usernotfriend", null, steamID64)); return true; } diff --git a/src/bot/helpers/handleLoginTimeout.js b/src/bot/helpers/handleLoginTimeout.js index 626fb72b..043390bd 100644 --- a/src/bot/helpers/handleLoginTimeout.js +++ b/src/bot/helpers/handleLoginTimeout.js @@ -4,7 +4,7 @@ * Created Date: 03.11.2022 12:27:46 * Author: 3urobeat * - * Last Modified: 24.07.2023 19:37:03 + * Last Modified: 14.10.2023 14:44:01 * Modified By: 3urobeat * * Copyright (c) 2022 3urobeat @@ -42,29 +42,26 @@ Bot.prototype.handleLoginTimeout = function() { // Check if all logOnRetries are used up and skip account if (this.loginData.logOnTries > this.data.advancedconfig.maxLogOnRetries) { - logger("", "", true); - logger("error", `Couldn't log in bot${this.index} after ${this.loginData.logOnTries} attempt(s). Error: Login attempt timed out and all available logOnRetries were used.`, true); + logger("error", `Couldn't log in bot${this.index} after ${this.loginData.logOnTries} attempt(s). Error: Login attempt timed out and all available logOnRetries were used.`); - // Add additional messages for specific errors to hopefully help the user diagnose the cause - if (this.loginData.proxy != null) logger("", ` Is your proxy ${this.loginData.proxyIndex} offline or maybe blocked by Steam?`, true); - - // Abort execution if account is bot0 - if (this.index == 0) { + // Abort if bot0 failed on initial login or skip account + if (this.index == 0 && this.controller.info.readyAfter == 0) { logger("", "", true); logger("error", "Aborting because the first bot account always needs to be logged in!\nPlease wait a moment and start the bot again.", true); return this.controller.stop(); } else { // Skip account if not bot0 - logger("info", "Failed account is not bot0. Skipping account...", true); - this.controller._statusUpdateEvent(this, Bot.EStatus.SKIPPED); - this.controller.info.skippedaccounts.push(this.loginData.logOnOptions.accountName); + //logger("info", "Failed account is not bot0. Skipping account...", true); + //this.controller.info.skippedaccounts.push(this.loginData.logOnOptions.accountName); + this.controller._statusUpdateEvent(this, Bot.EStatus.ERROR); + this.handleRelog(); } } else { // Force progress if account is stuck - logger("warn", `Detected timed out login attempt for bot${this.index}! Force progressing login attempt to avoid soft-locking the bot...`, true); + logger("warn", `Detected timed out login attempt for bot${this.index}! Force progressing login attempt to avoid soft-locking the bot...`, false, false, null, true); this.user.logOff(); // Call logOff() just to be sure if (this.sessionHandler.session) this.sessionHandler.session.cancelLoginAttempt(); // TODO: This might cause an error as idk if we are polling. Maybe use the timeout event of steam-session diff --git a/src/bot/helpers/handleMissingGameLicenses.js b/src/bot/helpers/handleMissingGameLicenses.js index 495338e8..3f73deef 100644 --- a/src/bot/helpers/handleMissingGameLicenses.js +++ b/src/bot/helpers/handleMissingGameLicenses.js @@ -4,7 +4,7 @@ * Created Date: 29.06.2023 21:31:53 * Author: 3urobeat * - * Last Modified: 29.06.2023 22:35:03 + * Last Modified: 14.10.2023 10:35:38 * Modified By: 3urobeat * * Copyright (c) 2023 3urobeat @@ -22,19 +22,31 @@ const Bot = require("../bot.js"); * Handles checking for missing game licenses, requests them and then starts playing */ Bot.prototype.handleMissingGameLicenses = function() { - - let startPlaying = () => { if (this.index == 0) this.user.gamesPlayed(this.controller.data.config.playinggames); else this.user.gamesPlayed(this.controller.data.config.childaccplayinggames); }; let data = this.controller.data; + // Check if user provided games specifically for this account. We only need to check this for child accounts + let configChildGames = data.config.childaccplayinggames; + + if (typeof configChildGames[0] == "object") { + if (Object.keys(configChildGames[0]).includes(this.loginData.logOnOptions.accountName)) configChildGames = configChildGames[0][this.loginData.logOnOptions.accountName]; // Get the specific settings for this account if included + else configChildGames = configChildGames.slice(1); // ...otherwise remove object containing acc specific settings to use the generic ones + + logger("debug", `[${this.logPrefix}] Bot handleMissingGameLicenses(): Setting includes specific games, filtered for this account: ${configChildGames.join(", ")}`); + } + + // Shorthander for starting to play + let startPlaying = () => { if (this.index == 0) this.user.gamesPlayed(this.controller.data.config.playinggames); else this.user.gamesPlayed(configChildGames); }; + + let options = { includePlayedFreeGames: true, - filterAppids: this.index == 0 ? data.config.playinggames.filter(e => !isNaN(e)) : data.config.childaccplayinggames.filter(e => !isNaN(e)), // We only need to check for these appIDs. Filter custom game string + filterAppids: this.index == 0 ? data.config.playinggames.filter(e => !isNaN(e)) : configChildGames.filter(e => !isNaN(e)), // We only need to check for these appIDs. Filter custom game string includeFreeSub: false }; // Only request owned apps if we are supposed to idle something if (options.filterAppids.length > 0) { - this.user.getUserOwnedApps(data.cachefile.botaccid[this.index], options, (err, res) => { + this.user.getUserOwnedApps(this.user.steamID, options, (err, res) => { if (err) { logger("error", `[${this.logPrefix}] Failed to get owned apps! Attempting to play set appIDs anyways...`); @@ -44,7 +56,7 @@ Bot.prototype.handleMissingGameLicenses = function() { } // Check if we are missing a license - let missingLicenses = this.data.config.playinggames.filter(e => !isNaN(e) && res.apps.filter(f => f.appid == e).length == 0); + let missingLicenses = options.filterAppids.filter(e => !isNaN(e) && res.apps.filter(f => f.appid == e).length == 0); // Redeem missing licenses or start playing if none are missing. Event will get triggered again on change. if (missingLicenses.length > 0) { diff --git a/src/bot/helpers/handleRelog.js b/src/bot/helpers/handleRelog.js new file mode 100644 index 00000000..76063182 --- /dev/null +++ b/src/bot/helpers/handleRelog.js @@ -0,0 +1,150 @@ +/* + * File: handleRelog.js + * Project: steam-comment-service-bot + * Created Date: 05.10.2023 16:14:46 + * Author: 3urobeat + * + * Last Modified: 15.10.2023 20:15:36 + * Modified By: 3urobeat + * + * Copyright (c) 2023 3urobeat + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. If not, see . + */ + + +const SteamUser = require("steam-user"); +const SteamCommunity = require("steamcommunity"); +const request = require("request"); +const Bot = require("../bot"); + + +/** + * Changes the proxy of this bot account and relogs it. + * @param {number} newProxyIndex Index of the new proxy inside the DataManager.proxies array. + */ +Bot.prototype.switchProxy = function(newProxyIndex) { + + if (!newProxyIndex) return new Error("newProxyIndex is undefined"); + + logger("info", `[${this.logPrefix}] Switching proxy from ${this.loginData.proxyIndex} to ${newProxyIndex}. The bot account will relog in a moment...`); + + // Update proxy usage + this.loginData.proxy = this.data.proxies[newProxyIndex].proxy; + this.loginData.proxyIndex = newProxyIndex; + + // Log off account. This will trigger a relog from the disconnected event + this.user.logOff(); + + // Recreate user and community object with new proxy + this.user = new SteamUser({ autoRelogin: false, renewRefreshTokens: true, httpProxy: this.loginData.proxy, protocol: SteamUser.EConnectionProtocol.WebSocket }); + this.community = new SteamCommunity({ request: request.defaults({ "proxy": this.loginData.proxy }) }); + + // Attach event listeners again. The old ones are taken care of by the Garbage Collector because the object gets destroyed + this._attachSteamDebugEvent(); + this._attachSteamDisconnectedEvent(); + this._attachSteamErrorEvent(); + this._attachSteamFriendMessageEvent(); + this._attachSteamLoggedOnEvent(); + this._attachSteamFriendRelationshipEvent(); + this._attachSteamGroupRelationshipEvent(); + this._attachSteamWebSessionEvent(); + +}; + + +/** + * Checks host internet connection, updates the status of all proxies checked >2.5 min ago and switches the proxy of this bot account if necessary. + * @returns {Promise.} Resolves with a boolean indicating whether the proxy was switched when done. A relog is triggered when the proxy was switched. + */ +Bot.prototype.checkAndSwitchMyProxy = async function() { + + // Attempt to ping github.com (basically any non steamcommunity url) without a proxy to determine if the internet connection is not working + let hostConnectionRes = await this.controller.misc.checkConnection("https://github.com/3urobeat/steam-comment-service-bot", true) + .catch((err) => { + if (this.index == 0) logger("info", `[Main] Your internet connection seems to be down. ${err.statusMessage}`); // Only log message for main acc to reduce clutter + }); + + if (!hostConnectionRes || !hostConnectionRes.statusCode) return false; // Return false if catch from above was triggered + + + // Return false if connection is up but Steam cannot be reached as the proxy is not at fault + if (!(hostConnectionRes.statusCode >= 200 && hostConnectionRes.statusCode < 300)) { + if (this.index == 0) logger("info", "[Main] Steam is unreachable but your internet connection seems to work."); // Only log message for main acc to reduce clutter + return false; + } + + if (!this.loginData.proxy) return false; // Ignore anything below if this account does not use a proxy + + logger("info", `[${this.logPrefix}] Steam appears to be reachable without a proxy. Checking if Steam is reachable using proxy ${this.loginData.proxyIndex}...`, false, false, logger.animation("loading")); + + + // Refresh online status of all proxies if not done in the last 2.5 minutes to check if this one is down and potentially switch to a working one + await this.data.checkAllProxies(150000); + + + // Check if our proxy is down + let thisProxy = this.data.proxies.find((e) => e.proxyIndex == this.loginData.proxyIndex); + + if (!thisProxy.isOnline) { + let activeProxies = this.controller.getBotsPerProxy(true); // Get all online proxies and their associated bot accounts + + // Check if no available proxy was found (exclude host) and return false + if (activeProxies.length == 0) { + logger("warn", `[${this.logPrefix}] Failed to ping Steam using proxy ${this.loginData.proxyIndex} but no other available proxy was found! Continuing to try with this proxy...`); + return false; + } + + + // Find proxy with least amount of associated bots + let leastUsedProxy = activeProxies.reduce((a, b) => a.bots.length < b.bots.length ? a : b); + + logger("warn", `[${this.logPrefix}] Failed to ping Steam using proxy ${this.loginData.proxyIndex}! Switched to proxy ${leastUsedProxy.proxyIndex} which currently has the least amount of usage and appears to be online.`); + + + // Switch proxy and relog, no need for handleRelog() to do something + this.switchProxy(leastUsedProxy.proxyIndex); + this.status = Bot.EStatus.OFFLINE; + this.controller.login(); + return true; + + } else { + + logger("info", `[${this.logPrefix}] Successfully pinged Steam using proxy ${this.loginData.proxyIndex}. I'll keep using this proxy.`); + return false; + } + +}; + + +/** + * Attempts to get this account, after failing all logOnRetries, back online after some time. Does not apply to initial logins. + */ +Bot.prototype.handleRelog = async function() { + + this.loginData.relogTries++; + + // Check if proxy might be offline + let proxySwitched = await this.checkAndSwitchMyProxy(); + + if (proxySwitched) return; // Stop execution if proxy was switched and bot is getting relogged + + // Ignore if login timeout handler is disabled in advancedconfig + if (this.data.advancedconfig.relogTimeout == 0) return logger("debug", `Bot handleRelog(): Ignoring timeout attach request for bot${this.index} because relogTimeout is disabled in advancedconfig!`); + else logger("info", `[${this.logPrefix}] Attempting to recover lost connection in ${this.data.advancedconfig.relogTimeout / 60000} minutes (attempt ${this.loginData.relogTries})...`, false, false, logger.animation("waiting")); + + // Attempt to relog account after relogTimeout ms + setTimeout(() => { + + // Abort if account is online again for some reason + if (this.status == Bot.EStatus.ONLINE) return logger("debug", `Bot handleRelog(): Timeout elapsed but bot${this.index} is not offline anymore. Ignoring...`); + + // Update status to offline and call login again + this.status = Bot.EStatus.OFFLINE; + this.controller.login(); + + }, this.controller.data.advancedconfig.relogTimeout); + +}; \ No newline at end of file diff --git a/src/commands/commandHandler.js b/src/commands/commandHandler.js index 3d4c1a42..fe5da09a 100644 --- a/src/commands/commandHandler.js +++ b/src/commands/commandHandler.js @@ -4,7 +4,7 @@ * Created Date: 01.04.2023 21:54:21 * Author: 3urobeat * - * Last Modified: 10.07.2023 21:25:57 + * Last Modified: 10.09.2023 11:51:58 * Modified By: 3urobeat * * Copyright (c) 2023 3urobeat @@ -19,6 +19,7 @@ const fs = require("fs"); const Controller = require("../controller/controller.js"); // eslint-disable-line + /** * @typedef Command Documentation of the Command structure * @type {object} @@ -194,7 +195,7 @@ CommandHandler.prototype.unregisterCommand = function(commandName) { * @param {resInfo} resInfo Object containing additional information * @returns {boolean} `true` if command was found, `false` if not */ -CommandHandler.prototype.runCommand = function(name, args, respondModule, context, resInfo) { +CommandHandler.prototype.runCommand = async function(name, args, respondModule, context, resInfo) { // Iterate through all command objects in commands array and check if name is included in names array of each command. let thisCmd = this.commands.find(e => e.names.includes(name)); @@ -221,7 +222,7 @@ CommandHandler.prototype.runCommand = function(name, args, respondModule, contex // If command is ownersOnly, check if user is included in owners array. If not, send error msg and return true to avoid caller sending a not found msg if (thisCmd.ownersOnly && !owners.includes(resInfo.userID)) { // If no userID was provided this check will also trigger - respondModule(context, resInfo, this.data.lang.commandowneronly); + respondModule(context, resInfo, await this.data.getLang("commandowneronly", null, resInfo.userID)); return true; } diff --git a/src/commands/core/block.js b/src/commands/core/block.js index 2d090cb2..cdb02179 100644 --- a/src/commands/core/block.js +++ b/src/commands/core/block.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 10.07.2023 12:58:49 + * Last Modified: 10.09.2023 00:18:49 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -42,26 +42,26 @@ module.exports.block = { * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). */ - run: (commandHandler, args, respondModule, context, resInfo) => { + run: async (commandHandler, args, respondModule, context, resInfo) => { let respond = ((txt) => respondModule(context, resInfo, txt)); // Shorten each call - if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.botnotready); // Check if bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained + if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("botnotready", null, resInfo.userID)); // Check if bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained - if (!args[0]) return respond(commandHandler.data.lang.invalidprofileid); + if (!args[0]) return respond(await commandHandler.data.getLang("invalidprofileid", null, resInfo.userID)); // Get the correct ownerid array for this request let owners = commandHandler.data.cachefile.ownerid; if (resInfo.ownerIDs && resInfo.ownerIDs.length > 0) owners = resInfo.ownerIDs; - commandHandler.controller.handleSteamIdResolving(args[0], "profile", (err, res) => { - if (err) return respond(commandHandler.data.lang.invalidprofileid + "\n\nError: " + err); - if (owners.includes(res)) return respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.idisownererror); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained + commandHandler.controller.handleSteamIdResolving(args[0], "profile", async (err, res) => { + if (err) return respond((await commandHandler.data.getLang("invalidprofileid", null, resInfo.userID)) + "\n\nError: " + err); + if (owners.includes(res)) return respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("idisownererror", null, resInfo.userID)); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained commandHandler.controller.getBots().forEach((e, i) => { e.user.blockUser(new SteamID(res), (err) => { if (err) logger("error", `[Bot ${i}] Error blocking user ${res}: ${err}`); }); }); - respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.blockcmdsuccess.replace("profileid", res)); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained + respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("blockcmdsuccess", { "profileid": res }, resInfo.userID)); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained logger("info", `Blocked ${res} with all bot accounts.`); }); } @@ -90,21 +90,21 @@ module.exports.unblock = { * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). */ - run: (commandHandler, args, respondModule, context, resInfo) => { + run: async (commandHandler, args, respondModule, context, resInfo) => { let respond = ((txt) => respondModule(context, resInfo, txt)); // Shorten each call - if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.botnotready); // Check if bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained + if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("botnotready", null, resInfo.userID)); // Check if bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained - if (!args[0]) return respond(commandHandler.data.lang.invalidprofileid); + if (!args[0]) return respond(await commandHandler.data.getLang("invalidprofileid", null, resInfo.userID)); - commandHandler.controller.handleSteamIdResolving(args[0], "profile", (err, res) => { - if (err) return respond(commandHandler.data.lang.invalidprofileid + "\n\nError: " + err); + commandHandler.controller.handleSteamIdResolving(args[0], "profile", async (err, res) => { + if (err) return respond((await commandHandler.data.getLang("invalidprofileid", null, resInfo.userID)) + "\n\nError: " + err); commandHandler.controller.getBots().forEach((e, i) => { e.user.unblockUser(new SteamID(res), (err) => { if (err) logger("error", `[Bot ${i}] Error unblocking user ${res}: ${err}`); }); }); - respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.unblockcmdsuccess.replace("profileid", res)); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained + respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("unblockcmdsuccess", { "profileid": res }, resInfo.userID)); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained logger("info", `Unblocked ${res} with all bot accounts.`); }); } diff --git a/src/commands/core/comment.js b/src/commands/core/comment.js index 7ba9ebb4..593338fe 100644 --- a/src/commands/core/comment.js +++ b/src/commands/core/comment.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 24.07.2023 19:41:59 + * Last Modified: 19.10.2023 19:00:06 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -26,7 +26,7 @@ const { logCommentError, handleIterationSkip } = require("../helpers/handleComme module.exports.comment = { names: ["comment", "gcomment", "groupcomment"], - description: "Request comments from all available bot accounts for a profile, group or sharedfile", + description: "Request comments from all available bot accounts for a profile, group, sharedfile or discussion", args: [ { name: "amount", @@ -37,7 +37,7 @@ module.exports.comment = { }, { name: "ID", - description: "The link, steamID64 or vanity of the profile, group or sharedfile to comment on", + description: "The link, steamID64 or vanity of the profile, group, sharedfile or discussion to comment on", type: "string", isOptional: true, ownersOnly: true @@ -67,33 +67,33 @@ module.exports.comment = { let owners = commandHandler.data.cachefile.ownerid; if (resInfo.ownerIDs && resInfo.ownerIDs.length > 0) owners = resInfo.ownerIDs; - let requesterSteamID64 = resInfo.userID; - let receiverSteamID64 = requesterSteamID64; - let ownercheck = owners.includes(requesterSteamID64); + let requesterID = resInfo.userID; + let receiverSteamID64 = requesterID; + let ownercheck = owners.includes(requesterID); /* --------- Various checks --------- */ if (!resInfo.userID) { - respond(commandHandler.data.lang.nouserid); // Reject usage of command without an userID to avoid cooldown bypass + respond(await commandHandler.data.getLang("nouserid")); // Reject usage of command without an userID to avoid cooldown bypass return logger("err", "The comment command was called without resInfo.userID! Blocking the command as I'm unable to apply cooldowns, which is required for this command!"); } - if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.botnotready); // Bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained - if (commandHandler.controller.info.activeLogin) return respond(commandHandler.data.lang.activerelog); // Bot is waiting for relog - if (commandHandler.data.config.maxComments == 0 && !ownercheck) return respond(commandHandler.data.lang.commandowneronly); // Comment command is restricted to owners only + if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("botnotready", null, requesterID)); // Bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained + if (commandHandler.controller.info.activeLogin) return respond(await commandHandler.data.getLang("activerelog", null, requesterID)); // Bot is waiting for relog + if (commandHandler.data.config.maxRequests == 0 && !ownercheck) return respond(await commandHandler.data.getLang("commandowneronly", null, requesterID)); // Command is restricted to owners only // Check for no id param as default behavior is unavailable when calling from outside the Steam Chat - if (!resInfo.fromSteamChat && !args[1]) return respond(commandHandler.data.lang.noidparam); + if (!resInfo.fromSteamChat && !args[1]) return respond(await commandHandler.data.getLang("noidparam", null, requesterID)); /* --------- Calculate maxRequestAmount and get arguments from comment request --------- */ - let { maxRequestAmount, numberOfComments, profileID, idType, quotesArr } = await getCommentArgs(commandHandler, args, requesterSteamID64, resInfo, respond); + let { maxRequestAmount, numberOfComments, profileID, idType, quotesArr } = await getCommentArgs(commandHandler, args, requesterID, resInfo, respond); if (!maxRequestAmount && !numberOfComments && !quotesArr) return; // Looks like the helper aborted the request // Update receiverSteamID64 if profileID was returned - if (profileID && profileID != requesterSteamID64) { - logger("debug", "Custom profileID provided that is != requesterSteamID64, modifying steamID object..."); + if (profileID && profileID != requesterID) { + logger("debug", "Custom profileID provided that is != requesterID, modifying steamID object..."); receiverSteamID64 = profileID; // Update receiverSteamID64 } @@ -102,13 +102,13 @@ module.exports.comment = { // Check if user is already receiving comments right now let activeReqEntry = commandHandler.controller.activeRequests[receiverSteamID64]; - if (activeReqEntry && activeReqEntry.status == "active") return respond(commandHandler.data.lang.idalreadyreceiving); + if (activeReqEntry && activeReqEntry.status == "active") return respond(await commandHandler.data.getLang("idalreadyreceiving", null, requesterID)); // Check if user has cooldown - let { until, untilStr } = await commandHandler.data.getUserCooldown(requesterSteamID64); + let { until, untilStr } = await commandHandler.data.getUserCooldown(requesterID); - if (until > Date.now()) return respond(commandHandler.data.lang.idoncooldown.replace("remainingcooldown", untilStr)); + if (until > Date.now()) return respond(await commandHandler.data.getLang("idoncooldown", { "remainingcooldown": untilStr }, requesterID)); // Get all currently available bot accounts. Block limited accounts from being eligible from commenting in groups @@ -116,14 +116,14 @@ module.exports.comment = { let { accsNeeded, availableAccounts, accsToAdd, whenAvailableStr } = getAvailableBotsForCommenting(commandHandler, numberOfComments, allowLimitedAccounts, idType, receiverSteamID64); if (availableAccounts.length == 0 && !whenAvailableStr) { // Check if this bot has no suitable accounts for this request and there won't be any available at any point - if (!allowLimitedAccounts) respond(commandHandler.data.lang.commentnounlimitedaccs.replace(/cmdprefix/g, resInfo.cmdprefix)); // Send less generic message for requests which require unlimited accounts - else respond(commandHandler.data.lang.commentnoaccounts.replace(/cmdprefix/g, resInfo.cmdprefix)); + if (!allowLimitedAccounts) respond(await commandHandler.data.getLang("commentnounlimitedaccs", { "cmdprefix": resInfo.cmdprefix }, requesterID)); // Send less generic message for requests which require unlimited accounts + else respond(await commandHandler.data.getLang("commentnoaccounts", { "cmdprefix": resInfo.cmdprefix }, requesterID)); return; } if (availableAccounts.length - accsToAdd.length < accsNeeded && !whenAvailableStr) { // Check if user needs to add accounts first. Make sure the lack of accounts is caused by accsToAdd, not cooldown - let addStr = commandHandler.data.lang.commentaddbotaccounts; + let addStr = await commandHandler.data.getLang("commentaddbotaccounts", null, requesterID); accsToAdd.forEach(e => addStr += `\n' steamcommunity.com/profiles/${commandHandler.data.cachefile.botaccid[commandHandler.controller.getBots(null, true)[e].index]} '`); logger("info", `Found enough available accounts but user needs to add ${accsToAdd.length} limited accounts first before I'm able to comment.`); @@ -133,8 +133,8 @@ module.exports.comment = { } if (availableAccounts.length < accsNeeded) { // Check if not enough available accounts were found because of cooldown - if (availableAccounts.length > 0) respond(commandHandler.data.lang.commentnotenoughavailableaccs.replace("waittime", whenAvailableStr).replace("availablenow", availableAccounts.length)); // Using allAccounts.length works for the "spread requests on as many accounts as possible" method - else respond(commandHandler.data.lang.commentzeroavailableaccs.replace("waittime", whenAvailableStr)); + if (availableAccounts.length > 0) respond(await commandHandler.data.getLang("commentnotenoughavailableaccs", { "waittime": whenAvailableStr, "availablenow": availableAccounts.length }, requesterID)); // Using allAccounts.length works for the "spread requests on as many accounts as possible" method + else respond(await commandHandler.data.getLang("commentzeroavailableaccs", { "waittime": whenAvailableStr }, requesterID)); logger("info", `Found only ${availableAccounts.length} available account(s) but ${accsNeeded} account(s) are needed to send ${numberOfComments} comments.`); return; @@ -147,12 +147,12 @@ module.exports.comment = { type: idType + "Comment", // Add "Comment" to the end of type to differentiate a comment process from other requests amount: numberOfComments, quotesArr: quotesArr, - requestedby: requesterSteamID64, + requestedby: requesterID, accounts: availableAccounts, thisIteration: -1, // Set to -1 so that first iteration will increase it to 0 retryAttempt: 0, amountBeforeRetry: 0, // Saves the amount of requested comments before the most recent retry attempt was made to send a correct finished message - until: Date.now() + ((numberOfComments - 1) * commandHandler.data.config.commentdelay), // Calculate estimated wait time (first comment is instant -> remove 1 from numberOfComments) + until: Date.now() + ((numberOfComments - 1) * commandHandler.data.config.requestDelay), // Calculate estimated wait time (first comment is instant -> remove 1 from numberOfComments) failed: {} }; @@ -190,18 +190,44 @@ module.exports.comment = { }); })(); break; + case "discussionComment": + postComment = commandHandler.controller.main.community.postDiscussionComment; + commentArgs = { topicOwner: null, gidforum: null, discussionId: null, quote: null }; + + // Get topicOwner & gidforum by scraping discussion DOM - Quick hack to await function that only supports callbacks + await (() => { + return new Promise((resolve) => { + commandHandler.controller.main.community.getSteamDiscussion(receiverSteamID64, (err, obj) => { // ReceiverSteamID64 is a URL in this case + if (err) { + logger("error", "Couldn't get discussion even though it exists?! Aborting!\n" + err); + respond("Error: Couldn't get discussion even though it exists?! Aborting!\n" + err); + return; + } + + commentArgs.topicOwner = obj.topicOwner; + commentArgs.gidforum = obj.gidforum; + commentArgs.discussionId = obj.id; + resolve(); + }); + }); + })(); + break; + default: + logger("warn", `[Main] Unsupported comment type '${activeRequestsObj.type}'! Rejecting request...`); + respond(await commandHandler.data.getLang("commentunsupportedtype", null, requesterID)); + return; } // Check if profile is private if (idType == "profile") { - commandHandler.controller.main.community.getSteamUser(new SteamID(receiverSteamID64), (err, user) => { + commandHandler.controller.main.community.getSteamUser(new SteamID(receiverSteamID64), async (err, user) => { if (err) { logger("warn", `[Main] Failed to check if ${receiverSteamID64} is private: ${err}\n Trying to comment anyway and hoping no error occurs...`); // This can happen sometimes and most of the times commenting will still work } else { logger("debug", "Successfully checked privacyState of receiving user: " + user.privacyState); - if (user.privacyState != "public") return respond(commandHandler.data.lang.commentuserprofileprivate); // Only check if getting the Steam user's data didn't result in an error + if (user.privacyState != "public") return respond(await commandHandler.data.getLang("commentuserprofileprivate", null, requesterID)); // Only check if getting the Steam user's data didn't result in an error } // Register this comment process in activeRequests @@ -232,8 +258,9 @@ module.exports.comment = { * @param {object} commentArgs All arguments this postComment function needs, without callback. It will be applied and a callback added as last param. Include a key called "quote" to dynamically replace it with a random quote. * @param {string} receiverSteamID64 steamID64 of the profile to receive the comments */ -function comment(commandHandler, resInfo, respond, postComment, commentArgs, receiverSteamID64) { - let activeReqEntry = commandHandler.controller.activeRequests[receiverSteamID64]; // Make using the obj shorter +async function comment(commandHandler, resInfo, respond, postComment, commentArgs, receiverSteamID64) { + let activeReqEntry = commandHandler.controller.activeRequests[receiverSteamID64]; // Make using the obj shorter + let requesterID = resInfo.userID; // Log request start and give user cooldown on the first iteration @@ -244,13 +271,13 @@ function comment(commandHandler, resInfo, respond, postComment, commentArgs, rec // Only send estimated wait time message for multiple comments if (activeReqEntry.amount > 1) { - let waitTime = timeToString(Date.now() + ((activeReqEntry.amount - 1) * commandHandler.data.config.commentdelay)); // Amount - 1 because the first comment is instant. Multiply by delay and add to current time to get timestamp when last comment was sent + let waitTime = timeToString(Date.now() + ((activeReqEntry.amount - 1) * commandHandler.data.config.requestDelay)); // Amount - 1 because the first comment is instant. Multiply by delay and add to current time to get timestamp when last comment was sent - respond(commandHandler.data.lang.commentprocessstarted.replace("numberOfComments", activeReqEntry.amount).replace("waittime", waitTime)); + respond(await commandHandler.data.getLang("commentprocessstarted", { "numberOfComments": activeReqEntry.amount, "waittime": waitTime }, requesterID)); } // Give requesting user cooldown. Set timestamp to now if cooldown is disabled to avoid issues when a process is aborted but cooldown can't be cleared - if (commandHandler.data.config.commentcooldown == 0) commandHandler.data.setUserCooldown(activeReqEntry.requestedby, Date.now()); + if (commandHandler.data.config.requestCooldown == 0) commandHandler.data.setUserCooldown(activeReqEntry.requestedby, Date.now()); else commandHandler.data.setUserCooldown(activeReqEntry.requestedby, activeReqEntry.until); } @@ -288,15 +315,15 @@ function comment(commandHandler, resInfo, respond, postComment, commentArgs, rec }); - }, commandHandler.data.config.commentdelay * (i > 0)); // Delay every comment that is not the first one + }, commandHandler.data.config.requestDelay * (i > 0)); // Delay every comment that is not the first one - }, () => { // Function that will run on exit, aka the last iteration: Respond to the user + }, async () => { // Function that will run on exit, aka the last iteration: Respond to the user // Handle singular comments separately if (activeReqEntry.amount == 1) { // Check if an error occurred - if (Object.keys(activeReqEntry.failed).length > 0) respond(`${commandHandler.data.lang.commenterroroccurred}\n${Object.values(activeReqEntry.failed)[0]}`); // TODO: Do I want to handle retryComments for singular comments? - else respond(commandHandler.data.lang.commentsuccess.replace("failedamount", "0").replace("numberOfComments", "1")); + if (Object.keys(activeReqEntry.failed).length > 0) respond(`${await commandHandler.data.getLang("commenterroroccurred", null, requesterID)}\n${Object.values(activeReqEntry.failed)[0]}`); // TODO: Do I want to handle retryComments for singular comments? + else respond(await commandHandler.data.getLang("commentsuccess", { "failedamount": "0", "numberOfComments": "1" }, requesterID)); // Instantly set status of this request to cooldown activeReqEntry.status = "cooldown"; @@ -315,14 +342,14 @@ function comment(commandHandler, resInfo, respond, postComment, commentArgs, rec // Log and notify user about retry attempt starting in retryFailedCommentsDelay ms let untilStr = timeToString(Date.now() + commandHandler.data.advancedconfig.retryFailedCommentsDelay); - respond(commandHandler.data.lang.commentretrying.replace("failedamount", Object.keys(activeReqEntry.failed).length).replace("numberOfComments", activeReqEntry.amount - activeReqEntry.amountBeforeRetry).replace("untilStr", untilStr).replace("thisattempt", activeReqEntry.retryAttempt).replace("maxattempt", commandHandler.data.advancedconfig.retryFailedCommentsAttempts)); + respond(await commandHandler.data.getLang("commentretrying", { "failedamount": Object.keys(activeReqEntry.failed).length, "numberOfComments": activeReqEntry.amount - activeReqEntry.amountBeforeRetry, "untilStr": untilStr, "thisattempt": activeReqEntry.retryAttempt, "maxattempt": commandHandler.data.advancedconfig.retryFailedCommentsAttempts }, requesterID)); logger("info", `${Object.keys(activeReqEntry.failed).length}/${activeReqEntry.amount - activeReqEntry.amountBeforeRetry} comments failed for ${receiverSteamID64}. Retrying in ${untilStr} (Attempt ${activeReqEntry.retryAttempt}/${commandHandler.data.advancedconfig.retryFailedCommentsAttempts})`, false, false, logger.animation("waiting")); // Wait retryFailedCommentsDelay ms before retrying failed comments - setTimeout(() => { + setTimeout(async () => { // Check if comment process was aborted, send finished message and avoid increasing cooldown etc. if (!activeReqEntry || activeReqEntry.status == "aborted") { - respond(commandHandler.data.lang.requestaborted.replace("successAmount", "0").replace("totalAmount", Object.keys(activeReqEntry.failed).length)); + respond(await commandHandler.data.getLang("requestaborted", { "successAmount": "0", "totalAmount": Object.keys(activeReqEntry.failed).length }, requesterID)); logger("info", `Comment process for ${receiverSteamID64} was aborted while waiting for retry attempt ${activeReqEntry.retryAttempt}. Stopping...`); return; } @@ -331,8 +358,8 @@ function comment(commandHandler, resInfo, respond, postComment, commentArgs, rec activeReqEntry.amountBeforeRetry = activeReqEntry.amount; activeReqEntry.amount += Object.keys(activeReqEntry.failed).length; - // Increase until value (amount of retried comments * commentdelay) + delay before starting retry attempts - activeReqEntry.until = activeReqEntry.until + (Object.keys(activeReqEntry.failed).length * commandHandler.data.config.commentdelay) + commandHandler.data.advancedconfig.retryFailedCommentsDelay; + // Increase until value (amount of retried comments * requestDelay) + delay before starting retry attempts + activeReqEntry.until = activeReqEntry.until + (Object.keys(activeReqEntry.failed).length * commandHandler.data.config.requestDelay) + commandHandler.data.advancedconfig.retryFailedCommentsDelay; // Update cooldown to new extended until value commandHandler.data.setUserCooldown(activeReqEntry.requestedby, activeReqEntry.until); @@ -355,11 +382,11 @@ function comment(commandHandler, resInfo, respond, postComment, commentArgs, rec /* ------------- Send finished message for corresponding status ------------- */ if (activeReqEntry.status == "aborted") { - respond(commandHandler.data.lang.requestaborted.replace("successAmount", activeReqEntry.amount - activeReqEntry.amountBeforeRetry - Object.keys(activeReqEntry.failed).length).replace("totalAmount", activeReqEntry.amount - activeReqEntry.amountBeforeRetry)); + respond(await commandHandler.data.getLang("requestaborted", { "successAmount": activeReqEntry.amount - activeReqEntry.amountBeforeRetry - Object.keys(activeReqEntry.failed).length, "totalAmount": activeReqEntry.amount - activeReqEntry.amountBeforeRetry }, requesterID)); } else if (activeReqEntry.status == "error") { - respond(`${commandHandler.data.lang.comment429stop.replace("failedamount", Object.keys(activeReqEntry.failed).length).replace("numberOfComments", activeReqEntry.amount - activeReqEntry.amountBeforeRetry)}\n\n${commandHandler.data.lang.commentfailedcmdreference.replace(/cmdprefix/g, resInfo.cmdprefix)}`); // Add !failed cmd reference to message + respond(`${await commandHandler.data.getLang("comment429stop", { "failedamount": Object.keys(activeReqEntry.failed).length, "numberOfComments": activeReqEntry.amount - activeReqEntry.amountBeforeRetry }, requesterID)}\n\n${await commandHandler.data.getLang("commentfailedcmdreference", { "cmdprefix": resInfo.cmdprefix }, requesterID)}`); // Add !failed cmd reference to message logger("warn", "Stopped comment process because all proxies had a HTTP 429 (IP cooldown) error!"); } else { @@ -372,7 +399,7 @@ function comment(commandHandler, resInfo, respond, postComment, commentArgs, rec } // Send finished message - respond(`${commandHandler.data.lang.commentsuccess.replace("failedamount", Object.keys(activeReqEntry.failed).length).replace("numberOfComments", activeReqEntry.amount - activeReqEntry.amountBeforeRetry)}\n${failedcmdreference}`); + respond(`${await commandHandler.data.getLang("commentsuccess", { "failedamount": Object.keys(activeReqEntry.failed).length, "numberOfComments": activeReqEntry.amount - activeReqEntry.amountBeforeRetry }, requesterID)}\n${failedcmdreference}`); // Set status of this request to cooldown and add amount of successful comments to our global commentCounter activeReqEntry.status = "cooldown"; diff --git a/src/commands/core/favorite.js b/src/commands/core/favorite.js index 6ef4d24b..ecc96d1b 100644 --- a/src/commands/core/favorite.js +++ b/src/commands/core/favorite.js @@ -4,7 +4,7 @@ * Created Date: 02.06.2023 13:23:01 * Author: 3urobeat * - * Last Modified: 24.07.2023 19:42:37 + * Last Modified: 19.10.2023 19:00:06 * Modified By: 3urobeat * * Copyright (c) 2023 3urobeat @@ -58,22 +58,22 @@ module.exports.favorite = { let owners = commandHandler.data.cachefile.ownerid; if (resInfo.ownerIDs && resInfo.ownerIDs.length > 0) owners = resInfo.ownerIDs; - let requesterSteamID64 = resInfo.userID; - let ownercheck = owners.includes(requesterSteamID64); + let requesterID = resInfo.userID; + let ownercheck = owners.includes(requesterID); /* --------- Various checks --------- */ if (!resInfo.userID) { - respond(commandHandler.data.lang.nouserid); // Reject usage of command without an userID to avoid cooldown bypass + respond(await commandHandler.data.getLang("nouserid")); // Reject usage of command without an userID to avoid cooldown bypass return logger("err", "The favorite command was called without resInfo.userID! Blocking the command as I'm unable to apply cooldowns, which is required for this command!"); } - if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.botnotready); // Bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained - if (commandHandler.controller.info.activeLogin) return respond(commandHandler.data.lang.activerelog); // Bot is waiting for relog - if (commandHandler.data.config.maxComments == 0 && !ownercheck) return respond(commandHandler.data.lang.commandowneronly); // Command is restricted to owners only + if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("botnotready", null, requesterID)); // Bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained + if (commandHandler.controller.info.activeLogin) return respond(await commandHandler.data.getLang("activerelog", null, requesterID)); // Bot is waiting for relog + if (commandHandler.data.config.maxRequests == 0 && !ownercheck) return respond(await commandHandler.data.getLang("commandowneronly", null, requesterID)); // Command is restricted to owners only // Check and get arguments from user - let { amountRaw, id } = await getSharedfileArgs(commandHandler, args, "favorite", resInfo, respond); // We can use the voteArgs function here as it uses the same arguments + let { amountRaw, id } = await getSharedfileArgs(commandHandler, args, "favorite", resInfo, respond); if (!amountRaw && !id) return; // Looks like the helper aborted the request @@ -81,49 +81,49 @@ module.exports.favorite = { // Check if this id is already receiving something right now let idReq = commandHandler.controller.activeRequests[id]; - if (idReq && idReq.status == "active") return respond(commandHandler.data.lang.idalreadyreceiving); // Note: No need to check for user as that is supposed to be handled by a cooldown + if (idReq && idReq.status == "active") return respond(await commandHandler.data.getLang("idalreadyreceiving", null, requesterID)); // Note: No need to check for user as that is supposed to be handled by a cooldown // Check if user has cooldown - let { until, untilStr } = await commandHandler.data.getUserCooldown(requesterSteamID64); + let { until, untilStr } = await commandHandler.data.getUserCooldown(requesterID); - if (until > Date.now()) return respond(commandHandler.data.lang.idoncooldown.replace("remainingcooldown", untilStr)); + if (until > Date.now()) return respond(await commandHandler.data.getLang("idoncooldown", { "remainingcooldown": untilStr }, requesterID)); // Get all available bot accounts let { amount, availableAccounts, whenAvailableStr } = await getAvailableBotsForFavorizing(commandHandler, amountRaw, id, "favorite"); if ((availableAccounts.length < amount || availableAccounts.length == 0) && !whenAvailableStr) { // Check if this bot has not enough accounts suitable for this request and there won't be more available at any point. - if (availableAccounts.length == 0) respond(commandHandler.data.lang.favoritenoaccounts); // The < || == 0 check is intentional, as providing "all" will set amount to 0 if 0 accounts have been found - else respond(commandHandler.data.lang.favoriterequestless.replace("availablenow", availableAccounts.length)); + if (availableAccounts.length == 0) respond(await commandHandler.data.getLang("genericnoaccounts", null, requesterID)); // The < || == 0 check is intentional, as providing "all" will set amount to 0 if 0 accounts have been found + else respond(await commandHandler.data.getLang("genericrequestless", { "availablenow": availableAccounts.length }, requesterID)); return; } if (availableAccounts.length < amount) { // Check if not enough available accounts were found because of cooldown - respond(commandHandler.data.lang.favoritenotenoughavailableaccs.replace("waittime", whenAvailableStr).replace("availablenow", availableAccounts.length)); + respond(await commandHandler.data.getLang("genericnotenoughavailableaccs", { "waittime": whenAvailableStr, "availablenow": availableAccounts.length }, requesterID)); return; } // Get the sharedfile - commandHandler.controller.main.community.getSteamSharedFile(id, (err, sharedfile) => { + commandHandler.controller.main.community.getSteamSharedFile(id, async (err, sharedfile) => { if (err) { - respond(commandHandler.data.lang.errloadingsharedfile + err); + respond((await commandHandler.data.getLang("errloadingsharedfile", null, requesterID)) + err); return; } - // Register this favorite process in activeRequests. We use commentdelay here for now, not sure if I'm going to add a separate setting + // Register this favorite process in activeRequests commandHandler.controller.activeRequests[id] = { status: "active", type: "favorite", amount: amount, - requestedby: requesterSteamID64, + requestedby: requesterID, accounts: availableAccounts, thisIteration: -1, // Set to -1 so that first iteration will increase it to 0 retryAttempt: 0, - until: Date.now() + ((amount - 1) * commandHandler.data.config.commentdelay), // Calculate estimated wait time (first favorite is instant -> remove 1 from numberOfComments) + until: Date.now() + ((amount - 1) * commandHandler.data.config.requestDelay), // Calculate estimated wait time (first favorite is instant -> remove 1 from numberOfComments) failed: {} }; @@ -136,13 +136,13 @@ module.exports.favorite = { // Only send estimated wait time message for multiple favorites if (activeReqEntry.amount > 1) { - let waitTime = timeToString(Date.now() + ((activeReqEntry.amount - 1) * commandHandler.data.config.commentdelay)); // Amount - 1 because the first fav is instant. Multiply by delay and add to current time to get timestamp when last fav was sent + let waitTime = timeToString(Date.now() + ((activeReqEntry.amount - 1) * commandHandler.data.config.requestDelay)); // Amount - 1 because the first fav is instant. Multiply by delay and add to current time to get timestamp when last fav was sent - respond(commandHandler.data.lang.favoriteprocessstarted.replace("numberOfFavs", activeReqEntry.amount).replace("waittime", waitTime)); + respond(await commandHandler.data.getLang("favoriteprocessstarted", { "numberOfFavs": activeReqEntry.amount, "waittime": waitTime }, requesterID)); } // Give requesting user cooldown. Set timestamp to now if cooldown is disabled to avoid issues when a process is aborted but cooldown can't be cleared - if (commandHandler.data.config.commentcooldown == 0) commandHandler.data.setUserCooldown(activeReqEntry.requestedby, Date.now()); + if (commandHandler.data.config.requestCooldown == 0) commandHandler.data.setUserCooldown(activeReqEntry.requestedby, Date.now()); else commandHandler.data.setUserCooldown(activeReqEntry.requestedby, activeReqEntry.until); } @@ -180,14 +180,14 @@ module.exports.favorite = { }); - }, commandHandler.data.config.commentdelay * (i > 0)); // We use commentdelay here for now, not sure if I'm going to add a separate setting + }, commandHandler.data.config.requestDelay * (i > 0)); - }, () => { // Function that will run on exit, aka the last iteration: Respond to the user + }, async () => { // Function that will run on exit, aka the last iteration: Respond to the user /* ------------- Send finished message for corresponding status ------------- */ if (activeReqEntry.status == "aborted") { - respond(commandHandler.data.lang.requestaborted.replace("successAmount", activeReqEntry.amount - Object.keys(activeReqEntry.failed).length).replace("totalAmount", activeReqEntry.amount)); + respond(await commandHandler.data.getLang("requestaborted", { "successAmount": activeReqEntry.amount - Object.keys(activeReqEntry.failed).length, "totalAmount": activeReqEntry.amount }, requesterID)); } else { @@ -199,7 +199,7 @@ module.exports.favorite = { } // Send finished message - respond(`${commandHandler.data.lang.favoritesuccess.replace("failedamount", Object.keys(activeReqEntry.failed).length).replace("numberOfFavs", activeReqEntry.amount)}\n${failedcmdreference}`); + respond(`${await commandHandler.data.getLang("favoritesuccess", { "failedamount": Object.keys(activeReqEntry.failed).length, "numberOfFavs": activeReqEntry.amount }, requesterID)}\n${failedcmdreference}`); // Set status of this request to cooldown and add amount of successful comments to our global commentCounter activeReqEntry.status = "cooldown"; @@ -248,22 +248,22 @@ module.exports.unfavorite = { let owners = commandHandler.data.cachefile.ownerid; if (resInfo.ownerIDs && resInfo.ownerIDs.length > 0) owners = resInfo.ownerIDs; - let requesterSteamID64 = resInfo.userID; - let ownercheck = owners.includes(requesterSteamID64); + let requesterID = resInfo.userID; + let ownercheck = owners.includes(requesterID); /* --------- Various checks --------- */ if (!resInfo.userID) { - respond(commandHandler.data.lang.nouserid); // Reject usage of command without an userID to avoid cooldown bypass + respond(await commandHandler.data.getLang("nouserid")); // Reject usage of command without an userID to avoid cooldown bypass return logger("err", "The unfavorite command was called without resInfo.userID! Blocking the command as I'm unable to apply cooldowns, which is required for this command!"); } - if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.botnotready); // Bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained - if (commandHandler.controller.info.activeLogin) return respond(commandHandler.data.lang.activerelog); // Bot is waiting for relog - if (commandHandler.data.config.maxComments == 0 && !ownercheck) return respond(commandHandler.data.lang.commandowneronly); // Command is restricted to owners only + if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("botnotready", null, requesterID)); // Bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained + if (commandHandler.controller.info.activeLogin) return respond(await commandHandler.data.getLang("activerelog", null, requesterID)); // Bot is waiting for relog + if (commandHandler.data.config.maxRequests == 0 && !ownercheck) return respond(await commandHandler.data.getLang("commandowneronly", null, requesterID)); // Command is restricted to owners only // Check and get arguments from user - let { amountRaw, id } = await getSharedfileArgs(commandHandler, args, "unfavorite", resInfo, respond); // We can use the voteArgs function here as it uses the same arguments + let { amountRaw, id } = await getSharedfileArgs(commandHandler, args, "unfavorite", resInfo, respond); if (!amountRaw && !id) return; // Looks like the helper aborted the request @@ -271,49 +271,49 @@ module.exports.unfavorite = { // Check if this id is already receiving something right now let idReq = commandHandler.controller.activeRequests[id]; - if (idReq && idReq.status == "active") return respond(commandHandler.data.lang.idalreadyreceiving); // Note: No need to check for user as that is supposed to be handled by a cooldown + if (idReq && idReq.status == "active") return respond(await commandHandler.data.getLang("idalreadyreceiving", null, requesterID)); // Note: No need to check for user as that is supposed to be handled by a cooldown // Check if user has cooldown - let { until, untilStr } = await commandHandler.data.getUserCooldown(requesterSteamID64); + let { until, untilStr } = await commandHandler.data.getUserCooldown(requesterID); - if (until > Date.now()) return respond(commandHandler.data.lang.idoncooldown.replace("remainingcooldown", untilStr)); + if (until > Date.now()) return respond(await commandHandler.data.getLang("idoncooldown", { "remainingcooldown": untilStr }, requesterID)); // Get all available bot accounts let { amount, availableAccounts, whenAvailableStr } = await getAvailableBotsForFavorizing(commandHandler, amountRaw, id, "unfavorite"); if ((availableAccounts.length < amount || availableAccounts.length == 0) && !whenAvailableStr) { // Check if this bot has not enough accounts suitable for this request and there won't be more available at any point. - if (availableAccounts.length == 0) respond(commandHandler.data.lang.favoritenoaccounts); // The < || == 0 check is intentional, as providing "all" will set amount to 0 if 0 accounts have been found - else respond(commandHandler.data.lang.favoriterequestless.replace("availablenow", availableAccounts.length)); + if (availableAccounts.length == 0) respond(await commandHandler.data.getLang("genericnoaccounts", null, requesterID)); // The < || == 0 check is intentional, as providing "all" will set amount to 0 if 0 accounts have been found + else respond(await commandHandler.data.getLang("genericrequestless", { "availablenow": availableAccounts.length }, requesterID)); return; } if (availableAccounts.length < amount) { // Check if not enough available accounts were found because of cooldown - respond(commandHandler.data.lang.favoritenotenoughavailableaccs.replace("waittime", whenAvailableStr).replace("availablenow", availableAccounts.length)); + respond(await commandHandler.data.getLang("genericnotenoughavailableaccs", { "waittime": whenAvailableStr, "availablenow": availableAccounts.length }, requesterID)); return; } // Get the sharedfile - commandHandler.controller.main.community.getSteamSharedFile(id, (err, sharedfile) => { + commandHandler.controller.main.community.getSteamSharedFile(id, async (err, sharedfile) => { if (err) { - respond(commandHandler.data.lang.errloadingsharedfile + err); + respond((await commandHandler.data.getLang("errloadingsharedfile", null, requesterID)) + err); return; } - // Register this unfavorite process in activeRequests. We use commentdelay here for now, not sure if I'm going to add a separate setting + // Register this unfavorite process in activeRequests commandHandler.controller.activeRequests[id] = { status: "active", type: "unfavorite", amount: amount, - requestedby: requesterSteamID64, + requestedby: requesterID, accounts: availableAccounts, thisIteration: -1, // Set to -1 so that first iteration will increase it to 0 retryAttempt: 0, - until: Date.now() + ((amount - 1) * commandHandler.data.config.commentdelay), // Calculate estimated wait time (first unfavorite is instant -> remove 1 from numberOfComments) + until: Date.now() + ((amount - 1) * commandHandler.data.config.requestDelay), // Calculate estimated wait time (first unfavorite is instant -> remove 1 from numberOfComments) failed: {} }; @@ -326,13 +326,13 @@ module.exports.unfavorite = { // Only send estimated wait time message for multiple favorites if (activeReqEntry.amount > 1) { - let waitTime = timeToString(Date.now() + ((activeReqEntry.amount - 1) * commandHandler.data.config.commentdelay)); // Amount - 1 because the first fav is instant. Multiply by delay and add to current time to get timestamp when last fav was sent + let waitTime = timeToString(Date.now() + ((activeReqEntry.amount - 1) * commandHandler.data.config.requestDelay)); // Amount - 1 because the first fav is instant. Multiply by delay and add to current time to get timestamp when last fav was sent - respond(commandHandler.data.lang.favoriteprocessstarted.replace("numberOfFavs", activeReqEntry.amount).replace("waittime", waitTime)); + respond(await commandHandler.data.getLang("favoriteprocessstarted", { "numberOfFavs": activeReqEntry.amount, "waittime": waitTime }, requesterID)); } // Give requesting user cooldown. Set timestamp to now if cooldown is disabled to avoid issues when a process is aborted but cooldown can't be cleared - if (commandHandler.data.config.commentcooldown == 0) commandHandler.data.setUserCooldown(activeReqEntry.requestedby, Date.now()); + if (commandHandler.data.config.requestCooldown == 0) commandHandler.data.setUserCooldown(activeReqEntry.requestedby, Date.now()); else commandHandler.data.setUserCooldown(activeReqEntry.requestedby, activeReqEntry.until); } @@ -370,14 +370,14 @@ module.exports.unfavorite = { }); - }, commandHandler.data.config.commentdelay * (i > 0)); // We use commentdelay here for now, not sure if I'm going to add a separate setting + }, commandHandler.data.config.requestDelay * (i > 0)); - }, () => { // Function that will run on exit, aka the last iteration: Respond to the user + }, async () => { // Function that will run on exit, aka the last iteration: Respond to the user /* ------------- Send finished message for corresponding status ------------- */ if (activeReqEntry.status == "aborted") { - respond(commandHandler.data.lang.requestaborted.replace("successAmount", activeReqEntry.amount - Object.keys(activeReqEntry.failed).length).replace("totalAmount", activeReqEntry.amount)); + respond(await commandHandler.data.getLang("requestaborted", { "successAmount": activeReqEntry.amount - Object.keys(activeReqEntry.failed).length, "totalAmount": activeReqEntry.amount }, requesterID)); } else { @@ -389,7 +389,7 @@ module.exports.unfavorite = { } // Send finished message - respond(`${commandHandler.data.lang.favoritesuccess.replace("failedamount", Object.keys(activeReqEntry.failed).length).replace("numberOfFavs", activeReqEntry.amount)}\n${failedcmdreference}`); + respond(`${await commandHandler.data.getLang("favoritesuccess", { "failedamount": Object.keys(activeReqEntry.failed).length, "numberOfFavs": activeReqEntry.amount }, requesterID)}\n${failedcmdreference}`); // Set status of this request to cooldown and add amount of successful comments to our global commentCounter activeReqEntry.status = "cooldown"; diff --git a/src/commands/core/follow.js b/src/commands/core/follow.js new file mode 100644 index 00000000..2ae43ad2 --- /dev/null +++ b/src/commands/core/follow.js @@ -0,0 +1,390 @@ +/* + * File: follow.js + * Project: steam-comment-service-bot + * Created Date: 24.09.2023 15:04:33 + * Author: 3urobeat + * + * Last Modified: 19.10.2023 19:00:06 + * Modified By: 3urobeat + * + * Copyright (c) 2023 3urobeat + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. If not, see . + */ + + +const CommandHandler = require("../commandHandler.js"); // eslint-disable-line +const { getFollowArgs } = require("../helpers/getFollowArgs.js"); +const { getAvailableBotsForFollowing } = require("../helpers/getFollowBots.js"); +const { syncLoop, timeToString } = require("../../controller/helpers/misc.js"); +const { handleFollowIterationSkip, logFollowError } = require("../helpers/handleFollowErrors.js"); + + +module.exports.follow = { + names: ["follow"], + description: "Follows a user with all bot accounts that haven't yet done so", + args: [ + { + name: "amount", + description: "The amount of follows to request", + type: "string", + isOptional: false, + ownersOnly: false + }, + { + name: "ID", + description: "The link, steamID64 or vanity of the profile to follow", + type: "string", + isOptional: true, + ownersOnly: true + } + ], + ownersOnly: false, + + /** + * The follow command + * @param {CommandHandler} commandHandler The commandHandler object + * @param {Array} args Array of arguments that will be passed to the command + * @param {function(object, object, string): void} respondModule Function that will be called to respond to the user's request. Passes context, resInfo and txt as parameters. + * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. + * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). + */ + run: async (commandHandler, args, respondModule, context, resInfo) => { + let respond = ((txt) => respondModule(context, resInfo, txt)); // Shorten each call + + // Get the correct ownerid array for this request + let owners = commandHandler.data.cachefile.ownerid; + if (resInfo.ownerIDs && resInfo.ownerIDs.length > 0) owners = resInfo.ownerIDs; + + let requesterID = resInfo.userID; + let ownercheck = owners.includes(requesterID); + + + /* --------- Various checks --------- */ + if (!resInfo.userID) { + respond(await commandHandler.data.getLang("nouserid")); // Reject usage of command without an userID to avoid cooldown bypass + return logger("err", "The follow command was called without resInfo.userID! Blocking the command as I'm unable to apply cooldowns, which is required for this command!"); + } + if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("botnotready", null, requesterID)); // Bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained + if (commandHandler.controller.info.activeLogin) return respond(await commandHandler.data.getLang("activerelog", null, requesterID)); // Bot is waiting for relog + if (commandHandler.data.config.maxRequests == 0 && !ownercheck) return respond(await commandHandler.data.getLang("commandowneronly", null, requesterID)); // Command is restricted to owners only + + + // Check and get arguments from user + let { amountRaw, id, idType } = await getFollowArgs(commandHandler, args, "follow", resInfo, respond); + + if (!amountRaw && !id) return; // Looks like the helper aborted the request + + + // Check if this id is already receiving something right now + let idReq = commandHandler.controller.activeRequests[id]; + + if (idReq && idReq.status == "active") return respond(await commandHandler.data.getLang("idalreadyreceiving", null, requesterID)); // Note: No need to check for user as that is supposed to be handled by a cooldown + + + // Check if user has cooldown + let { until, untilStr } = await commandHandler.data.getUserCooldown(requesterID); + + if (until > Date.now()) return respond(await commandHandler.data.getLang("idoncooldown", { "remainingcooldown": untilStr }, requesterID)); + + + // Get all available bot accounts. Block limited accounts from following curators + let allowLimitedAccounts = (idType != "curator"); + let { amount, availableAccounts, whenAvailableStr } = await getAvailableBotsForFollowing(commandHandler, amountRaw, allowLimitedAccounts, id, idType, "follow"); + + if ((availableAccounts.length < amount || availableAccounts.length == 0) && !whenAvailableStr) { // Check if this bot has not enough accounts suitable for this request and there won't be more available at any point. + if (availableAccounts.length == 0) respond(await commandHandler.data.getLang("genericnoaccounts", null, requesterID)); // The < || == 0 check is intentional, as providing "all" will set amount to 0 if 0 accounts have been found + else respond(await commandHandler.data.getLang("genericrequestless", { "availablenow": availableAccounts.length }, requesterID)); + + return; + } + + if (availableAccounts.length < amount) { // Check if not enough available accounts were found because of cooldown + respond(await commandHandler.data.getLang("genericnotenoughavailableaccs", { "waittime": whenAvailableStr, "availablenow": availableAccounts.length }, requesterID)); + return; + } + + + // Register this follow process in activeRequests + commandHandler.controller.activeRequests[id] = { + status: "active", + type: idType + "Follow", + amount: amount, + requestedby: requesterID, + accounts: availableAccounts, + thisIteration: -1, // Set to -1 so that first iteration will increase it to 0 + retryAttempt: 0, + until: Date.now() + ((amount - 1) * commandHandler.data.config.requestDelay), // Calculate estimated wait time (first follow is instant -> remove 1 from numberOfComments) + failed: {} + }; + + let activeReqEntry = commandHandler.controller.activeRequests[id]; // Make using the obj shorter + + + // Log request start and give user cooldown on the first iteration + if (activeReqEntry.thisIteration == -1) { + logger("info", `${logger.colors.fggreen}[${commandHandler.controller.main.logPrefix}] ${activeReqEntry.amount} Follow(s) requested. Starting to follow ${idType} ${id}...`); + + // Only send estimated wait time message for multiple follow + if (activeReqEntry.amount > 1) { + let waitTime = timeToString(Date.now() + ((activeReqEntry.amount - 1) * commandHandler.data.config.requestDelay)); // Amount - 1 because the first fav is instant. Multiply by delay and add to current time to get timestamp when last fav was sent + + respond(await commandHandler.data.getLang("followprocessstarted", { "totalamount": activeReqEntry.amount, "waittime": waitTime }, requesterID)); + } + + // Give requesting user cooldown. Set timestamp to now if cooldown is disabled to avoid issues when a process is aborted but cooldown can't be cleared + if (commandHandler.data.config.requestCooldown == 0) commandHandler.data.setUserCooldown(activeReqEntry.requestedby, Date.now()); + else commandHandler.data.setUserCooldown(activeReqEntry.requestedby, activeReqEntry.until); + } + + + // Start voting with all available accounts + syncLoop(amount, (loop, i) => { + setTimeout(() => { + + let bot = commandHandler.controller.bots[availableAccounts[i]]; + activeReqEntry.thisIteration++; + + if (!handleFollowIterationSkip(commandHandler, loop, bot, id)) return; // Skip iteration if false was returned + + /* --------- Try to follow --------- */ + let followFunc = activeReqEntry.type == "curatorFollow" ? bot.community.followCurator : bot.community.followUser; // Get the correct function, depending on if the user provided a curator id or a user id + + followFunc.call(bot.community, id, (error) => { // Very important! Using call() and passing the bot's community instance will keep context (this.) as it was lost by our postComment variable assignment! + + /* --------- Handle errors thrown by this follow attempt or update ratingHistory db and log success message --------- */ + if (error) { + logFollowError(error, commandHandler, bot, id); + + } else { + + // Add follow entry + commandHandler.data.ratingHistoryDB.insert({ id: id, accountName: activeReqEntry.accounts[i], type: idType + "Follow", time: Date.now() }, (err) => { + if (err) logger("warn", `Failed to insert '${idType}Follow' entry for '${activeReqEntry.accounts[i]}' for '${id}' into ratingHistory database! Error: ` + err); + }); + + // Log success message + if (commandHandler.data.proxies.length > 1) logger("info", `[${bot.logPrefix}] Sending follow ${activeReqEntry.thisIteration + 1}/${activeReqEntry.amount} for ${idType} ${id} with proxy ${bot.loginData.proxyIndex}...`); + else logger("info", `[${bot.logPrefix}] Sending follow ${activeReqEntry.thisIteration + 1}/${activeReqEntry.amount} for ${idType} ${id}...`); + } + + // Continue with the next iteration + loop.next(); + + }); + + }, commandHandler.data.config.requestDelay * (i > 0)); + + }, async () => { // Function that will run on exit, aka the last iteration: Respond to the user + + /* ------------- Send finished message for corresponding status ------------- */ + if (activeReqEntry.status == "aborted") { + + respond(await commandHandler.data.getLang("requestaborted", { "successAmount": activeReqEntry.amount - Object.keys(activeReqEntry.failed).length, "totalAmount": activeReqEntry.amount }, requesterID)); + + } else { + + // Add reference to !failed command to finished message if at least one follow failed + let failedcmdreference = ""; + + if (Object.keys(commandHandler.controller.activeRequests[id].failed).length > 0) { + failedcmdreference = `\nTo get detailed information why which request failed please type '${resInfo.cmdprefix}failed'. You can read why your error was probably caused here: https://github.com/3urobeat/steam-comment-service-bot/blob/master/docs/wiki/errors_doc.md`; + } + + // Send finished message + respond(`${await commandHandler.data.getLang("followsuccess", { "failedamount": Object.keys(activeReqEntry.failed).length, "totalamount": activeReqEntry.amount }, requesterID)}\n${failedcmdreference}`); + + // Set status of this request to cooldown and add amount of successful comments to our global commentCounter + activeReqEntry.status = "cooldown"; + + } + + }); + } +}; + + +module.exports.unfollow = { + names: ["unfollow"], + description: "Unfollows a user with all bot accounts that have followed them", + args: [ + { + name: "amount", + description: "The amount of unfollows to request", + type: "string", + isOptional: false, + ownersOnly: false + }, + { + name: "ID", + description: "The link, steamID64 or vanity of the profile to unfollow", + type: "string", + isOptional: true, + ownersOnly: true + } + ], + ownersOnly: false, + + /** + * The unfollow command + * @param {CommandHandler} commandHandler The commandHandler object + * @param {Array} args Array of arguments that will be passed to the command + * @param {function(object, object, string): void} respondModule Function that will be called to respond to the user's request. Passes context, resInfo and txt as parameters. + * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. + * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). + */ + run: async (commandHandler, args, respondModule, context, resInfo) => { + let respond = ((txt) => respondModule(context, resInfo, txt)); // Shorten each call + + // Get the correct ownerid array for this request + let owners = commandHandler.data.cachefile.ownerid; + if (resInfo.ownerIDs && resInfo.ownerIDs.length > 0) owners = resInfo.ownerIDs; + + let requesterID = resInfo.userID; + let ownercheck = owners.includes(requesterID); + + + /* --------- Various checks --------- */ + if (!resInfo.userID) { + respond(await commandHandler.data.getLang("nouserid")); // Reject usage of command without an userID to avoid cooldown bypass + return logger("err", "The unfollow command was called without resInfo.userID! Blocking the command as I'm unable to apply cooldowns, which is required for this command!"); + } + if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("botnotready", null, requesterID)); // Bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained + if (commandHandler.controller.info.activeLogin) return respond(await commandHandler.data.getLang("activerelog", null, requesterID)); // Bot is waiting for relog + if (commandHandler.data.config.maxRequests == 0 && !ownercheck) return respond(await commandHandler.data.getLang("commandowneronly", null, requesterID)); // Command is restricted to owners only + + + // Check and get arguments from user + let { amountRaw, id, idType } = await getFollowArgs(commandHandler, args, "unfollow", resInfo, respond); + + if (!amountRaw && !id) return; // Looks like the helper aborted the request + + + // Check if this id is already receiving something right now + let idReq = commandHandler.controller.activeRequests[id]; + + if (idReq && idReq.status == "active") return respond(await commandHandler.data.getLang("idalreadyreceiving", null, requesterID)); // Note: No need to check for user as that is supposed to be handled by a cooldown + + + // Check if user has cooldown + let { until, untilStr } = await commandHandler.data.getUserCooldown(requesterID); + + if (until > Date.now()) return respond(await commandHandler.data.getLang("idoncooldown", { "remainingcooldown": untilStr }, requesterID)); + + + // Get all available bot accounts. Block limited accounts from following curators + let allowLimitedAccounts = (idType != "curator"); + let { amount, availableAccounts, whenAvailableStr } = await getAvailableBotsForFollowing(commandHandler, amountRaw, allowLimitedAccounts, id, idType, "unfollow"); + + if ((availableAccounts.length < amount || availableAccounts.length == 0) && !whenAvailableStr) { // Check if this bot has not enough accounts suitable for this request and there won't be more available at any point. + if (availableAccounts.length == 0) respond(await commandHandler.data.getLang("genericnoaccounts", null, requesterID)); // The < || == 0 check is intentional, as providing "all" will set amount to 0 if 0 accounts have been found + else respond(await commandHandler.data.getLang("genericrequestless", { "availablenow": availableAccounts.length }, requesterID)); + + return; + } + + if (availableAccounts.length < amount) { // Check if not enough available accounts were found because of cooldown + respond(await commandHandler.data.getLang("genericnotenoughavailableaccs", { "waittime": whenAvailableStr, "availablenow": availableAccounts.length }, requesterID)); + return; + } + + + // Register this unfollow process in activeRequests + commandHandler.controller.activeRequests[id] = { + status: "active", + type: idType + "Unfollow", + amount: amount, + requestedby: requesterID, + accounts: availableAccounts, + thisIteration: -1, // Set to -1 so that first iteration will increase it to 0 + retryAttempt: 0, + until: Date.now() + ((amount - 1) * commandHandler.data.config.requestDelay), // Calculate estimated wait time (first unfollow is instant -> remove 1 from numberOfComments) + failed: {} + }; + + let activeReqEntry = commandHandler.controller.activeRequests[id]; // Make using the obj shorter + + + // Log request start and give user cooldown on the first iteration + if (activeReqEntry.thisIteration == -1) { + logger("info", `${logger.colors.fggreen}[${commandHandler.controller.main.logPrefix}] ${activeReqEntry.amount} Unfollow(s) requested. Starting to unfollow ${idType} ${id}...`); + + // Only send estimated wait time message for multiple unfollow + if (activeReqEntry.amount > 1) { + let waitTime = timeToString(Date.now() + ((activeReqEntry.amount - 1) * commandHandler.data.config.requestDelay)); // Amount - 1 because the first fav is instant. Multiply by delay and add to current time to get timestamp when last fav was sent + + respond(await commandHandler.data.getLang("followprocessstarted", { "totalamount": activeReqEntry.amount, "waittime": waitTime }, requesterID)); + } + + // Give requesting user cooldown. Set timestamp to now if cooldown is disabled to avoid issues when a process is aborted but cooldown can't be cleared + if (commandHandler.data.config.requestCooldown == 0) commandHandler.data.setUserCooldown(activeReqEntry.requestedby, Date.now()); + else commandHandler.data.setUserCooldown(activeReqEntry.requestedby, activeReqEntry.until); + } + + + // Start voting with all available accounts + syncLoop(amount, (loop, i) => { + setTimeout(() => { + + let bot = commandHandler.controller.bots[availableAccounts[i]]; + activeReqEntry.thisIteration++; + + if (!handleFollowIterationSkip(commandHandler, loop, bot, id)) return; // Skip iteration if false was returned + + /* --------- Try to unfollow --------- */ + let followFunc = activeReqEntry.type == "curatorUnfollow" ? bot.community.unfollowCurator : bot.community.unfollowUser; // Get the correct function, depending on if the user provided a curator id or a user id + + followFunc.call(bot.community, id, (error) => { // Very important! Using call() and passing the bot's community instance will keep context (this.) as it was lost by our postComment variable assignment! + + /* --------- Handle errors thrown by this unfollow attempt or update ratingHistory db and log success message --------- */ + if (error) { + logFollowError(error, commandHandler, bot, id); + + } else { + + // Remove follow entry + commandHandler.data.ratingHistoryDB.remove({ id: id, accountName: activeReqEntry.accounts[i], type: idType + "Follow" }, (err) => { + if (err) logger("warn", `Failed to remove '${idType}Follow' entry for '${activeReqEntry.accounts[i]}' for '${id}' from ratingHistory database! Error: ` + err); + }); + + // Log success message + if (commandHandler.data.proxies.length > 1) logger("info", `[${bot.logPrefix}] Sending unfollow ${activeReqEntry.thisIteration + 1}/${activeReqEntry.amount} for ${idType} ${id} with proxy ${bot.loginData.proxyIndex}...`); + else logger("info", `[${bot.logPrefix}] Sending unfollow ${activeReqEntry.thisIteration + 1}/${activeReqEntry.amount} for ${idType} ${id}...`); + } + + // Continue with the next iteration + loop.next(); + + }); + + }, commandHandler.data.config.requestDelay * (i > 0)); + + }, async () => { // Function that will run on exit, aka the last iteration: Respond to the user + + /* ------------- Send finished message for corresponding status ------------- */ + if (activeReqEntry.status == "aborted") { + + respond(await commandHandler.data.getLang("requestaborted", { "successAmount": activeReqEntry.amount - Object.keys(activeReqEntry.failed).length, "totalAmount": activeReqEntry.amount }, requesterID)); + + } else { + + // Add reference to !failed command to finished message if at least one unfollow failed + let failedcmdreference = ""; + + if (Object.keys(commandHandler.controller.activeRequests[id].failed).length > 0) { + failedcmdreference = `\nTo get detailed information why which request failed please type '${resInfo.cmdprefix}failed'. You can read why your error was probably caused here: https://github.com/3urobeat/steam-comment-service-bot/blob/master/docs/wiki/errors_doc.md`; + } + + // Send finished message + respond(`${await commandHandler.data.getLang("followsuccess", { "failedamount": Object.keys(activeReqEntry.failed).length, "totalamount": activeReqEntry.amount }, requesterID)}\n${failedcmdreference}`); + + // Set status of this request to cooldown and add amount of successful comments to our global commentCounter + activeReqEntry.status = "cooldown"; + + } + + }); + } +}; \ No newline at end of file diff --git a/src/commands/core/friend.js b/src/commands/core/friend.js index acfb2d4e..aaef2c35 100644 --- a/src/commands/core/friend.js +++ b/src/commands/core/friend.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 10.07.2023 13:02:11 + * Last Modified: 07.10.2023 23:34:56 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -42,23 +42,24 @@ module.exports.addFriend = { * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). */ - run: (commandHandler, args, respondModule, context, resInfo) => { + run: async (commandHandler, args, respondModule, context, resInfo) => { let respond = ((txt) => respondModule(context, resInfo, txt)); // Shorten each call + let requesterID = resInfo.userID; - if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.botnotready); // Check if bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained + if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("botnotready", null, requesterID)); // Check if bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained - if (!args[0]) return respond(commandHandler.data.lang.invalidprofileid); + if (!args[0]) return respond(await commandHandler.data.getLang("invalidprofileid", null, requesterID)); - commandHandler.controller.handleSteamIdResolving(args[0], "profile", (err, res) => { - if (err) return respond(commandHandler.data.lang.invalidprofileid + "\n\nError: " + err); + commandHandler.controller.handleSteamIdResolving(args[0], "profile", async (err, res) => { + if (err) return respond((await commandHandler.data.getLang("invalidprofileid", null, requesterID)) + "\n\nError: " + err); // Check if first bot account is limited to be able to display error message instantly if (commandHandler.controller.main.user.limitations && commandHandler.controller.main.user.limitations.limited == true) { - respond(commandHandler.data.lang.addfriendcmdacclimited.replace("profileid", res)); + respond(await commandHandler.data.getLang("addfriendcmdacclimited", { "profileid": res }, requesterID)); return; } - respond(commandHandler.data.lang.addfriendcmdsuccess.replace("profileid", res).replace("estimatedtime", 5 * commandHandler.controller.getBots().length)); + respond(await commandHandler.data.getLang("addfriendcmdsuccess", { "profileid": res, "estimatedtime": 5 * commandHandler.controller.getBots().length }, requesterID)); logger("info", `Adding friend ${res} with all bot accounts... This will take ~${5 * commandHandler.controller.getBots().length} seconds.`); commandHandler.controller.getBots().forEach((e, i) => { @@ -110,17 +111,18 @@ module.exports.unfriend = { * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). */ - run: (commandHandler, args, respondModule, context, resInfo) => { + run: async (commandHandler, args, respondModule, context, resInfo) => { let respond = ((txt) => respondModule(context, resInfo, txt)); // Shorten each call + let requesterID = resInfo.userID; - if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.botnotready); // Check if bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained + if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("botnotready", null, requesterID)); // Check if bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained // Check for no args again as the default behavior from above might be unavailable when calling from outside of the Steam Chat - if (!args[0] && !resInfo.fromSteamChat) return respond(commandHandler.data.lang.noidparam); + if (!args[0] && !resInfo.fromSteamChat) return respond(await commandHandler.data.getLang("noidparam", null, requesterID)); // Unfriend message sender with all bot accounts if no id was provided and the command was called from the steam chat if (!args[0] && resInfo.userID && resInfo.fromSteamChat) { - respond(commandHandler.data.lang.unfriendcmdsuccess); + respond(commandHandler.data.getLang("unfriendcmdsuccess", null, requesterID)); logger("info", `Removing friend ${resInfo.userID} from all bot accounts...`); commandHandler.controller.getBots().forEach((e, i) => { @@ -136,11 +138,11 @@ module.exports.unfriend = { if (resInfo.ownerIDs && resInfo.ownerIDs.length > 0) owners = resInfo.ownerIDs; // Unfriending a specific user is owner only - if (!owners.includes(resInfo.userID)) return respond(commandHandler.data.lang.commandowneronly); + if (!owners.includes(resInfo.userID)) return respond(await commandHandler.data.getLang("commandowneronly", null, requesterID)); - commandHandler.controller.handleSteamIdResolving(args[0], "profile", (err, res) => { - if (err) return respond(commandHandler.data.lang.invalidprofileid + "\n\nError: " + err); - if (commandHandler.data.cachefile.ownerid.includes(res)) return respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.idisownererror); // Check for the "original" ownerid array here, we don't care about non Steam IDs + commandHandler.controller.handleSteamIdResolving(args[0], "profile", async (err, res) => { + if (err) return respond((await commandHandler.data.getLang("invalidprofileid", null, requesterID)) + "\n\nError: " + err); + if (commandHandler.data.cachefile.ownerid.includes(res)) return respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("idisownererror", null, requesterID)); // Check for the "original" ownerid array here, we don't care about non Steam IDs commandHandler.controller.getBots().forEach((e, i) => { setTimeout(() => { @@ -148,7 +150,7 @@ module.exports.unfriend = { }, 1000 * i); // Delay every iteration so that we don't make a ton of requests at once }); - respond(commandHandler.data.lang.unfriendidcmdsuccess.replace("profileid", res)); + respond(await commandHandler.data.getLang("unfriendidcmdsuccess", { "profileid": res }, requesterID)); logger("info", `Removed friend ${res} from all bot accounts.`); }); } @@ -178,25 +180,26 @@ module.exports.unfriendall = { * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). */ - run: (commandHandler, args, respondModule, context, resInfo) => { + run: async (commandHandler, args, respondModule, context, resInfo) => { let respond = ((txt) => respondModule(context, resInfo, txt)); // Shorten each call + let requesterID = resInfo.userID; - if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.botnotready); // Check if bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained + if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("botnotready", null, requesterID)); // Check if bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained // TODO: This is bad. Rewrite using a message collector, maybe add one to steamChatInteraction helper var abortunfriendall; // eslint-disable-line no-var if (args[0] == "abort") { - respond(commandHandler.data.lang.unfriendallcmdabort); + respond(await commandHandler.data.getLang("unfriendallcmdabort", null, requesterID)); return abortunfriendall = true; } abortunfriendall = false; - respond(commandHandler.data.lang.unfriendallcmdpending.replace(/cmdprefix/g, resInfo.cmdprefix)); + respond(await commandHandler.data.getLang("unfriendallcmdpending", { cmdprefix: resInfo.cmdprefix }, requesterID)); - setTimeout(() => { + setTimeout(async () => { if (abortunfriendall) return logger("info", "unfriendall process was aborted."); - respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.unfriendallcmdstart); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained + respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("unfriendallcmdstart", null, requesterID)); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained logger("info", "Starting to unfriend everyone..."); for (let i = 0; i < commandHandler.controller.getBots().length; i++) { diff --git a/src/commands/core/general.js b/src/commands/core/general.js index c4d6e16e..f9536798 100644 --- a/src/commands/core/general.js +++ b/src/commands/core/general.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 26.07.2023 16:03:51 + * Last Modified: 18.10.2023 23:07:24 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -36,41 +36,49 @@ module.exports.help = { * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). */ - run: (commandHandler, args, respondModule, context, resInfo) => { + run: async (commandHandler, args, respondModule, context, resInfo) => { let respond = ((txt) => respondModule(context, resInfo, txt)); // Shorten each call + let requesterID = resInfo.userID; // Get the correct ownerid array for this request let owners = commandHandler.data.cachefile.ownerid; if (resInfo.ownerIDs && resInfo.ownerIDs.length > 0) owners = resInfo.ownerIDs; - // Construct comment text for owner or non owner + // Construct comment text for owner and user let commentText; if (owners.includes(resInfo.userID)) { - if (commandHandler.controller.getBots().length > 1 || commandHandler.data.config.maxOwnerComments) commentText = `'${resInfo.cmdprefix}comment (amount/"all") [profileid] [custom, quotes]' - ${commandHandler.data.lang.helpcommentowner1.replace("maxOwnerComments", commandHandler.data.config.maxOwnerComments)}`; - else commentText = `'${resInfo.cmdprefix}comment ("1") [profileid] [custom, quotes]' - ${commandHandler.data.lang.helpcommentowner2}`; + commentText = `'${resInfo.cmdprefix}comment (amount/"all") [id/url] [custom, quotes]' - ${await commandHandler.data.getLang("helpcommentowner", { "maxOwnerRequests": commandHandler.data.config.maxOwnerRequests }, requesterID)}`; } else { - if (commandHandler.controller.getBots().length > 1 || commandHandler.data.config.maxComments) commentText = `'${resInfo.cmdprefix}comment (amount/"all")' - ${commandHandler.data.lang.helpcommentuser1.replace("maxComments", commandHandler.data.config.maxComments)}`; - else commentText = `'${resInfo.cmdprefix}comment' - ${commandHandler.data.lang.helpcommentuser2}`; + commentText = `'${resInfo.cmdprefix}comment (amount/"all")' - ${await commandHandler.data.getLang("helpcommentuser", { "maxRequests": commandHandler.data.config.maxRequests }, requesterID)}`; } - // Add yourgroup text if one was set - let yourgroupText; + // Construct follow text for owner and user + let followText; + + if (owners.includes(resInfo.userID)) { + followText = `'${resInfo.cmdprefix}follow (amount/"all") [id/url]'`; + } else { + followText = `'${resInfo.cmdprefix}follow (amount/"all")'`; + } - if (commandHandler.data.config.yourgroup.length > 1) yourgroupText = commandHandler.data.lang.helpjoingroup.replace(/cmdprefix/g, resInfo.cmdprefix); + // Get amount user is allowed to request + let maxTotalComments = commandHandler.data.config.maxRequests; + if (owners.includes(resInfo.userID)) maxTotalComments = commandHandler.data.config.maxOwnerRequests; // Send message respond(` - ${commandHandler.data.datafile.mestr}'s Comment Bot | ${commandHandler.data.lang.helpcommandlist}\n - ${commentText}\n - '${resInfo.cmdprefix}ping' - ${commandHandler.data.lang.helpping} - '${resInfo.cmdprefix}info' - ${commandHandler.data.lang.helpinfo} - '${resInfo.cmdprefix}abort' - ${commandHandler.data.lang.helpabort} - '${resInfo.cmdprefix}about' - ${commandHandler.data.lang.helpabout} - '${resInfo.cmdprefix}owner' - ${commandHandler.data.lang.helpowner} - ${yourgroupText} + ${commandHandler.data.datafile.mestr}'s Comment Bot | ${await commandHandler.data.getLang("helpcommandlist", null, requesterID)}\n + ${commentText} + '${resInfo.cmdprefix}vote (amount/"all") (id/url)' - ${await commandHandler.data.getLang("helpvote", { "maxRequests": maxTotalComments }, requesterID)} + '${resInfo.cmdprefix}favorite (amount/"all") (id/url)' - ${await commandHandler.data.getLang("helpfavorite", { "maxRequests": maxTotalComments }, requesterID)} + ${followText} - ${await commandHandler.data.getLang("helpfollow", { "maxRequests": commandHandler.data.config.maxRequests }, requesterID)}\n + '${resInfo.cmdprefix}info' - ${await commandHandler.data.getLang("helpinfo", null, requesterID)} + '${resInfo.cmdprefix}abort' - ${await commandHandler.data.getLang("helpabort", null, requesterID)} + '${resInfo.cmdprefix}about' - ${await commandHandler.data.getLang("helpabout", null, requesterID)} + '${resInfo.cmdprefix}owner' - ${await commandHandler.data.getLang("helpowner", null, requesterID)} - ${commandHandler.data.lang.helpreadothercmdshere} ' https://github.com/3urobeat/steam-comment-service-bot/blob/master/docs/wiki/commands_doc.md ' + ${await commandHandler.data.getLang("helpreadothercmdshere", null, requesterID)} ' https://github.com/3urobeat/steam-comment-service-bot/blob/master/docs/wiki/commands_doc.md ' `.replace(/^( {4})+/gm, "")); // Remove all the whitespaces that are added by the proper code indentation here } }; @@ -144,7 +152,7 @@ module.exports.ping = { https.get("https://steamcommunity.com/ping", (res) => { // Ping steamcommunity.com/ping and measure time res.setEncoding("utf8"); res.on("data", () => {}); - res.on("end", () => respond(commandHandler.data.lang.pingcmdmessage.replace("pingtime", Date.now() - pingStart))); + res.on("end", async () => respond(await commandHandler.data.getLang("pingcmdmessage", { "pingtime": Date.now() - pingStart }, resInfo.userID))); }); } }; @@ -186,13 +194,13 @@ module.exports.owner = { * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). */ - run: (commandHandler, args, respondModule, context, resInfo) => { + run: async (commandHandler, args, respondModule, context, resInfo) => { let respond = ((txt) => respondModule(context, resInfo, txt)); // Shorten each call // Check if no owner link is set - if (commandHandler.data.config.owner.length < 1) return respond(commandHandler.data.lang.ownercmdnolink); + if (commandHandler.data.config.owner.length < 1) return respond(await commandHandler.data.getLang("ownercmdnolink", null, resInfo.userID)); - respond(commandHandler.data.lang.ownercmdmsg.replace(/cmdprefix/g, resInfo.cmdprefix) + "\n" + commandHandler.data.config.owner); + respond((await commandHandler.data.getLang("ownercmdmsg", { "cmdprefix": resInfo.cmdprefix }, resInfo.userID)) + "\n" + commandHandler.data.config.owner); } }; @@ -215,19 +223,118 @@ module.exports.test = { run: async (commandHandler, args, respondModule, context, resInfo) => { let respond = ((txt) => respondModule(context, resInfo, txt)); // eslint-disable-line - /* // Do not remove, these are handleSteamIdResolving test cases. Might be useful to include later in steamid-resolving lib test suite - let handleSteamIdResolving = commandHandler.controller.handleSteamIdResolving; + // Test steamcommunity follow & unfollow implementation + /* commandHandler.controller.main.community.followUser(args[0], (err, res) => { + if (err) return logger("", err, true); + + logger("", res, true); + }); + + setTimeout(() => { + commandHandler.controller.main.community.unfollowUser(args[0], (err, res) => { + if (err) return logger("", err, true); + + logger("", res, true); + }); + }, 10000); + + commandHandler.controller.main.community.followCurator(args[0], (err, res) => { + if (err) return logger("", err, true); + + logger("", res, true); + }); + + setTimeout(() => { + commandHandler.controller.main.community.unfollowCurator(args[0], (err, res) => { + if (err) return logger("", err, true); + + logger("", res, true); + }); + }, 10000); */ + + + // Test steamcommunity steam discussion implementation + // App Discussion with lots of comments: https://steamcommunity.com/app/739630/discussions/0/1750150652078713439 + // Forum Discussion I can post in with my test acc: https://steamcommunity.com/discussions/forum/24/3815167348912316274 + // Group discussion with no comment rights: https://steamcommunity.com/groups/SteamLabs/discussions/2/1643168364649277130/ + + /* commandHandler.controller.main.community.getSteamDiscussion(args[0], async (err, discussion) => { + if (err) { + respond("Error loading discussion: " + err); + return; + } + + logger("", discussion, true); + }); + + commandHandler.controller.main.community.getDiscussionComments("https://steamcommunity.com/app/739630/discussions/0/5904837854428568148", 0, null, (err, res) => { + logger("", res, true); + }); + + commandHandler.controller.main.community.postDiscussionComment("103582791432902485", "882957625821686010", "5291222404430243834", "bleh", (err, res) => { + logger("", res + " " + err, true); + }); + + setTimeout(() => { + commandHandler.controller.main.community.getDiscussionComments("https://steamcommunity.com/app/730/discussions/0/5291222404430243834/", 32, 32, (err, res) => { + let id = res[0].commentId; + + console.log(id); + + commandHandler.controller.main.community.deleteDiscussionComment("103582791432902485", "882957625821686010", "5291222404430243834", id, (err, res) => { + logger("", res + " " + err, true); + }); + }); + }, 5000); + + commandHandler.controller.main.community.getSteamDiscussion("https://steamcommunity.com/discussions/forum/24/3815167348912316274", (err, res) => { + if (err) logger("", err.stack, true); + else logger("", res, true); + }); + + commandHandler.controller.main.community.setDiscussionCommentsPerPage("30", (err) => { + logger("", err, true); + }); + + commandHandler.controller.main.community.getSteamDiscussion("https://steamcommunity.com/groups/SteamLabs/discussions/2/1643168364649277130/", (err, res) => { + if (err) logger("", err.stack, true); + else logger("", res, true); + }); + + commandHandler.controller.main.community.getSteamDiscussion("https://steamcommunity.com/app/739630/discussions/0/1750150652078713439/", (err, res) => { + res.getComments(35, 37, (err, res2) => { + console.log(res2); + }); + }); */ + + + // Test getLang(): + /* logger("", "1: " + await commandHandler.data.getLang("resetcooldowncmdsuccess", { "profileid": "1234" }), true); // Valid test cases + logger("", "2: " + await commandHandler.data.getLang("resetcooldowncmdsuccess", { "profileid": "1234" }, "russian"), true); + logger("", "3: " + await commandHandler.data.getLang("resetcooldowncmdsuccess", { "profileid": "1234" }, resInfo.userID), true); // Note: Make sure you exist in the database + + logger("", "4: " + await commandHandler.data.getLang("resetcooldowncmdsuccess", { "testvalue": "1234" }), true); // Invalid test cases + logger("", "5: " + await commandHandler.data.getLang("resetcooldowncmdsucces"), true); + logger("", "6: " + await commandHandler.data.getLang("resetcooldowncmdsuccess", { "profileid": "1234" }, "99827634"), true); */ + + + // Test handleSteamIdResolving(): + /* let handleSteamIdResolving = commandHandler.controller.handleSteamIdResolving; // With type param handleSteamIdResolving("3urobeat", "profile", console.log); handleSteamIdResolving("3urobeatGroup", "group", console.log); handleSteamIdResolving("2966606880", "sharedfile", console.log); + handleSteamIdResolving("https://steamcommunity.com/app/739630/discussions/0/1750150652078713439/", "discussion", console.log); // Link matching handleSteamIdResolving("https://steamcommunity.com/id/3urobeat", null, console.log); handleSteamIdResolving("https://steamcommunity.com/profiles/76561198260031749", null, console.log); handleSteamIdResolving("https://steamcommunity.com/groups/3urobeatGroup", null, console.log); handleSteamIdResolving("https://steamcommunity.com/sharedfiles/filedetails/?id=2966606880", null, console.log); + handleSteamIdResolving("https://steamcommunity.com/discussions/forum/24/3815167348912316274", null, console.log); + handleSteamIdResolving("https://steamcommunity.com/groups/SteamLabs/discussions/2/1643168364649277130/", null, console.log); + handleSteamIdResolving("https://steamcommunity.com/app/739630/discussions/0/1750150652078713439/", null, console.log); // We don't know, let helper figure it out handleSteamIdResolving("3urobeat", null, console.log); diff --git a/src/commands/core/group.js b/src/commands/core/group.js index f21962fa..cc702cc7 100644 --- a/src/commands/core/group.js +++ b/src/commands/core/group.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 10.07.2023 21:25:38 + * Last Modified: 07.10.2023 23:34:56 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -34,15 +34,15 @@ module.exports.group = { * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). */ - run: (commandHandler, args, respondModule, context, resInfo) => { + run: async (commandHandler, args, respondModule, context, resInfo) => { let respond = ((txt) => respondModule(context, resInfo, txt)); // Shorten each call - if (commandHandler.data.config.yourgroup.length < 1 || !commandHandler.data.cachefile.configgroup64id) return respond(commandHandler.data.lang.groupcmdnolink); // No group info at all? stop. + if (commandHandler.data.config.yourgroup.length < 1 || !commandHandler.data.cachefile.configgroup64id) return respond(await commandHandler.data.getLang("groupcmdnolink", null, resInfo.userID)); // No group info at all? stop. // Send user an invite if a group is set in the config and userID is a Steam ID by checking fromSteamChat if (resInfo.userID && resInfo.fromSteamChat && commandHandler.data.cachefile.configgroup64id && Object.keys(commandHandler.controller.main.user.myGroups).includes(commandHandler.data.cachefile.configgroup64id)) { commandHandler.controller.main.user.inviteToGroup(resInfo.userID, commandHandler.data.cachefile.configgroup64id); - respond(commandHandler.data.lang.groupcmdinvitesent); + respond(await commandHandler.data.getLang("groupcmdinvitesent", null, resInfo.userID)); if (commandHandler.data.cachefile.configgroup64id != "103582791464712227") { // https://steamcommunity.com/groups/3urobeatGroup commandHandler.controller.main.user.inviteToGroup(resInfo.userID, new SteamID("103582791464712227")); @@ -50,7 +50,7 @@ module.exports.group = { return; // Id? send invite and stop } - respond(commandHandler.data.lang.groupcmdinvitelink + commandHandler.data.config.yourgroup); // Seems like no id has been saved but an url. Send the user the url + respond((await commandHandler.data.getLang("groupcmdinvitelink", null, resInfo.userID)) + commandHandler.data.config.yourgroup); // Seems like no id has been saved but an url. Send the user the url } }; @@ -77,15 +77,16 @@ module.exports.joinGroup = { * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). */ - run: (commandHandler, args, respondModule, context, resInfo) => { + run: async (commandHandler, args, respondModule, context, resInfo) => { let respond = ((txt) => respondModule(context, resInfo, txt)); // Shorten each call + let requesterID = resInfo.userID; - if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.botnotready); // Check if bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained + if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("botnotready", null, requesterID)); // Check if bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained - if (isNaN(args[0]) && !String(args[0]).startsWith("https://steamcommunity.com/groups/")) return respond(commandHandler.data.lang.invalidgroupid); + if (isNaN(args[0]) && !String(args[0]).startsWith("https://steamcommunity.com/groups/")) return respond(await commandHandler.data.getLang("invalidgroupid", null, requesterID)); - commandHandler.controller.handleSteamIdResolving(args[0], "group", (err, id) => { - if (err) return respond(commandHandler.data.lang.invalidgroupid + "\n\nError: " + err); + commandHandler.controller.handleSteamIdResolving(args[0], "group", async (err, id) => { + if (err) return respond((await commandHandler.data.getLang("invalidgroupid", null, requesterID)) + "\n\nError: " + err); commandHandler.controller.getBots().forEach((e, i) => { setTimeout(() => { @@ -93,7 +94,7 @@ module.exports.joinGroup = { }, 1000 * i); // Delay every iteration so that we don't make a ton of requests at once }); - respond(commandHandler.data.lang.joingroupcmdsuccess.replace("groupid", id)); + respond(await commandHandler.data.getLang("joingroupcmdsuccess", { "groupid": id }, requesterID)); logger("info", `Joining group '${id}' with all bot accounts...`); }); } @@ -122,15 +123,16 @@ module.exports.leaveGroup = { * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). */ - run: (commandHandler, args, respondModule, context, resInfo) => { + run: async (commandHandler, args, respondModule, context, resInfo) => { let respond = ((txt) => respondModule(context, resInfo, txt)); // Shorten each call + let requesterID = resInfo.userID; - if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.botnotready); // Check if bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained + if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("botnotready", null, requesterID)); // Check if bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained - if (isNaN(args[0]) && !String(args[0]).startsWith("https://steamcommunity.com/groups/")) return respond(commandHandler.data.lang.invalidgroupid); + if (isNaN(args[0]) && !String(args[0]).startsWith("https://steamcommunity.com/groups/")) return respond(await commandHandler.data.getLang("invalidgroupid", null, requesterID)); - commandHandler.controller.handleSteamIdResolving(args[0], "group", (err, id) => { - if (err) return respond(commandHandler.data.lang.invalidgroupid + "\n\nError: " + err); + commandHandler.controller.handleSteamIdResolving(args[0], "group", async (err, id) => { + if (err) return respond((await commandHandler.data.getLang("invalidgroupid", null, requesterID)) + "\n\nError: " + err); commandHandler.controller.getBots().forEach((e, i) => { setTimeout(() => { @@ -138,7 +140,7 @@ module.exports.leaveGroup = { }, 1000 * i); // Delay every iteration so that we don't make a ton of requests at once }); - respond(commandHandler.data.lang.leavegroupcmdsuccess.replace("groupid", id)); + respond(await commandHandler.data.getLang("leavegroupcmdsuccess", { "groupid": id }, requesterID)); logger("info", `Leaving group ${id} with all bot accounts.`); }); } @@ -167,25 +169,26 @@ module.exports.leaveAllGroups = { * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). */ - run: (commandHandler, args, respondModule, context, resInfo) => { + run: async (commandHandler, args, respondModule, context, resInfo) => { let respond = ((txt) => respondModule(context, resInfo, txt)); // Shorten each call + let requesterID = resInfo.userID; - if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.botnotready); // Check if bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained + if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("botnotready", null, requesterID)); // Check if bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained // TODO: This is bad. Rewrite using a message collector, maybe add one to steamChatInteraction helper var abortleaveallgroups; // eslint-disable-line no-var if (args[0] == "abort") { - respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.leaveallgroupscmdabort); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained + respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("leaveallgroupscmdabort", null, requesterID)); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained return abortleaveallgroups = true; } abortleaveallgroups = false; - respond(commandHandler.data.lang.leaveallgroupscmdpending.replace(/cmdprefix/g, resInfo.cmdprefix)); + respond(await commandHandler.data.getLang("leaveallgroupscmdpending", { "cmdprefix": resInfo.cmdprefix }, requesterID)); - setTimeout(() => { + setTimeout(async () => { if (abortleaveallgroups) return logger("info", "leaveallgroups process was aborted."); - respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.leaveallgroupscmdstart); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained + respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("leaveallgroupscmdstart", null, requesterID)); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained logger("info", "Starting to leave all groups..."); for (let i = 0; i < commandHandler.controller.getBots().length; i++) { diff --git a/src/commands/core/requests.js b/src/commands/core/requests.js index cf841bac..6917f8ef 100644 --- a/src/commands/core/requests.js +++ b/src/commands/core/requests.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 10.07.2023 13:04:06 + * Last Modified: 19.10.2023 19:00:06 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -41,16 +41,18 @@ module.exports.abort = { * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). */ - run: (commandHandler, args, respondModule, context, resInfo) => { + run: async (commandHandler, args, respondModule, context, resInfo) => { let respond = ((txt) => respondModule(context, resInfo, txt)); // Shorten each call - if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.botnotready); // Check if bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained + let requesterID = resInfo.userID; + + if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("botnotready", null, requesterID)); // Check if bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained let userID = resInfo.userID; // Check for no userID and no id param as both can be missing if called from outside the Steam Chat - if (!userID && !args[0]) return respond(commandHandler.data.lang.noidparam); + if (!userID && !args[0]) return respond(await commandHandler.data.getLang("noidparam", null, requesterID)); - commandHandler.controller.handleSteamIdResolving(args[0], null, (err, res) => { + commandHandler.controller.handleSteamIdResolving(args[0], null, async (err, res) => { if (res) { let activeReqEntry = commandHandler.controller.activeRequests[res]; @@ -59,19 +61,19 @@ module.exports.abort = { if (resInfo.ownerIDs && resInfo.ownerIDs.length > 0) owners = resInfo.ownerIDs; // Refuse if user is not an owner and the request is not from them - if (!owners.includes(resInfo.userID) && (activeReqEntry && activeReqEntry.requestedby != resInfo.userID)) return respond(commandHandler.data.lang.commandowneronly); + if (!owners.includes(resInfo.userID) && (activeReqEntry && activeReqEntry.requestedby != resInfo.userID)) return respond(await commandHandler.data.getLang("commandowneronly", null, requesterID)); else logger("debug", "CommandHandler abort cmd: Non-owner provided ID as parameter but is requester of that request. Permitting abort..."); userID = res; // If user provided an id as argument then use that instead of their id } - if (!commandHandler.controller.activeRequests[userID] || commandHandler.controller.activeRequests[userID].status != "active") return respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.abortcmdnoprocess); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained + if (!commandHandler.controller.activeRequests[userID] || commandHandler.controller.activeRequests[userID].status != "active") return respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("abortcmdnoprocess", null, requesterID)); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained // Set new status for this request commandHandler.controller.activeRequests[userID].status = "aborted"; logger("info", `Aborting active process for ID ${userID}...`); - respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.abortcmdsuccess); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained + respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("abortcmdsuccess", null, requesterID)); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained }); } }; @@ -99,34 +101,35 @@ module.exports.resetCooldown = { * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). */ - run: (commandHandler, args, respondModule, context, resInfo) => { + run: async (commandHandler, args, respondModule, context, resInfo) => { let respond = ((txt) => respondModule(context, resInfo, txt)); // Shorten each call + let requesterID = resInfo.userID; if (args[0] && args[0] == "global") { // Check if user wants to reset the global cooldown (will reset all until entries in activeRequests) - if (commandHandler.data.config.botaccountcooldown == 0) return respond(commandHandler.data.lang.resetcooldowncmdcooldowndisabled); // Is the global cooldown enabled? + if (commandHandler.data.config.botaccountcooldown == 0) return respond(await commandHandler.data.getLang("resetcooldowncmdcooldowndisabled", null, requesterID)); // Is the global cooldown enabled? Object.keys(commandHandler.controller.activeRequests).forEach((e) => { commandHandler.controller.activeRequests[e].until = Date.now() - (commandHandler.data.config.botaccountcooldown * 60000); // Since the cooldown checks will add the cooldown we need to subtract it (can't delete the entry because we might abort running processes with it) }); - respond(commandHandler.data.lang.resetcooldowncmdglobalreset); + respond(await commandHandler.data.getLang("resetcooldowncmdglobalreset", null, requesterID)); } else { let userID = resInfo.userID; // Check for no userID and no id param as both can be missing if called from outside the Steam Chat - if (!userID && !args[0]) return respond(commandHandler.data.lang.noidparam); + if (!userID && !args[0]) return respond(await commandHandler.data.getLang("noidparam", null, requesterID)); - commandHandler.controller.handleSteamIdResolving(args[0], "profile", (err, res) => { - if (err) return respond(commandHandler.data.lang.invalidprofileid + "\n\nError: " + err); + commandHandler.controller.handleSteamIdResolving(args[0], "profile", async (err, res) => { + if (err) return respond((await commandHandler.data.getLang("invalidprofileid", null, requesterID)) + "\n\nError: " + err); if (res) userID = res; // Change steamID64 to the provided id - if (commandHandler.data.config.commentcooldown == 0) return respond(commandHandler.data.lang.resetcooldowncmdcooldowndisabled); // Is the cooldown enabled? + if (commandHandler.data.config.requestCooldown == 0) return respond(await commandHandler.data.getLang("resetcooldowncmdcooldowndisabled", null, requesterID)); // Is the cooldown enabled? - commandHandler.data.lastCommentDB.update({ id: userID }, { $set: { time: Date.now() - (commandHandler.data.config.commentcooldown * 60000) } }, (err) => { + commandHandler.data.lastCommentDB.update({ id: userID }, { $set: { time: Date.now() - (commandHandler.data.config.requestCooldown * 60000) } }, async (err) => { if (err) return respond("Error updating database entry: " + err); - else respond(commandHandler.data.lang.resetcooldowncmdsuccess.replace("profileid", userID.toString())); + else respond(await commandHandler.data.getLang("resetcooldowncmdsuccess", { "profileid": userID.toString() }, requesterID)); }); }); } @@ -156,15 +159,15 @@ module.exports.failed = { * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). */ - run: (commandHandler, args, respondModule, context, resInfo) => { + run: async (commandHandler, args, respondModule, context, resInfo) => { let respond = ((txt) => respondModule(context, resInfo, txt)); // Shorten each call let userID = resInfo.userID; // Check for no userID and no id param as both can be missing if called from outside the Steam Chat - if (!userID && !args[0]) return respond(commandHandler.data.lang.noidparam); + if (!userID && !args[0]) return respond(await commandHandler.data.getLang("noidparam", null, resInfo.userID)); - commandHandler.controller.handleSteamIdResolving(args[0], null, (err, res) => { + commandHandler.controller.handleSteamIdResolving(args[0], null, async (err, res) => { if (res) { let activeReqEntry = commandHandler.controller.activeRequests[res]; @@ -173,13 +176,13 @@ module.exports.failed = { if (resInfo.ownerIDs && resInfo.ownerIDs.length > 0) owners = resInfo.ownerIDs; // Refuse if user is not an owner and the request is not from them - if (!owners.includes(userID) && (activeReqEntry && activeReqEntry.requestedby != userID)) return respond(commandHandler.data.lang.commandowneronly); + if (!owners.includes(userID) && (activeReqEntry && activeReqEntry.requestedby != userID)) return respond(await commandHandler.data.getLang("commandowneronly", null, resInfo.userID)); else logger("debug", "CommandHandler failed cmd: Non-owner provided ID as parameter but is requester of that request. Permitting data retrieval..."); userID = res; // If user provided an id as argument then use that instead of their id } - if (!commandHandler.controller.activeRequests[userID] || Object.keys(commandHandler.controller.activeRequests[userID].failed).length < 1) return respond(commandHandler.data.lang.failedcmdnothingfound); + if (!commandHandler.controller.activeRequests[userID] || Object.keys(commandHandler.controller.activeRequests[userID].failed).length < 1) return respond(await commandHandler.data.getLang("failedcmdnothingfound", null, resInfo.userID)); // Get timestamp of request let requestTime = new Date(commandHandler.controller.activeRequests[userID].until).toISOString().replace(/T/, " ").replace(/\..+/, ""); @@ -188,7 +191,7 @@ module.exports.failed = { let failedcommentsstr = failedCommentsObjToString(commandHandler.controller.activeRequests[userID].failed); // Get start of message from lang file and add data - let messagestart = commandHandler.data.lang.failedcmdmsg.replace("steamID64", userID).replace("requesttime", requestTime); + let messagestart = await commandHandler.data.getLang("failedcmdmsg", { "steamID64": userID, "requesttime": requestTime }, resInfo.userID); // Send message and limit to 500 chars as this call can cause many messages to be sent respondModule(context, { prefix: "/pre", charLimit: 500, ...resInfo }, messagestart + "\nc = Comment, b = Bot, p = Proxy\n\n" + failedcommentsstr); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained @@ -211,7 +214,7 @@ module.exports.sessions = { * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). */ - run: (commandHandler, args, respondModule, context, resInfo) => { + run: async (commandHandler, args, respondModule, context, resInfo) => { let respond = ((txt) => respondModule(context, resInfo, txt)); // Shorten each call let str = ""; @@ -219,7 +222,7 @@ module.exports.sessions = { if (Object.keys(commandHandler.controller.activeRequests).length > 0) { // Only loop through object if it isn't empty let objlength = Object.keys(commandHandler.controller.activeRequests).length; // Save this before the loop as deleting entries will change this number and lead to the loop finished check never triggering - Object.keys(commandHandler.controller.activeRequests).forEach((e, i) => { + Object.keys(commandHandler.controller.activeRequests).forEach(async (e, i) => { if (Date.now() < commandHandler.controller.activeRequests[e].until + (commandHandler.data.config.botaccountcooldown * 60000)) { // Check if entry is not finished yet str += `- Status: ${commandHandler.controller.activeRequests[e].status} | ${commandHandler.controller.activeRequests[e].amount} iterations with ${commandHandler.controller.activeRequests[e].accounts.length} accounts by ${commandHandler.controller.activeRequests[e].requestedby} for ${commandHandler.controller.activeRequests[e].type} ${Object.keys(commandHandler.controller.activeRequests)[i]}\n`; } else { @@ -228,14 +231,14 @@ module.exports.sessions = { if (i == objlength - 1) { if (Object.keys(commandHandler.controller.activeRequests).length > 0) { // Check if obj is still not empty - respond(commandHandler.data.lang.sessionscmdmsg.replace("amount", Object.keys(commandHandler.controller.activeRequests).length) + "\n" + str); + respond((await commandHandler.data.getLang("sessionscmdmsg", { "amount": Object.keys(commandHandler.controller.activeRequests).length }, resInfo.userID)) + "\n" + str); } else { - respond(commandHandler.data.lang.sessionscmdnosessions); + respond(await commandHandler.data.getLang("sessionscmdnosessions", null, resInfo.userID)); } } }); } else { - respond(commandHandler.data.lang.sessionscmdnosessions); + respond(await commandHandler.data.getLang("sessionscmdnosessions", null, resInfo.userID)); } } }; @@ -255,17 +258,17 @@ module.exports.mySessions = { * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). */ - run: (commandHandler, args, respondModule, context, resInfo) => { + run: async (commandHandler, args, respondModule, context, resInfo) => { let respond = ((txt) => respondModule(context, resInfo, txt)); // Shorten each call let str = ""; // Check for no userID as the default behavior might be unavailable when calling from outside of the Steam Chat - if (!resInfo.userID) return respond(commandHandler.data.lang.nouserid); // In this case the cmd doesn't have an ID param so send this message instead of noidparam + if (!resInfo.userID) return respond(await commandHandler.data.getLang("nouserid")); // In this case the cmd doesn't have an ID param so send this message instead of noidparam if (Object.keys(commandHandler.controller.activeRequests).length > 0) { // Only loop through object if it isn't empty let objlength = Object.keys(commandHandler.controller.activeRequests).length; // Save this before the loop as deleting entries will change this number and lead to the loop finished check never triggering - Object.keys(commandHandler.controller.activeRequests).forEach((e, i) => { + Object.keys(commandHandler.controller.activeRequests).forEach(async (e, i) => { if (Date.now() < commandHandler.controller.activeRequests[e].until + (commandHandler.data.config.botaccountcooldown * 60000)) { // Check if entry is not finished yet if (commandHandler.controller.activeRequests[e].requestedby == resInfo.userID) str += `- Status: ${commandHandler.controller.activeRequests[e].status} | ${commandHandler.controller.activeRequests[e].amount} iterations with ${commandHandler.controller.activeRequests[e].accounts.length} accounts by ${commandHandler.controller.activeRequests[e].requestedby} for ${commandHandler.controller.activeRequests[e].type} ${Object.keys(commandHandler.controller.activeRequests)[i]}`; } else { @@ -275,15 +278,15 @@ module.exports.mySessions = { if (i == objlength - 1) { if (i == objlength - 1) { if (Object.keys(commandHandler.controller.activeRequests).length > 0) { // Check if obj is still not empty - respond(commandHandler.data.lang.sessionscmdmsg.replace("amount", Object.keys(commandHandler.controller.activeRequests).length) + "\n" + str); + respond((await commandHandler.data.getLang("sessionscmdmsg", { "amount": Object.keys(commandHandler.controller.activeRequests).length }, resInfo.userID)) + "\n" + str); } else { - respond(commandHandler.data.lang.mysessionscmdnosessions); + respond(await commandHandler.data.getLang("mysessionscmdnosessions", null, resInfo.userID)); } } } }); } else { - respond(commandHandler.data.lang.mysessionscmdnosessions); + respond(await commandHandler.data.getLang("mysessionscmdnosessions", null, resInfo.userID)); } } }; \ No newline at end of file diff --git a/src/commands/core/settings.js b/src/commands/core/settings.js index 2688dcc2..1c8b9a6f 100644 --- a/src/commands/core/settings.js +++ b/src/commands/core/settings.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 26.07.2023 19:05:38 + * Last Modified: 18.10.2023 23:07:24 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -15,11 +15,68 @@ */ -const fs = require("fs"); - const CommandHandler = require("../commandHandler.js"); // eslint-disable-line +module.exports.lang = { + names: ["lang", "setlang"], + description: "Changes the language the bot will reply to you in. Call without params to see all supported languages.", + args: [ + { + name: "language", + description: "Name of the language", + type: "string", + isOptional: true, + ownersOnly: false + } + ], + ownersOnly: false, + + /** + * The lang command + * @param {CommandHandler} commandHandler The commandHandler object + * @param {Array} args Array of arguments that will be passed to the command + * @param {function(object, object, string): void} respondModule Function that will be called to respond to the user's request. Passes context, resInfo and txt as parameters. + * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. + * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). + */ + run: async (commandHandler, args, respondModule, context, resInfo) => { + let respond = ((txt) => respondModule(context, resInfo, txt)); // Shorten each call + + // List all supported languages by joining the keys of the data lang object with a line break and - + if (!args[0]) { + respond(`${await commandHandler.data.getLang("langcmdsupported", null, resInfo.userID)}\n- ${Object.keys(commandHandler.data.lang).join("\n - ")}`); + return; + } + + let suppliedLang = args[0].toLowerCase(); + + // Check if the supplied language is supported + if (!Object.keys(commandHandler.data.lang).includes(suppliedLang)) { + respond(await commandHandler.data.getLang("langcmdnotsupported", { "supportedlangs": "\n- " + Object.keys(commandHandler.data.lang).join("\n - ") }, resInfo.userID)); + return; + } + + // Check if command was called without a userid and reject database write + if (!resInfo.userID) { + respond(await commandHandler.data.getLang("nouserid")); // Reject usage of command without an userID to avoid cooldown bypass + return logger("err", "The lang command was called without resInfo.userID! Blocking the command as I'm unable to attribute the lang change to a user, which is required for this database write!"); + } + + // Upsert database record + commandHandler.data.userSettingsDB.update({ id: resInfo.userID }, { $set: { lang: suppliedLang } }, { upsert: true }, async (err) => { + if (err) { + respond("Error: Couldn't write to database! Please check the log for an error stacktrace."); + logger("error", "Failed to write language change to userSettings database!\nError: " + err); + return; + } + + respond(await commandHandler.data.getLang("langcmdsuccess", null, suppliedLang)); + }); + } +}; + + module.exports.settings = { names: ["settings", "set", "config"], description: "Change a value in the config", @@ -49,7 +106,7 @@ module.exports.settings = { * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). */ - run: (commandHandler, args, respondModule, context, resInfo) => { + run: async (commandHandler, args, respondModule, context, resInfo) => { let respond = ((txt) => respondModule(context, resInfo, txt)); // Shorten each call let config = commandHandler.data.config; @@ -68,7 +125,7 @@ module.exports.settings = { let currentsettingsarr = stringifiedconfig.toString().slice(1, -1).split("\n").map(s => s.trim()); // Send message with code prefix and only allow cuts at newlines - respondModule(context, { prefix: "/code", cutChars: ["\n"], ...resInfo }, commandHandler.data.lang.settingscmdcurrentsettings + "\n" + currentsettingsarr.join("\n")); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained + respondModule(context, { prefix: "/code", cutChars: ["\n"], ...resInfo }, (await commandHandler.data.getLang("settingscmdcurrentsettings", null, resInfo.userID)) + "\n" + currentsettingsarr.join("\n")); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained return; } @@ -79,28 +136,28 @@ module.exports.settings = { // Block those 3 values to don't allow another owner to take over ownership if (args[0] == "enableevalcmd" || args[0] == "ownerid" || args[0] == "owner") { - return respond(commandHandler.data.lang.settingscmdblockedvalues); + respond(await commandHandler.data.getLang("settingscmdblockedvalues", null, resInfo.userID)); + return; } let keyvalue = config[args[0]]; // Save old value to be able to reset changes - // I'm not proud of this code but whatever -> used to convert array into usable array + + // Convert array-like string into usable array if (Array.isArray(keyvalue)) { - let newarr = []; + try { + let newValue = args.slice(1).join(" "); // Remove first element, which is the key name and join the rest - args.forEach((e, i) => { - if (i == 0) return; // Skip args[0] - if (i == 1) e = e.slice(1); // Remove first char which is a [ - if (i == args.length - 1) e = e.slice(0, -1); // Remove last char which is a ] + args[1] = JSON.parse(newValue); // Attempt to parse user input - e = e.replace(/,/g, ""); // Remove , - if (e.startsWith('"')) newarr[i - 1] = String(e.replace(/"/g, "")); - else newarr[i - 1] = Number(e); - }); + } catch (err) { // Abort if user input contains issues - args[1] = newarr; + respond(await commandHandler.data.getLang("settingscmdcouldnotconvert", null, resInfo.userID) + err); + return; + } } + // Convert to number or boolean as input is always a String if (typeof(keyvalue) == "number") args[1] = Number(args[1]); if (typeof(keyvalue) == "boolean") { // Prepare for stupid code because doing Boolean(value) will always return true @@ -108,32 +165,56 @@ module.exports.settings = { if (args[1] == "false") args[1] = false; // Could have been worse tbh } - // Round maxComments value in order to avoid the possibility of weird amounts - if (args[0] == "maxComments" || args[0] == "maxOwnerComments") args[1] = Math.round(args[1]); + // Round maxRequests value in order to avoid the possibility of weird amounts + if (args[0] == "maxRequests" || args[0] == "maxOwnerRequests") args[1] = Math.round(args[1]); - if (keyvalue == undefined) return respond(commandHandler.data.lang.settingscmdkeynotfound); - if (keyvalue == args[1]) return respond(commandHandler.data.lang.settingscmdsamevalue.replace("value", args[1])); + if (keyvalue == undefined) return respond(await commandHandler.data.getLang("settingscmdkeynotfound", null, resInfo.userID)); + if (keyvalue == args[1]) return respond(await commandHandler.data.getLang("settingscmdsamevalue", { "value": args[1] }, resInfo.userID)); config[args[0]] = args[1]; // Apply changes - // 32-bit integer limit check from controller.js's startup checks - if (typeof(keyvalue) == "number" && config.commentdelay * config.maxComments > 2147483647 || typeof(keyvalue) == "number" && config.commentdelay * config.maxOwnerComments > 2147483647) { // Check this here after the key has been set and reset the changes if it should be true - config[args[0]] = keyvalue; - return respond(commandHandler.data.lang.settingscmdvaluetoobig); // Just using the check from controller.js - } + // Run dataCheck to verify updated settings + commandHandler.data.checkData() + .then(async (res) => { + if (res) { + respond(await commandHandler.data.getLang("settingscmdvaluereset", null, resInfo.userID) + "\n" + res); + logger("warn", `DataManager rejected change of '${args[0]}' to '${args[1]}' with this reason:\n` + res); + return; + } - respond(commandHandler.data.lang.settingscmdvaluechanged.replace("targetkey", args[0]).replace("oldvalue", keyvalue).replace("newvalue", args[1]).replace(/cmdprefix/g, resInfo.cmdprefix)); - logger("info", `${args[0]} has been changed from ${keyvalue} to ${args[1]}.`); + respond(await commandHandler.data.getLang("settingscmdvaluechanged", { "targetkey": args[0], "oldvalue": keyvalue, "newvalue": args[1], "cmdprefix": resInfo.cmdprefix }, resInfo.userID)); + logger("info", `${args[0]} has been changed from ${keyvalue} to ${args[1]}.`); - if (args[0] == "playinggames") { - logger("info", "Refreshing game status of all bot accounts..."); - commandHandler.controller.getBots().forEach((e) => { - if (e.index == 0) e.user.gamesPlayed(config.playinggames); // Set game only for the main bot - if (e.index != 0 && config.childaccsplaygames) e.user.gamesPlayed(config.playinggames.slice(1, config.playinggames.length)); // Play game with child bots but remove the custom game - }); - } + if (args[0] == "playinggames") { + logger("info", "Refreshing game status of all bot accounts..."); + + commandHandler.controller.getBots().forEach((e) => { + if (e.index == 0) e.user.gamesPlayed(config.playinggames); // Set game only for the main bot + + if (e.index != 0 && config.childaccsplaygames) { // Set game for child accounts - // Update config.json - commandHandler.data.writeConfigToDisk(); + // Check if user provided games specifically for this account. We only need to check this for child accounts + let configChildGames = config.childaccplayinggames; + + if (typeof configChildGames[0] == "object") { + if (Object.keys(configChildGames[0]).includes(e.loginData.logOnOptions.accountName)) configChildGames = configChildGames[0][e.loginData.logOnOptions.accountName]; // Get the specific settings for this account if included + else configChildGames = configChildGames.slice(1); // ...otherwise remove object containing acc specific settings to use the generic ones + + logger("debug", `settings: Setting includes specific games for ${e.logPrefix}, filtered for this account: ${configChildGames.join(", ")}`); + } + + e.user.gamesPlayed(configChildGames); + } + }); + } + + // Update config.json + commandHandler.data.writeConfigToDisk(); + }) + .catch(async (err) => { + respond(await commandHandler.data.getLang("settingscmdvaluereset", null, resInfo.userID) + "\n" + err); + logger("error", `DataManager rejected change of '${args[0]}' to '${args[1]}' with this reason:\n` + err); + return; + }); } }; \ No newline at end of file diff --git a/src/commands/core/system.js b/src/commands/core/system.js index 117bd42e..fe5b22d8 100644 --- a/src/commands/core/system.js +++ b/src/commands/core/system.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 26.07.2023 16:42:30 + * Last Modified: 21.10.2023 12:28:46 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -34,8 +34,8 @@ module.exports.restart = { * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). */ - run: (commandHandler, args, respondModule, context, resInfo) => { - respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.restartcmdrestarting); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained + run: async (commandHandler, args, respondModule, context, resInfo) => { + respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("restartcmdrestarting", null, resInfo.userID)); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained commandHandler.controller.restart(JSON.stringify({ skippedaccounts: commandHandler.controller.info.skippedaccounts })); } @@ -56,8 +56,8 @@ module.exports.stop = { * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). */ - run: (commandHandler, args, respondModule, context, resInfo) => { - respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.stopcmdstopping); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained + run: async (commandHandler, args, respondModule, context, resInfo) => { + respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("stopcmdstopping", null, resInfo.userID)); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained commandHandler.controller.stop(); } @@ -90,7 +90,7 @@ module.exports.reload = { await commandHandler.data._importFromDisk(); // Send response message - respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.reloadcmdreloaded); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained + respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("reloadcmdreloaded", null, resInfo.userID)); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained } }; @@ -118,15 +118,14 @@ module.exports.update = { * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). */ - run: (commandHandler, args, respondModule, context, resInfo) => { + run: async (commandHandler, args, respondModule, context, resInfo) => { let respond = ((modResInfo, txt) => respondModule(context, modResInfo, txt)); // Shorten each call. Updater takes resInfo as param and can modify it, so we need to pass the modified resInfo object here - // If the first argument is true then we shall force an update - let force = (args[0] == "true"); + // If the first argument is "true" or "force" then we shall force an update + let force = (args[0] == "true" || args[0] == "force"); - // Use the correct message depending on if force is true or false - if (force) respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.updatecmdforce.replace("branchname", commandHandler.data.datafile.branch)); - else respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.updatecmdcheck.replace("branchname", commandHandler.data.datafile.branch)); + // Use the correct message depending on if force is true or false with a ternary operator + respond({ prefix: "/me", ...resInfo }, await commandHandler.data.getLang(force ? "updatecmdforce" : "updatecmdcheck", { "branchname": commandHandler.data.datafile.branch }, resInfo.userID)); // Run the updater, pass force and our respond function which will allow the updater to text the user what's going on commandHandler.controller.updater.run(force, respond, resInfo); @@ -181,9 +180,13 @@ module.exports.eval = { * @param {object} context The context (this.) of the object calling this command. Will be passed to respondModule() as first parameter. * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). */ - run: (commandHandler, args, respondModule, context, resInfo) => { + run: async (commandHandler, args, respondModule, context, resInfo) => { let respond = ((txt) => respondModule(context, resInfo, txt)); // Shorten each call - if (!commandHandler.data.advancedconfig.enableevalcmd) return respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.evalcmdturnedoff); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained + + if (!commandHandler.data.advancedconfig.enableevalcmd) { + respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("evalcmdturnedoff", null, resInfo.userID)); // Pass new resInfo object which contains prefix and everything the original resInfo obj contained + return; + } const clean = text => { // eslint-disable-line no-case-declarations if (typeof(text) === "string") return text.replace(/`/g, "`" + String.fromCharCode(8203)).replace(/@/g, "@" + String.fromCharCode(8203)); @@ -192,28 +195,34 @@ module.exports.eval = { try { const code = args.join(" "); - if (code.includes("logininfo")) return respond(commandHandler.data.lang.evalcmdlogininfoblock); // Not 100% safe but should be at least some protection (only owners can use this cmd) + if (code.includes("logininfo") || code.includes("logOnOptions") || code.includes("tokensDB")) return respond(await commandHandler.data.getLang("evalcmdlogininfoblock", null, resInfo.userID)); // Not 100% safe but should prevent accidental leaks (only owners can use this cmd) // Make using the command a little bit easier let controller = commandHandler.controller; // eslint-disable-line let main = commandHandler.controller.main; // eslint-disable-line let data = commandHandler.data; // eslint-disable-line + // Run code & convert to string let evaled = eval(code); if (typeof evaled !== "string") evaled = require("util").inspect(evaled); + // Sanitize result to filter logindata. This is not 100% safe but should prevent accidental leaks (only owners can use this cmd) + commandHandler.data.logininfo.forEach((e) => { + evaled = evaled.replace(new RegExp(e.password, "g"), "\"censored\""); + }); + // Check for character limit and cut message let chatResult = clean(evaled); - if (chatResult.length >= 500) respond(`Code executed. Result:\n\n${chatResult.slice(0, 500)}.......\n\n\nResult too long for chat.`); - else respond(`Code executed. Result:\n\n${clean(evaled)}`); + if (chatResult.length >= 500) respond(`Code executed. Result:\n\n${chatResult.slice(0, 500)}.......\n\nResult too long for chat.`); + else respond(`Code executed. Result:\n\n${chatResult}`); - logger("info", `${logger.colors.fgyellow}Eval result:${logger.colors.reset} \n${clean(evaled)}\n`, true); + logger("info", `${logger.colors.fgyellow}Eval result:${logger.colors.reset} \n${clean(evaled)}`, true); } catch (err) { respond(`Error:\n${clean(err)}`); - logger("error", `${logger.colors.fgyellow}Eval error:${logger.colors.reset} \n${clean(err)}\n`, true); // Hi I'm a comment that serves no purpose + logger("error", `${logger.colors.fgyellow}Eval error:${logger.colors.reset} \n${clean(err)}`, true); // Hi I'm a comment that serves no purpose return; } } diff --git a/src/commands/core/vote.js b/src/commands/core/vote.js index ff87fd1f..f3f351e7 100644 --- a/src/commands/core/vote.js +++ b/src/commands/core/vote.js @@ -4,7 +4,7 @@ * Created Date: 28.05.2023 12:02:24 * Author: 3urobeat * - * Last Modified: 24.07.2023 19:42:37 + * Last Modified: 19.10.2023 19:00:06 * Modified By: 3urobeat * * Copyright (c) 2023 3urobeat @@ -58,18 +58,18 @@ module.exports.upvote = { let owners = commandHandler.data.cachefile.ownerid; if (resInfo.ownerIDs && resInfo.ownerIDs.length > 0) owners = resInfo.ownerIDs; - let requesterSteamID64 = resInfo.userID; - let ownercheck = owners.includes(requesterSteamID64); + let requesterID = resInfo.userID; + let ownercheck = owners.includes(requesterID); /* --------- Various checks --------- */ if (!resInfo.userID) { - respond(commandHandler.data.lang.nouserid); // Reject usage of command without an userID to avoid cooldown bypass + respond(await commandHandler.data.getLang("nouserid")); // Reject usage of command without an userID to avoid cooldown bypass return logger("err", "The upvote command was called without resInfo.userID! Blocking the command as I'm unable to apply cooldowns, which is required for this command!"); } - if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.botnotready); // Bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained - if (commandHandler.controller.info.activeLogin) return respond(commandHandler.data.lang.activerelog); // Bot is waiting for relog - if (commandHandler.data.config.maxComments == 0 && !ownercheck) return respond(commandHandler.data.lang.commandowneronly); // Command is restricted to owners only + if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("botnotready", null, requesterID)); // Bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained + if (commandHandler.controller.info.activeLogin) return respond(await commandHandler.data.getLang("activerelog", null, requesterID)); // Bot is waiting for relog + if (commandHandler.data.config.maxRequests == 0 && !ownercheck) return respond(await commandHandler.data.getLang("commandowneronly", null, requesterID)); // Command is restricted to owners only // Check and get arguments from user @@ -81,49 +81,49 @@ module.exports.upvote = { // Check if this id is already receiving something right now let idReq = commandHandler.controller.activeRequests[id]; - if (idReq && idReq.status == "active") return respond(commandHandler.data.lang.idalreadyreceiving); // Note: No need to check for user as that is supposed to be handled by a cooldown + if (idReq && idReq.status == "active") return respond(await commandHandler.data.getLang("idalreadyreceiving", null, requesterID)); // Note: No need to check for user as that is supposed to be handled by a cooldown // Check if user has cooldown - let { until, untilStr } = await commandHandler.data.getUserCooldown(requesterSteamID64); + let { until, untilStr } = await commandHandler.data.getUserCooldown(requesterID); - if (until > Date.now()) return respond(commandHandler.data.lang.idoncooldown.replace("remainingcooldown", untilStr)); + if (until > Date.now()) return respond(await commandHandler.data.getLang("idoncooldown", { "remainingcooldown": untilStr }, requesterID)); // Get all available bot accounts let { amount, availableAccounts, whenAvailableStr } = await getAvailableBotsForVoting(commandHandler, amountRaw, id, "upvote"); if ((availableAccounts.length < amount || availableAccounts.length == 0) && !whenAvailableStr) { // Check if this bot has not enough accounts suitable for this request and there won't be more available at any point. - if (availableAccounts.length == 0) respond(commandHandler.data.lang.votenoaccounts); // The < || == 0 check is intentional, as providing "all" will set amount to 0 if 0 accounts have been found - else respond(commandHandler.data.lang.voterequestless.replace("availablenow", availableAccounts.length)); + if (availableAccounts.length == 0) respond(await commandHandler.data.getLang("genericnoaccounts", null, requesterID)); // The < || == 0 check is intentional, as providing "all" will set amount to 0 if 0 accounts have been found + else respond(await commandHandler.data.getLang("genericrequestless", { "availablenow": availableAccounts.length }, requesterID)); return; } if (availableAccounts.length < amount) { // Check if not enough available accounts were found because of cooldown - respond(commandHandler.data.lang.votenotenoughavailableaccs.replace("waittime", whenAvailableStr).replace("availablenow", availableAccounts.length)); + respond(await commandHandler.data.getLang("genericnotenoughavailableaccs", { "waittime": whenAvailableStr, "availablenow": availableAccounts.length }, requesterID)); return; } // Get the sharedfile - commandHandler.controller.main.community.getSteamSharedFile(id, (err, sharedfile) => { + commandHandler.controller.main.community.getSteamSharedFile(id, async (err, sharedfile) => { if (err) { - respond(commandHandler.data.lang.errloadingsharedfile + err); + respond((await commandHandler.data.getLang("errloadingsharedfile", null, requesterID)) + err); return; } - // Register this vote process in activeRequests. We use commentdelay here for now, not sure if I'm going to add a separate setting + // Register this vote process in activeRequests commandHandler.controller.activeRequests[id] = { status: "active", type: "upvote", amount: amount, - requestedby: requesterSteamID64, + requestedby: requesterID, accounts: availableAccounts, thisIteration: -1, // Set to -1 so that first iteration will increase it to 0 retryAttempt: 0, - until: Date.now() + ((amount - 1) * commandHandler.data.config.commentdelay), // Calculate estimated wait time (first vote is instant -> remove 1 from numberOfComments) + until: Date.now() + ((amount - 1) * commandHandler.data.config.requestDelay), // Calculate estimated wait time (first vote is instant -> remove 1 from numberOfComments) failed: {} }; @@ -136,13 +136,13 @@ module.exports.upvote = { // Only send estimated wait time message for multiple votes if (activeReqEntry.amount > 1) { - let waitTime = timeToString(Date.now() + ((activeReqEntry.amount - 1) * commandHandler.data.config.commentdelay)); // Amount - 1 because the first vote is instant. Multiply by delay and add to current time to get timestamp when last vote was sent + let waitTime = timeToString(Date.now() + ((activeReqEntry.amount - 1) * commandHandler.data.config.requestDelay)); // Amount - 1 because the first vote is instant. Multiply by delay and add to current time to get timestamp when last vote was sent - respond(commandHandler.data.lang.voteprocessstarted.replace("numberOfVotes", activeReqEntry.amount).replace("waittime", waitTime)); + respond(await commandHandler.data.getLang("voteprocessstarted", { "numberOfVotes": activeReqEntry.amount, "waittime": waitTime }, requesterID)); } // Give requesting user cooldown. Set timestamp to now if cooldown is disabled to avoid issues when a process is aborted but cooldown can't be cleared - if (commandHandler.data.config.commentcooldown == 0) commandHandler.data.setUserCooldown(activeReqEntry.requestedby, Date.now()); + if (commandHandler.data.config.requestCooldown == 0) commandHandler.data.setUserCooldown(activeReqEntry.requestedby, Date.now()); else commandHandler.data.setUserCooldown(activeReqEntry.requestedby, activeReqEntry.until); } @@ -185,14 +185,14 @@ module.exports.upvote = { }); - }, commandHandler.data.config.commentdelay * (i > 0)); // We use commentdelay here for now, not sure if I'm going to add a separate setting + }, commandHandler.data.config.requestDelay * (i > 0)); - }, () => { // Function that will run on exit, aka the last iteration: Respond to the user + }, async () => { // Function that will run on exit, aka the last iteration: Respond to the user /* ------------- Send finished message for corresponding status ------------- */ if (activeReqEntry.status == "aborted") { - respond(commandHandler.data.lang.requestaborted.replace("successAmount", activeReqEntry.amount - Object.keys(activeReqEntry.failed).length).replace("totalAmount", activeReqEntry.amount)); + respond(await commandHandler.data.getLang("requestaborted", { "successAmount": activeReqEntry.amount - Object.keys(activeReqEntry.failed).length, "totalAmount": activeReqEntry.amount }, requesterID)); } else { @@ -204,7 +204,7 @@ module.exports.upvote = { } // Send finished message - respond(`${commandHandler.data.lang.votesuccess.replace("failedamount", Object.keys(activeReqEntry.failed).length).replace("numberOfVotes", activeReqEntry.amount)}\n${failedcmdreference}`); + respond(`${await commandHandler.data.getLang("votesuccess", { "failedamount": Object.keys(activeReqEntry.failed).length, "numberOfVotes": activeReqEntry.amount }, requesterID)}\n${failedcmdreference}`); // Set status of this request to cooldown and add amount of successful comments to our global commentCounter activeReqEntry.status = "cooldown"; @@ -253,18 +253,18 @@ module.exports.downvote = { let owners = commandHandler.data.cachefile.ownerid; if (resInfo.ownerIDs && resInfo.ownerIDs.length > 0) owners = resInfo.ownerIDs; - let requesterSteamID64 = resInfo.userID; - let ownercheck = owners.includes(requesterSteamID64); + let requesterID = resInfo.userID; + let ownercheck = owners.includes(requesterID); /* --------- Various checks --------- */ if (!resInfo.userID) { - respond(commandHandler.data.lang.nouserid); // Reject usage of command without an userID to avoid cooldown bypass + respond(await commandHandler.data.getLang("nouserid")); // Reject usage of command without an userID to avoid cooldown bypass return logger("err", "The downvote command was called without resInfo.userID! Blocking the command as I'm unable to apply cooldowns, which is required for this command!"); } - if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, commandHandler.data.lang.botnotready); // Bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained - if (commandHandler.controller.info.activeLogin) return respond(commandHandler.data.lang.activerelog); // Bot is waiting for relog - if (commandHandler.data.config.maxComments == 0 && !ownercheck) return respond(commandHandler.data.lang.commandowneronly); // Command is restricted to owners only + if (commandHandler.controller.info.readyAfter == 0) return respondModule(context, { prefix: "/me", ...resInfo }, await commandHandler.data.getLang("botnotready", null, requesterID)); // Bot isn't fully started yet - Pass new resInfo object which contains prefix and everything the original resInfo obj contained + if (commandHandler.controller.info.activeLogin) return respond(await commandHandler.data.getLang("activerelog", null, requesterID)); // Bot is waiting for relog + if (commandHandler.data.config.maxRequests == 0 && !ownercheck) return respond(await commandHandler.data.getLang("commandowneronly", null, requesterID)); // Command is restricted to owners only // Check and get arguments from user @@ -276,49 +276,49 @@ module.exports.downvote = { // Check if this id is already receiving something right now let idReq = commandHandler.controller.activeRequests[id]; - if (idReq && idReq.status == "active") return respond(commandHandler.data.lang.idalreadyreceiving); // Note: No need to check for user as that is supposed to be handled by a cooldown + if (idReq && idReq.status == "active") return respond(await commandHandler.data.getLang("idalreadyreceiving", null, requesterID)); // Note: No need to check for user as that is supposed to be handled by a cooldown // Check if user has cooldown - let { until, untilStr } = await commandHandler.data.getUserCooldown(requesterSteamID64); + let { until, untilStr } = await commandHandler.data.getUserCooldown(requesterID); - if (until > Date.now()) return respond(commandHandler.data.lang.idoncooldown.replace("remainingcooldown", untilStr)); + if (until > Date.now()) return respond(await commandHandler.data.getLang("idoncooldown", { "remainingcooldown": untilStr }, requesterID)); // Get all available bot accounts let { amount, availableAccounts, whenAvailableStr } = await getAvailableBotsForVoting(commandHandler, amountRaw, id, "downvote"); if ((availableAccounts.length < amount || availableAccounts.length == 0) && !whenAvailableStr) { // Check if this bot has not enough accounts suitable for this request and there won't be more available at any point. - if (availableAccounts.length == 0) respond(commandHandler.data.lang.votenoaccounts); // The < || == 0 check is intentional, as providing "all" will set amount to 0 if 0 accounts have been found - else respond(commandHandler.data.lang.voterequestless.replace("availablenow", availableAccounts.length)); + if (availableAccounts.length == 0) respond(await commandHandler.data.getLang("genericnoaccounts", null, requesterID)); // The < || == 0 check is intentional, as providing "all" will set amount to 0 if 0 accounts have been found + else respond(await commandHandler.data.getLang("genericrequestless", { "availablenow": availableAccounts.length }, requesterID)); return; } if (availableAccounts.length < amount) { // Check if not enough available accounts were found because of cooldown - respond(commandHandler.data.lang.votenotenoughavailableaccs.replace("waittime", whenAvailableStr).replace("availablenow", availableAccounts.length)); + respond(await commandHandler.data.getLang("genericnotenoughavailableaccs", { "waittime": whenAvailableStr, "availablenow": availableAccounts.length }, requesterID)); return; } // Get the sharedfile - commandHandler.controller.main.community.getSteamSharedFile(id, (err, sharedfile) => { + commandHandler.controller.main.community.getSteamSharedFile(id, async (err, sharedfile) => { if (err) { - respond(commandHandler.data.lang.errloadingsharedfile + err); + respond((await commandHandler.data.getLang("errloadingsharedfile", null, requesterID)) + err); return; } - // Register this vote process in activeRequests. We use commentdelay here for now, not sure if I'm going to add a separate setting + // Register this vote process in activeRequests commandHandler.controller.activeRequests[id] = { status: "active", type: "downvote", amount: amount, - requestedby: requesterSteamID64, + requestedby: requesterID, accounts: availableAccounts, thisIteration: -1, // Set to -1 so that first iteration will increase it to 0 retryAttempt: 0, - until: Date.now() + ((amount - 1) * commandHandler.data.config.commentdelay), // Calculate estimated wait time (first vote is instant -> remove 1 from numberOfComments) + until: Date.now() + ((amount - 1) * commandHandler.data.config.requestDelay), // Calculate estimated wait time (first vote is instant -> remove 1 from numberOfComments) failed: {} }; @@ -331,13 +331,13 @@ module.exports.downvote = { // Only send estimated wait time message for multiple votes if (activeReqEntry.amount > 1) { - let waitTime = timeToString(Date.now() + ((activeReqEntry.amount - 1) * commandHandler.data.config.commentdelay)); // Amount - 1 because the first vote is instant. Multiply by delay and add to current time to get timestamp when last vote was sent + let waitTime = timeToString(Date.now() + ((activeReqEntry.amount - 1) * commandHandler.data.config.requestDelay)); // Amount - 1 because the first vote is instant. Multiply by delay and add to current time to get timestamp when last vote was sent - respond(commandHandler.data.lang.voteprocessstarted.replace("numberOfVotes", activeReqEntry.amount).replace("waittime", waitTime)); + respond(await commandHandler.data.getLang("voteprocessstarted", { "numberOfVotes": activeReqEntry.amount, "waittime": waitTime }, requesterID)); } // Give requesting user cooldown. Set timestamp to now if cooldown is disabled to avoid issues when a process is aborted but cooldown can't be cleared - if (commandHandler.data.config.commentcooldown == 0) commandHandler.data.setUserCooldown(activeReqEntry.requestedby, Date.now()); + if (commandHandler.data.config.requestCooldown == 0) commandHandler.data.setUserCooldown(activeReqEntry.requestedby, Date.now()); else commandHandler.data.setUserCooldown(activeReqEntry.requestedby, activeReqEntry.until); } @@ -380,14 +380,14 @@ module.exports.downvote = { }); - }, commandHandler.data.config.commentdelay * (i > 0)); // We use commentdelay here for now, not sure if I'm going to add a separate setting + }, commandHandler.data.config.requestDelay * (i > 0)); - }, () => { // Function that will run on exit, aka the last iteration: Respond to the user + }, async () => { // Function that will run on exit, aka the last iteration: Respond to the user /* ------------- Send finished message for corresponding status ------------- */ if (activeReqEntry.status == "aborted") { - respond(commandHandler.data.lang.requestaborted.replace("successAmount", activeReqEntry.amount - Object.keys(activeReqEntry.failed).length).replace("totalAmount", activeReqEntry.amount)); + respond(await commandHandler.data.getLang("requestaborted", { "successAmount": activeReqEntry.amount - Object.keys(activeReqEntry.failed).length, "totalAmount": activeReqEntry.amount }, requesterID)); } else { @@ -399,7 +399,7 @@ module.exports.downvote = { } // Send finished message - respond(`${commandHandler.data.lang.votesuccess.replace("failedamount", Object.keys(activeReqEntry.failed).length).replace("numberOfVotes", activeReqEntry.amount)}\n${failedcmdreference}`); + respond(`${await commandHandler.data.getLang("votesuccess", { "failedamount": Object.keys(activeReqEntry.failed).length, "numberOfVotes": activeReqEntry.amount }, requesterID)}\n${failedcmdreference}`); // Set status of this request to cooldown and add amount of successful comments to our global commentCounter activeReqEntry.status = "cooldown"; diff --git a/src/commands/helpers/getCommentArgs.js b/src/commands/helpers/getCommentArgs.js index d3eda352..1575319a 100644 --- a/src/commands/helpers/getCommentArgs.js +++ b/src/commands/helpers/getCommentArgs.js @@ -4,7 +4,7 @@ * Created Date: 28.02.2022 11:55:06 * Author: 3urobeat * - * Last Modified: 10.07.2023 12:51:53 + * Last Modified: 18.10.2023 23:07:24 * Modified By: 3urobeat * * Copyright (c) 2022 3urobeat @@ -22,129 +22,131 @@ const CommandHandler = require("../commandHandler.js"); // eslint-disable-line * Retrieves arguments from a comment request. If request is invalid (for example too many comments requested) an error message will be sent * @param {CommandHandler} commandHandler The commandHandler object * @param {Array} args The command arguments - * @param {string} requesterSteamID64 The steamID64 of the requesting user + * @param {string} requesterID The steamID64 of the requesting user * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). * @param {function(string): void} respond The shortened respondModule call * @returns {Promise.<{ maxRequestAmount: number, commentcmdUsage: string, numberOfComments: number, profileID: string, idType: string, quotesArr: Array. }>} Resolves promise with object containing all relevant data when done */ -module.exports.getCommentArgs = (commandHandler, args, requesterSteamID64, resInfo, respond) => { +module.exports.getCommentArgs = (commandHandler, args, requesterID, resInfo, respond) => { return new Promise((resolve) => { + (async () => { // Lets us use await insidea Promise without creating an antipattern - // Get the correct ownerid array for this request - let owners = commandHandler.data.cachefile.ownerid; - if (resInfo.ownerIDs && resInfo.ownerIDs.length > 0) owners = resInfo.ownerIDs; + // Get the correct ownerid array for this request + let owners = commandHandler.data.cachefile.ownerid; + if (resInfo.ownerIDs && resInfo.ownerIDs.length > 0) owners = resInfo.ownerIDs; - let maxRequestAmount = commandHandler.data.config.maxComments; // Set to default value and if the requesting user is an owner it gets changed below - let numberOfComments = 0; - let quotesArr = commandHandler.data.quotes; + let maxRequestAmount = commandHandler.data.config.maxRequests; // Set to default value and if the requesting user is an owner it gets changed below + let numberOfComments = 0; + let quotesArr = commandHandler.data.quotes; - let profileID; - let idType = "profile"; // Set profile as default because that makes sense (because it can only be a group/sharedfile when param was provided yk) + let profileID; + let idType = "profile"; // Set profile as default because that makes sense (because it can only be a group/sharedfile/discussion when param was provided yk) - /* --------- Define command usage messages & maxRequestAmount for each user's privileges --------- */ - let commentcmdUsage; + /* --------- Define command usage messages & maxRequestAmount for each user's privileges --------- */ + let commentcmdUsage; - if (owners.includes(requesterSteamID64)) { - maxRequestAmount = commandHandler.data.config.maxOwnerComments; + if (owners.includes(requesterID)) { + maxRequestAmount = commandHandler.data.config.maxOwnerRequests; - if (maxRequestAmount > 1) commentcmdUsage = commandHandler.data.lang.commentcmdusageowner.replace(/cmdprefix/g, resInfo.cmdprefix).replace("maxRequestAmount", maxRequestAmount); - else commentcmdUsage = commandHandler.data.lang.commentcmdusageowner2.replace(/cmdprefix/g, resInfo.cmdprefix); - } else { - if (maxRequestAmount > 1) commentcmdUsage = commandHandler.data.lang.commentcmdusage.replace(/cmdprefix/g, resInfo.cmdprefix).replace("maxRequestAmount", maxRequestAmount); - else commentcmdUsage = commandHandler.data.lang.commentcmdusage2.replace(/cmdprefix/g, resInfo.cmdprefix); - } + if (maxRequestAmount > 1) commentcmdUsage = await commandHandler.data.getLang("commentcmdusageowner", { "cmdprefix": resInfo.cmdprefix }, requesterID); + else commentcmdUsage = await commandHandler.data.getLang("commentcmdusageowner2", { "cmdprefix": resInfo.cmdprefix }, requesterID); + } else { + if (maxRequestAmount > 1) commentcmdUsage = await commandHandler.data.getLang("commentcmdusage", { "cmdprefix": resInfo.cmdprefix }, requesterID); + else commentcmdUsage = await commandHandler.data.getLang("commentcmdusage2", { "cmdprefix": resInfo.cmdprefix }, requesterID); + } - /* --------- Check numberOfComments argument if it was provided --------- */ - if (args[0] !== undefined) { - if (isNaN(args[0])) { // Isn't a number? - if (args[0].toLowerCase() == "all" || args[0].toLowerCase() == "max") { - args[0] = maxRequestAmount; // Replace the argument with the max amount of comments this user is allowed to request - } else { - logger("debug", `CommandHandler getCommentArgs(): User provided invalid request amount "${args[0]}". Stopping...`); + /* --------- Check numberOfComments argument if it was provided --------- */ + if (args[0] !== undefined) { + if (isNaN(args[0])) { // Isn't a number? + if (args[0].toLowerCase() == "all" || args[0].toLowerCase() == "max") { + args[0] = maxRequestAmount; // Replace the argument with the max amount of comments this user is allowed to request + } else { + logger("debug", `CommandHandler getCommentArgs(): User provided invalid request amount "${args[0]}". Stopping...`); - respond(commandHandler.data.lang.invalidnumber.replace("cmdusage", commentcmdUsage)); - return resolve(false); + respond(await commandHandler.data.getLang("invalidnumber", { "cmdusage": commentcmdUsage }, requesterID)); + return resolve(false); + } } - } - if (args[0] > maxRequestAmount) { // Number is greater than maxRequestAmount? - logger("debug", `CommandHandler getCommentArgs(): User requested ${args[0]} but is only allowed ${maxRequestAmount} comments. Stopping...`); + if (args[0] > maxRequestAmount) { // Number is greater than maxRequestAmount? + logger("debug", `CommandHandler getCommentArgs(): User requested ${args[0]} but is only allowed ${maxRequestAmount} comments. Stopping...`); - respond(commandHandler.data.lang.commentrequesttoohigh.replace("maxRequestAmount", maxRequestAmount).replace("commentcmdusage", commentcmdUsage)); - return resolve(false); - } + respond(await commandHandler.data.getLang("commentrequesttoohigh", { "maxRequestAmount": maxRequestAmount, "commentcmdusage": commentcmdUsage }, requesterID)); + return resolve(false); + } + + numberOfComments = args[0]; - numberOfComments = args[0]; + /* --------- Check profileid argument if it was provided --------- */ + if (args[1]) { + if (owners.includes(requesterID) || args[1] == requesterID) { // Check if user is a bot owner or if they provided their own profile id + let arg = args[1]; - /* --------- Check profileid argument if it was provided --------- */ - if (args[1]) { - if (owners.includes(requesterSteamID64) || args[1] == requesterSteamID64) { // Check if user is a bot owner or if he provided his own profile id - let arg = args[1]; + commandHandler.controller.handleSteamIdResolving(arg, null, async (err, res, type) => { + if (err) { + respond((await commandHandler.data.getLang("commentinvalidid", { "commentcmdusage": commentcmdUsage }, requesterID)) + "\n\nError: " + err); + return resolve(false); + } - commandHandler.controller.handleSteamIdResolving(arg, null, (err, res, type) => { - if (err) { - respond(commandHandler.data.lang.commentinvalidid.replace("commentcmdusage", commentcmdUsage) + "\n\nError: " + err); - return resolve(false); - } + profileID = res; // Will be null on err + idType = type; // Update idType with what handleSteamIdResolving determined + }); - profileID = res; // Will be null on err - idType = type; // Update idType with what handleSteamIdResolving determined - }); + } else { + logger("debug", "CommandHandler getCommentArgs(): Non-Owner tried to provide profileid for another profile. Stopping..."); + respond(await commandHandler.data.getLang("commentprofileidowneronly", null, requesterID)); + return resolve(false); + } } else { - logger("debug", "CommandHandler getCommentArgs(): Non-Owner tried to provide profileid for another profile. Stopping..."); + logger("debug", "CommandHandler getCommentArgs(): No profileID parameter received, setting profileID to requesterID..."); - profileID = null; - respond(commandHandler.data.lang.commentprofileidowneronly); - return resolve(false); + profileID = requesterID; + idType = "profile"; } - } else { - logger("debug", "CommandHandler getCommentArgs(): No profileID parameter received, setting profileID to requesterSteamID64..."); - - profileID = requesterSteamID64; - idType = "profile"; - } - /* --------- Check if custom quotes were provided --------- */ - if (args[2] !== undefined) { - quotesArr = args.slice(2).join(" ").replace(/^\[|\]$/g, "").split(", "); // Change default quotes to custom quotes - } + /* --------- Check if custom quotes were provided --------- */ + if (args[2] !== undefined) { + quotesArr = args.slice(2).join(" ").replace(/^\[|\]$/g, "").split(", "); // Change default quotes to custom quotes + } - } // Arg[0] if statement ends here + } // Arg[0] if statement ends here - /* --------- Check if user did not provide numberOfComments --------- */ - if (numberOfComments == 0) { // No numberOfComments given? Ask again if maxRequestAmount > 1 (numberOfComments default value at the top is 0) - if (commandHandler.controller.getBots().length == 1 && maxRequestAmount == 1) { - logger("debug", "CommandHandler getCommentArgs(): User didn't provide numberOfComments but maxRequestAmount is 1. Accepting request as numberOfComments = 1."); + /* --------- Check if user did not provide numberOfComments --------- */ + if (numberOfComments == 0) { // No numberOfComments given? Ask again if maxRequestAmount > 1 (numberOfComments default value at the top is 0) + if (commandHandler.controller.getBots().length == 1 && maxRequestAmount == 1) { + logger("debug", "CommandHandler getCommentArgs(): User didn't provide numberOfComments but maxRequestAmount is 1. Accepting request as numberOfComments = 1."); - numberOfComments = 1; // If only one account is active, set 1 automatically - profileID = requesterSteamID64; // Define profileID so that the interval below resolves - } else { - logger("debug", `CommandHandler getCommentArgs(): User didn't provide numberOfComments and maxRequestAmount is ${maxRequestAmount} (> 1). Rejecting request.`); + numberOfComments = 1; // If only one account is active, set 1 automatically + profileID = requesterID; // Define profileID so that the interval below resolves + } else { + logger("debug", `CommandHandler getCommentArgs(): User didn't provide numberOfComments and maxRequestAmount is ${maxRequestAmount} (> 1). Rejecting request.`); - respond(commandHandler.data.lang.commentmissingnumberofcomments.replace("maxRequestAmount", maxRequestAmount).replace("commentcmdusage", commentcmdUsage)); - return resolve(false); + respond(await commandHandler.data.getLang("commentmissingnumberofcomments", { "maxRequestAmount": maxRequestAmount, "commentcmdusage": commentcmdUsage }, requesterID)); + return resolve(false); + } } - } - /* --------- Resolve promise with calculated values when profileID is defined --------- */ - let profileIDDefinedInterval = setInterval(() => { // Check if profileID is defined every 250ms and only then return values - if (profileID != undefined) { - clearInterval(profileIDDefinedInterval); + /* --------- Resolve promise with calculated values when profileID is defined --------- */ + let profileIDDefinedInterval = setInterval(() => { // Check if profileID is defined every 250ms and only then return values + if (profileID != undefined) { + clearInterval(profileIDDefinedInterval); - // Log debug values - logger("debug", `CommandHandler getCommentArgs() success. maxRequestAmount: ${maxRequestAmount} | numberOfComments: ${numberOfComments} | ID: ${profileID} | idType: ${idType} | quotesArr.length: ${quotesArr.length}`); + // Log debug values + logger("debug", `CommandHandler getCommentArgs() success. maxRequestAmount: ${maxRequestAmount} | numberOfComments: ${numberOfComments} | ID: ${profileID} | idType: ${idType} | quotesArr.length: ${quotesArr.length}`); - // Return obj if profileID is not null, otherwise return false as an error has occurred, the user was informed and execution should be stopped - if (profileID) resolve({ maxRequestAmount, numberOfComments, profileID, idType, quotesArr }); - else return resolve(false); - } - }, 250); + // Return obj if profileID is not null, otherwise return false as an error has occurred, the user was informed and execution should be stopped + if (profileID) resolve({ maxRequestAmount, numberOfComments, profileID, idType, quotesArr }); + else return resolve(false); + } + }, 250); + + })(); }); }; \ No newline at end of file diff --git a/src/commands/helpers/getCommentBots.js b/src/commands/helpers/getCommentBots.js index f0bc3bc4..3ece9cdc 100644 --- a/src/commands/helpers/getCommentBots.js +++ b/src/commands/helpers/getCommentBots.js @@ -4,7 +4,7 @@ * Created Date: 09.04.2023 12:49:53 * Author: 3urobeat * - * Last Modified: 04.07.2023 19:30:59 + * Last Modified: 24.09.2023 13:12:26 * Modified By: 3urobeat * * Copyright (c) 2023 3urobeat @@ -24,7 +24,7 @@ const { timeToString } = require("../../controller/helpers/misc.js"); * @param {CommandHandler} commandHandler The commandHandler object * @param {number} numberOfComments Number of requested comments * @param {boolean} canBeLimited If the accounts are allowed to be limited - * @param {string} idType Type of the request. This can either be "profile", "group" or "sharedfile". This is used to determine if limited accs need to be added first. + * @param {string} idType Type of the request. This can either be "profile", "group", "sharedfile" or "discussion". This is used to determine if limited accs need to be added first. * @param {string} receiverSteamID Optional: steamID64 of the receiving user. If set, accounts that are friend with the user will be prioritized and accsToAdd will be calculated. * @returns {{ accsNeeded: number, availableAccounts: Array., accsToAdd: Array., whenAvailable: number, whenAvailableStr: string }} `availableAccounts` contains all account names from bot object, `accsToAdd` account names which are limited and not friend, `whenAvailable` is a timestamp representing how long to wait until accsNeeded accounts will be available and `whenAvailableStr` is formatted human-readable as time from now */ @@ -37,7 +37,7 @@ module.exports.getAvailableBotsForCommenting = function(commandHandler, numberOf if (numberOfComments <= commandHandler.controller.getBots().length) accountsNeeded = numberOfComments; else accountsNeeded = commandHandler.controller.getBots().length; // Cap accountsNeeded at amount of accounts because if numberOfComments is greater we will start at account 1 again - // Method 2: Use as few accounts as possible to maximize the amount of parallel requests (Not implemented yet, probably coming in 2.12) + // Method 2: Use as few accounts as possible to maximize the amount of parallel requests (Not implemented yet) // TODO diff --git a/src/commands/helpers/getFavoriteBots.js b/src/commands/helpers/getFavoriteBots.js index d2d7c9c7..98225e74 100644 --- a/src/commands/helpers/getFavoriteBots.js +++ b/src/commands/helpers/getFavoriteBots.js @@ -4,7 +4,7 @@ * Created Date: 02.06.2023 14:07:27 * Author: 3urobeat * - * Last Modified: 04.07.2023 19:31:26 + * Last Modified: 25.09.2023 18:58:50 * Modified By: 3urobeat * * Copyright (c) 2023 3urobeat @@ -50,7 +50,7 @@ module.exports.getAvailableBotsForFavorizing = async (commandHandler, amount, id }); } - if (previousLengthFavorized - allAccounts.length > 0) logger("info", `${previousLengthFavorized - allAccounts.length} of ${previousLengthFavorized} bot accounts were removed from available accounts because we know that they have already favorized this item!`); + if (previousLengthFavorized - allAccounts.length > 0) logger("info", `${previousLengthFavorized - allAccounts.length} of ${previousLengthFavorized} bot accounts were removed from available accounts because we know that they have already un-/favorized this item!`); // Loop over activeRequests and remove all active entries from allAccounts if both are not empty diff --git a/src/commands/helpers/getFollowArgs.js b/src/commands/helpers/getFollowArgs.js new file mode 100644 index 00000000..4a5dcf36 --- /dev/null +++ b/src/commands/helpers/getFollowArgs.js @@ -0,0 +1,92 @@ +/* + * File: getFollowArgs.js + * Project: steam-comment-service-bot + * Created Date: 24.09.2023 16:10:36 + * Author: 3urobeat + * + * Last Modified: 07.10.2023 23:34:56 + * Modified By: 3urobeat + * + * Copyright (c) 2023 3urobeat + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. If not, see . + */ + + +const CommandHandler = require("../commandHandler.js"); // eslint-disable-line + + +/** + * Retrieves arguments from a follow request. If request is invalid, an error message will be sent + * @param {CommandHandler} commandHandler The commandHandler object + * @param {Array} args The command arguments + * @param {string} cmd Either "upvote", "downvote", "favorite" or "unfavorite", depending on which command is calling this function + * @param {CommandHandler.resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). + * @param {function(string): void} respond The shortened respondModule call + * @returns {Promise.<{ amount: number|string, id: string }>} If the user provided a specific amount, amount will be a number. If user provided "all" or "max", it will be returned as an unmodified string for getVoteBots.js to handle + */ +module.exports.getFollowArgs = (commandHandler, args, cmd, resInfo, respond) => { + return new Promise((resolve) => { + (async () => { // Lets us use await insidea Promise without creating an antipattern + + // Check for missing params + let cmdUsage = `'${resInfo.cmdprefix}${cmd} amount/"all" id/link'`; + + if (args[0]) args[0] = args[0].toLowerCase(); + if (args[0] == "max") args[0] = "all"; // Convert "all" alias + let amount = args[0] == "all" ? args[0] : Number(args[0]); // If user provides "all" then keep it as is and update it later to how many accounts are available, otherwise convert it to a number + + if (args.length == 0 || (amount != "all" && isNaN(amount)) || amount == 0) { + respond(await commandHandler.data.getLang("invalidnumber", { "cmdusage": cmdUsage }, resInfo.userID)); // An empty string will become a 0 + return resolve({}); + } + + + // Get the correct ownerid array for this request + let owners = commandHandler.data.cachefile.ownerid; + if (resInfo.ownerIDs && resInfo.ownerIDs.length > 0) owners = resInfo.ownerIDs; + + let requesterID = resInfo.userID; + + + // Check if id was provided and process input + if (args[1]) { + if (owners.includes(requesterID) || args[1] == requesterID) { // Check if user is a bot owner or if they provided their own profile id + let arg = args[1]; + + commandHandler.controller.handleSteamIdResolving(arg, null, async (err, res, idType) => { + if (err || (idType != "profile" && idType != "curator")) { + respond((await commandHandler.data.getLang("invalidprofileid", null, requesterID)) + "\n\nError: " + err); + return resolve({}); + } + + logger("debug", `CommandHandler getFollowArgs(): Owner provided valid id - amount: ${amount} | id: ${res} | idType: ${idType}`); + + resolve({ + "amountRaw": amount, + "id": res, + "idType": idType + }); + }); + + } else { + logger("debug", "CommandHandler getFollowArgs(): Non-Owner tried to provide id for another profile. Stopping..."); + + respond(await commandHandler.data.getLang("commentprofileidowneronly", null, requesterID)); + return resolve({}); + } + } else { + logger("debug", `CommandHandler getFollowArgs(): No id parameter received, using requesterID - amount: ${amount} | id: ${requesterID} | idType: profile`); + + resolve({ + "amountRaw": amount, + "id": requesterID, + "idType": "profile" + }); + } + + })(); + }); +}; \ No newline at end of file diff --git a/src/commands/helpers/getFollowBots.js b/src/commands/helpers/getFollowBots.js new file mode 100644 index 00000000..174fa838 --- /dev/null +++ b/src/commands/helpers/getFollowBots.js @@ -0,0 +1,111 @@ +/* + * File: getFollowBots.js + * Project: steam-comment-service-bot + * Created Date: 24.09.2023 18:01:44 + * Author: 3urobeat + * + * Last Modified: 26.09.2023 22:19:34 + * Modified By: 3urobeat + * + * Copyright (c) 2023 3urobeat + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. If not, see . + */ + + +const CommandHandler = require("../commandHandler.js"); // eslint-disable-line +const { timeToString } = require("../../controller/helpers/misc.js"); + + +/** + * Finds all needed and currently available bot accounts for a follow request. + * @param {CommandHandler} commandHandler The commandHandler object + * @param {number|"all"} amount Amount of favs requested or "all" to get the max available amount + * @param {boolean} canBeLimited If the accounts are allowed to be limited + * @param {string} id The user id to follow + * @param {string} idType Either "user" or "curator" + * @param {string} favType Either "follow" or "unfollow", depending on which request this is + * @returns {Promise.<{ amount: number, availableAccounts: Array., whenAvailable: number, whenAvailableStr: string }>} Resolves with obj: `availableAccounts` contains all account names from bot object, `whenAvailable` is a timestamp representing how long to wait until accsNeeded accounts will be available and `whenAvailableStr` is formatted human-readable as time from now + */ +module.exports.getAvailableBotsForFollowing = async (commandHandler, amount, canBeLimited, id, idType, favType) => { + + /* --------- Get all bots which haven't followed this id yet and aren't currently in another follow request --------- */ + let whenAvailable; // We will save the until value of the account that the user has to wait for here + let whenAvailableStr; + let allAccsOnline = commandHandler.controller.getBots(null, true); + let allAccounts = [ ... Object.keys(allAccsOnline) ]; // Clone keys array (bot usernames) of bots object + + + // Remove limited accounts from allAccounts array if desired + if (!canBeLimited) { + let previousLength = allAccounts.length; + allAccounts = allAccounts.filter(e => allAccsOnline[e].user.limitations && !allAccsOnline[e].user.limitations.limited); + + if (previousLength - allAccounts.length > 0) logger("info", `${previousLength - allAccounts.length} of ${previousLength} bot accounts were removed from available accounts as they are limited and can't be used for this request!`); + } + + + // Remove bot accounts from allAccounts which have already followed this id, or only allow them for type unfollow + let previousLength = allAccounts.length; + let alreadyUsed = await commandHandler.data.ratingHistoryDB.findAsync({ id: id, type: idType + "Follow" }, {}); + + if (favType == "follow") { + alreadyUsed.forEach((e) => { + if (allAccounts.indexOf(e.accountName) != -1) allAccounts.splice(allAccounts.indexOf(e.accountName), 1); // Remove all accounts that already followed + }); + } else { + allAccounts.forEach((e) => { + if (!alreadyUsed.some(e => e.accountName)) allAccounts.splice(allAccounts.indexOf(e), 1); // Remove all accounts that have not followed + }); + } + + if (previousLength - allAccounts.length > 0) logger("info", `${previousLength - allAccounts.length} of ${previousLength} bot accounts were removed from available accounts because we know that they have already un-/followed this user!`); + + + // Loop over activeRequests and remove all active entries from allAccounts if both are not empty + if (allAccounts.length > 0 && Object.keys(commandHandler.controller.activeRequests).length > 0) { + Object.keys(commandHandler.controller.activeRequests).forEach((e) => { + if (!commandHandler.controller.activeRequests[e].type.toLowerCase().includes("follow")) return; // Ignore entry if not of this type. ToLowerCase() is important here because type is in camelCase + + if (Date.now() < commandHandler.controller.activeRequests[e].until + (commandHandler.data.config.botaccountcooldown * 60000)) { // Check if entry is not finished yet + commandHandler.controller.activeRequests[e].accounts.forEach((f) => { // Loop over every account used in this request + allAccounts.splice(allAccounts.indexOf(f), 1); // Remove that accountindex from the allAccounts array + }); + + // If this removal causes the user to need to wait, update whenAvailable. Don't bother if user provided "all" + if (amount != "all" && allAccounts.length - commandHandler.controller.activeRequests[e].accounts.length < amount) { + whenAvailable = commandHandler.controller.activeRequests[e].until + (commandHandler.data.config.botaccountcooldown * 60000); + whenAvailableStr = timeToString(whenAvailable); + } + } else { + delete commandHandler.controller.activeRequests[e]; // Remove entry from object if it is finished to keep the object clean + } + }); + } + + + // Update amount if "all" + if (amount == "all") { + amount = allAccounts.length; + logger("debug", `CommandHandler getFollowBots(): User provided max amount keyword "all", updating it to ${allAccounts.length}`); + } + + + // Cut result to only include needed accounts + if (allAccounts.length > amount) allAccounts = allAccounts.slice(0, amount); + + + // Log result to debug + if (allAccounts.length < amount) logger("debug", `CommandHandler getFollowBots(): Found ${allAccounts.length} available bot accounts to un-/folloe ${id} but ${amount} are needed. If accs will become available, the user needs to wait: ${whenAvailableStr || "/"}`); + else logger("debug", `CommandHandler getFollowBots(): Found ${allAccounts.length} available bot accounts to un-/follow ${id}: ${allAccounts}`); + + // Resolve promise with values + return { + "amount": amount, + "availableAccounts": allAccounts, + "whenAvailable": whenAvailable, + "whenAvailableStr": whenAvailableStr + }; +}; \ No newline at end of file diff --git a/src/commands/helpers/getSharedfileArgs.js b/src/commands/helpers/getSharedfileArgs.js index 2af68ede..463fd651 100644 --- a/src/commands/helpers/getSharedfileArgs.js +++ b/src/commands/helpers/getSharedfileArgs.js @@ -4,7 +4,7 @@ * Created Date: 28.05.2023 12:18:49 * Author: 3urobeat * - * Last Modified: 09.07.2023 13:31:26 + * Last Modified: 24.09.2023 17:18:35 * Modified By: 3urobeat * * Copyright (c) 2023 3urobeat @@ -19,7 +19,7 @@ const CommandHandler = require("../commandHandler.js"); // eslint-disable-line /** - * Retrieves arguments from a vote request. If request is invalid, an error message will be sent + * Retrieves arguments from a favorite/vote request. If request is invalid, an error message will be sent * @param {CommandHandler} commandHandler The commandHandler object * @param {Array} args The command arguments * @param {string} cmd Either "upvote", "downvote", "favorite" or "unfavorite", depending on which command is calling this function @@ -29,37 +29,39 @@ const CommandHandler = require("../commandHandler.js"); // eslint-disable-line */ module.exports.getSharedfileArgs = (commandHandler, args, cmd, resInfo, respond) => { return new Promise((resolve) => { + (async () => { // Lets us use await insidea Promise without creating an antipattern - // Check for missing params - let voteCmdUsage = `'${resInfo.cmdprefix}${cmd} amount/"all" id/link'`; + // Check for missing params + let cmdUsage = `'${resInfo.cmdprefix}${cmd} amount/"all" id/link'`; - if (args[0]) args[0] = args[0].toLowerCase(); - if (args[0] == "max") args[0] = "all"; // Convert "all" alias - let amount = args[0] == "all" ? args[0] : Number(args[0]); // If user provides "all" then keep it as is and update it later to how many accounts are available, otherwise convert it to a number + if (args[0]) args[0] = args[0].toLowerCase(); + if (args[0] == "max") args[0] = "all"; // Convert "all" alias + let amount = args[0] == "all" ? args[0] : Number(args[0]); // If user provides "all" then keep it as is and update it later to how many accounts are available, otherwise convert it to a number - if (args.length == 0 || (amount != "all" && isNaN(amount)) || amount == 0) { - respond(commandHandler.data.lang.invalidnumber.replace("cmdusage", voteCmdUsage)); // An empty string will become a 0 - return resolve({}); - } - - // Process input and check if ID is valid - commandHandler.controller.handleSteamIdResolving(args[1], "sharedfile", (err, id, idType) => { // eslint-disable-line no-unused-vars - - // Send error if item could not be found - if (err || !id) { - respond(commandHandler.data.lang.invalidsharedfileid.replace("cmdusage", voteCmdUsage)); + if (args.length == 0 || (amount != "all" && isNaN(amount)) || amount == 0) { + respond(await commandHandler.data.getLang("invalidnumber", { "cmdusage": cmdUsage }, resInfo.userID)); // An empty string will become a 0 return resolve({}); } - // ...otherwise resolve - logger("debug", `CommandHandler getSharedfileArgs() success. amount: ${amount} | id: ${id}`); + // Process input and check if ID is valid + commandHandler.controller.handleSteamIdResolving(args[1], "sharedfile", async (err, id, idType) => { // eslint-disable-line no-unused-vars - resolve({ - "amountRaw": amount, - "id": id - }); + // Send error if item could not be found + if (err || !id) { + respond(await commandHandler.data.getLang("invalidsharedfileid", { "cmdusage": cmdUsage }, resInfo.userID)); + return resolve({}); + } - }); + // ...otherwise resolve + logger("debug", `CommandHandler getSharedfileArgs() success. amount: ${amount} | id: ${id}`); + + resolve({ + "amountRaw": amount, + "id": id + }); + + }); + })(); }); }; \ No newline at end of file diff --git a/src/commands/helpers/handleFollowErrors.js b/src/commands/helpers/handleFollowErrors.js new file mode 100644 index 00000000..461e08ed --- /dev/null +++ b/src/commands/helpers/handleFollowErrors.js @@ -0,0 +1,94 @@ +/* + * File: handleFollowErrors.js + * Project: steam-comment-service-bot + * Created Date: 24.09.2023 22:57:21 + * Author: 3urobeat + * + * Last Modified: 25.09.2023 18:35:20 + * Modified By: 3urobeat + * + * Copyright (c) 2023 3urobeat + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. If not, see . + */ + + +const Bot = require("../../bot/bot.js"); // eslint-disable-line + + +/** + * Checks if the following follow process iteration should be skipped + * @param {CommandHandler} commandHandler The commandHandler object + * @param {{ next: function(): void, break: function(): void, index: function(): number }} loop Object returned by misc.js syncLoop() helper + * @param {Bot} bot Bot object of the account making this request + * @param {string} id ID of the profile that receives the follow + * @returns {boolean} `true` if iteration should continue, `false` if iteration should be skipped using return + */ +module.exports.handleFollowIterationSkip = function(commandHandler, loop, bot, id) { + let activeReqEntry = commandHandler.controller.activeRequests[id]; // Make using the obj shorter + + // Check if no bot account was found + if (!bot) { + activeReqEntry.failed[`c${activeReqEntry.thisIteration + 1} b? p?`] = "Skipped because bot account does not exist"; + + logger("error", `[Bot ?] Error while sending un-/follow ${activeReqEntry.thisIteration + 1}/${activeReqEntry.amount} for ${id}: Bot account '${activeReqEntry.accounts[loop.index() % activeReqEntry.accounts.length]}' does not exist?! Skipping...`); + loop.next(); + return false; + } + + // Check if bot account is offline + if (bot.status != Bot.EStatus.ONLINE) { + activeReqEntry.failed[`c${activeReqEntry.thisIteration + 1} b${bot.index} p${bot.loginData.proxyIndex}`] = "Skipped because bot account is offline"; + + logger("error", `[${bot.logPrefix}] Error while sending un-/follow ${activeReqEntry.thisIteration + 1}/${activeReqEntry.amount} for ${id}: Skipped because bot account is offline`); + loop.next(); + return false; + } + + // If nothing above terminated the function then return true to let the vote loop continue + return true; +}; + + +/** + * Logs follow errors + * @param {string} error The error string returned by steam-community + * @param {CommandHandler} commandHandler The commandHandler object + * @param {Bot} bot Bot object of the account making this request + * @param {string} id ID of the profile that receives the follow + */ +module.exports.logFollowError = (error, commandHandler, bot, id) => { + let activeReqEntry = commandHandler.controller.activeRequests[id]; // Make using the obj shorter + + // Add proxy information if one was used for this account + let proxiesDescription = ""; + if (commandHandler.data.proxies.length > 1) proxiesDescription = ` using proxy ${bot.loginData.proxyIndex}`; + + + // Log error, add it to failed obj and continue with next iteration + logger("error", `[${bot.logPrefix}] Error while sending un-/follow ${activeReqEntry.thisIteration + 1}/${activeReqEntry.amount} for ${id}${proxiesDescription}: ${error}`); + + activeReqEntry.failed[`c${activeReqEntry.thisIteration + 1} b${bot.index} p${bot.loginData.proxyIndex}`] = `${error}`; + + + // Sort failed object to make it easier to read + activeReqEntry.failed = sortFailedCommentsObject(activeReqEntry.failed); +}; + + +/** + * Helper function to sort failed object by number so that it is easier to read + * @param {object} failedObj Current state of failed object + */ +function sortFailedCommentsObject(failedObj) { + let sortedvals = Object.keys(failedObj).sort((a, b) => { + return Number(a.split(" ")[0].replace("c", "")) - Number(b.split(" ")[0].replace("c", "")); + }); + + // Map sortedvals back to object if array is not empty - Credit: https://www.geeksforgeeks.org/how-to-create-an-object-from-two-arrays-in-javascript/ + if (sortedvals.length > 0) failedObj = Object.assign(...sortedvals.map(k => ({ [k]: failedObj[k] }))); + + return failedObj; +} \ No newline at end of file diff --git a/src/controller/controller.js b/src/controller/controller.js index 700c5e8e..a8f26c8d 100644 --- a/src/controller/controller.js +++ b/src/controller/controller.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 08.07.2023 00:47:49 + * Last Modified: 15.10.2023 11:13:59 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -35,7 +35,7 @@ const Controller = function() { /** * Implementation of a synchronous for loop in JS (Used as reference: https://whitfin.io/handling-synchronous-asynchronous-loops-javascriptnode-js/) * @param {number} iterations The amount of iterations - * @param {function(): void} func The function to run each iteration (Params: loop, index) + * @param {function(object, number): void} func The function to run each iteration (Params: loop, index) * @param {function(): void} exit This function will be called when the loop is finished */ syncLoop: (iterations, func, exit) => {}, // eslint-disable-line @@ -56,12 +56,20 @@ const Controller = function() { timeToString: () => {}, // eslint-disable-line /** - * Pings an URL to check if the service and this internet connection is working + * Pings a *https* URL to check if the service and this internet connection is working * @param {string} url The URL of the service to check - * @param {boolean} throwTimeout If true, the function will throw a timeout error if Steam can't be reached after 20 seconds + * @param {boolean} [throwTimeout=false] If true, the function will throw a timeout error if Steam can't be reached after 20 seconds + * @param {{ ip: string, port: number, username: string, password: string }} [proxy] Provide a proxy if the connection check should be made through a proxy instead of the local connection * @returns {Promise.<{ statusMessage: string, statusCode: number|null }>} Resolves on response code 2xx and rejects on any other response code. Both are called with parameter `response` (Object) which has a `statusMessage` (String) and `statusCode` (Number) key. `statusCode` is `null` if request failed. */ - checkConnection: (url, throwTimeout) => {}, // eslint-disable-line + checkConnection: (url, throwTimeout = false, proxy) => {}, // eslint-disable-line + + /** + * Splits a HTTP proxy URL into its parts + * @param {string} url The HTTP proxy URL + * @returns {{ ip: string, port: number, username: string, password: string }} Object containing the proxy parts + */ + splitProxyString: (url) => {}, // eslint-disable-line /** * Helper function which attempts to cut Strings intelligently and returns all parts. It will attempt to not cut words & links in half. @@ -93,7 +101,7 @@ const Controller = function() { /** - * Internal: Inits the DataManager system, runs the updater and starts all bot accounts + * Internal: Initializes the bot by importing data from the disk, running the updater and finally logging in all bot accounts. */ Controller.prototype._start = async function() { let checkAndGetFile = require("../starter.js").checkAndGetFile; // Temp var to use checkAndGetFile() before it is referenced in DataManager @@ -135,9 +143,9 @@ Controller.prototype._start = async function() { logger("info", "Checking if Steam is reachable...", false, true, logger.animation("loading")); if (!await checkAndGetFile("./src/controller/helpers/misc.js", logger, false, false)) return this.stop(); - this.misc = await require("./helpers/misc.js"); + this.misc = require("./helpers/misc.js"); - this.misc.checkConnection("https://steamcommunity.com", true) + await this.misc.checkConnection("https://steamcommunity.com", true) .then((res) => logger("info", `SteamCommunity is up! Status code: ${res.statusCode}`, false, true, logger.animation("loading"))) .catch((res) => { if (!res.statusCode) logger("error", `SteamCommunity seems to be down or your internet isn't working! Check: https://steamstat.us \n ${res.statusMessage}\n\n Aborting...\n`, true); @@ -148,11 +156,12 @@ Controller.prototype._start = async function() { /* ------------ Init dataManager system and import: ------------ */ - if (!checkAndGetFile("./src/dataManager/dataManager.js", logger, false, false)) return; + if (!await checkAndGetFile("./src/dataManager/dataManager.js", logger, false, false)) return; let DataManager = require("../dataManager/dataManager.js"); this.data = new DataManager(this); // All functions provided by the DataManager, as well as all imported file data will be accessible here + await this.data._loadDataManagerFiles(); await this.data._importFromDisk(); // Call optionsUpdateAfterConfigLoad() to set previously inaccessible options @@ -185,6 +194,8 @@ Controller.prototype._start = async function() { /* ------------ Check imported data : ------------ */ + let forceUpdate = false; // Provide forceUpdate var which the following helpers can modify to force a update + global.extdata = this.data.datafile; // This needs to stay for backwards compatibility // Process imported owner & group ids and update cachefile @@ -193,10 +204,11 @@ Controller.prototype._start = async function() { // Check imported data await this.data.checkData().catch(() => this.stop()); // Terminate the bot if some critical check failed + // Verify integrity of all source code files and restore invalid ones. It is safe to use require() after this function is done! + await this.data.verifyIntegrity(); - /* ------------ Run compatibility feature and updater or start logging in: ------------ */ - let forceUpdate = false; // Provide forceUpdate var which is passed to updater which runCompatibility can overwrite + /* ------------ Run compatibility feature and updater or start logging in: ------------ */ let compatibility = await checkAndGetFile("./src/updater/compatibility.js", logger, false, false); if (compatibility) forceUpdate = await compatibility.runCompatibility(this); // Don't bother running it if it couldn't be found and just hope the next update will fix it @@ -204,8 +216,7 @@ Controller.prototype._start = async function() { let Updater = await checkAndGetFile("./src/updater/updater.js", logger, false, false); if (!Updater) { logger("error", "Fatal Error: Failed to load updater! Please reinstall the bot manually. Aborting..."); - this.stop(); - return; + return this.stop(); } // Init a new updater object. This will start our auto update checker @@ -326,7 +337,7 @@ let logger = function(type, str) { logger.animation = () => {}; // Just to be sure that no error occurs when trying to call this function without the real logger being present -/* ------------ Start the bot: ------------ */ // TODO: Not rewritten yet +/* ------------ Start the bot: ------------ */ if (parseInt(process.argv[3]) + 2500 > Date.now()) { // Check if this process just got started in the last 2.5 seconds or just required by itself by checking the timestamp attached by starter.js @@ -350,18 +361,27 @@ if (parseInt(process.argv[3]) + 2500 > Date.now()) { // Check if this process ju } -/* -------- Register functions to let the IntelliSense know what's going on in helper files -------- */ +/* ------------ Provide functions for restarting & stopping: ------------ */ /** * Restarts the whole application - * @param {string} data Stringified restartdata object that will be kept through restarts + * @param {string} data Optional: Stringified restartdata object that will be kept through restarts */ -Controller.prototype.restart = function(data) { process.send(`restart(${data})`); }; +Controller.prototype.restart = function(data) { + if (!data) data = JSON.stringify({ skippedaccounts: this.info.skippedaccounts, updateFailed: false }); + + process.send(`restart(${data})`); +}; /** * Stops the whole application */ -Controller.prototype.stop = function() { process.send("stop()"); }; +Controller.prototype.stop = function() { + process.send("stop()"); +}; + + +/* -------- Register functions to let the IntelliSense know what's going on in helper files -------- */ /** * Attempts to log in all bot accounts which are currently offline one after another. @@ -415,6 +435,13 @@ Controller.prototype._lastcommentUnfriendCheck = function() {} // eslint-disable */ Controller.prototype.getBots = function(statusFilter = EStatus.ONLINE, mapToObject = false) {}; // eslint-disable-line +/** + * Retrieves bot accounts per proxy. This can be used to find the most and least used active proxies for example. + * @param {boolean} [filterOffline=false] Set to true to remove proxies which are offline. Make sure to call `checkAllProxies()` beforehand! + * @returns {Array.<{ bots: Array., proxy: string, proxyIndex: number, isOnline: boolean, lastOnlineCheck: number }>} Bot accounts mapped to their associated proxy + */ +Controller.prototype.getBotsPerProxy = function(filterOffline = false) {}; // eslint-disable-line + /** * Internal: Handles process's unhandledRejection & uncaughtException error events. * Should a NPM related error be detected it attempts to reinstall all packages using our npminteraction helper function @@ -422,9 +449,10 @@ Controller.prototype.getBots = function(statusFilter = EStatus.ONLINE, mapToObje Controller.prototype._handleErrors = function() {} // eslint-disable-line /** - * Handles converting URLs to steamIDs, determining their type if unknown and checking if it matches your expectation + * Handles converting URLs to steamIDs, determining their type if unknown and checking if it matches your expectation. + * Note: You need to provide a full URL for discussions & curators. For discussions only type checking/determination is supported. * @param {string} str The profileID argument provided by the user - * @param {string} expectedIdType The type of SteamID expected ("profile", "group" or "sharedfile") or `null` if type should be assumed. + * @param {string} expectedIdType The type of SteamID expected ("profile", "group", "sharedfile", "discussion" or "curator") or `null` if type should be assumed. * @param {function(string|null, string|null, string|null): void} callback Called with `err` (String or null), `steamID64` (String or null), `idType` (String or null) parameters on completion */ Controller.prototype.handleSteamIdResolving = (str, expectedIdType, callback) => {} // eslint-disable-line diff --git a/src/controller/events/ready.js b/src/controller/events/ready.js index 1bafe872..5687af09 100644 --- a/src/controller/events/ready.js +++ b/src/controller/events/ready.js @@ -4,7 +4,7 @@ * Created Date: 29.03.2023 12:23:29 * Author: 3urobeat * - * Last Modified: 10.07.2023 09:33:30 + * Last Modified: 21.10.2023 12:55:18 * Modified By: 3urobeat * * Copyright (c) 2023 3urobeat @@ -15,7 +15,6 @@ */ -const fs = require("fs"); const SteamUser = require("steam-user"); const Controller = require("../controller"); @@ -34,12 +33,12 @@ Controller.prototype._readyEvent = function() { // Calculate what the max amount of comments per account is and log it - let maxCommentsOverall = this.data.config.maxOwnerComments; // Define what the absolute maximum is which the bot is allowed to process. This should make checks shorter - if (this.data.config.maxComments > this.data.config.maxOwnerComments) maxCommentsOverall = this.data.config.maxComments; + let maxRequestsOverall = this.data.config.maxOwnerRequests; // Define what the absolute maximum is which the bot is allowed to process. This should make checks shorter + if (this.data.config.maxRequests > this.data.config.maxOwnerRequests) maxRequestsOverall = this.data.config.maxRequests; let repeatedCommentsStr; - if (maxCommentsOverall > 3) repeatedCommentsStr = `${logger.colors.underscore}${logger.colors.fgred}${round(maxCommentsOverall / this.getBots().length, 2)}`; - else repeatedCommentsStr = round(maxCommentsOverall / this.getBots().length, 2); + if (maxRequestsOverall > 3) repeatedCommentsStr = `${logger.colors.underscore}${logger.colors.fgred}${round(maxRequestsOverall / this.getBots().length, 2)}`; + else repeatedCommentsStr = round(maxRequestsOverall / this.getBots().length, 2); logger("", `${logger.colors.brfgblue}>${logger.colors.reset} ${this.getBots().length} total account(s) | ${repeatedCommentsStr} comments per account allowed`, true, false, null, false, true); @@ -82,11 +81,11 @@ Controller.prototype._readyEvent = function() { if (Object.keys(this.pluginSystem.pluginList).length > 0) logger("", `${logger.colors.fgblack}>${logger.colors.reset} Successfully loaded ${Object.keys(this.pluginSystem.pluginList).length} plugins!`, true, false, null, false, true); - // Log which games the main and child bots are playing + // Log which games the main bot is playing let playinggames = ""; if (this.data.config.playinggames[1]) playinggames = `(${this.data.config.playinggames.slice(1, this.data.config.playinggames.length)})`; - logger("", `${logger.colors.brfgyellow}>${logger.colors.reset} Playing status: ${logger.colors.fggreen}${this.data.config.playinggames[0]}${logger.colors.reset} ${playinggames}`, true, false, null, false, true); + logger("", `${logger.colors.brfgyellow}>${logger.colors.reset} Playing status: ${logger.colors.fggreen}${this.data.config.playinggames[0] || "/"}${logger.colors.reset} ${playinggames}`, true, false, null, false, true); // Calculate time the bot took to start @@ -113,7 +112,7 @@ Controller.prototype._readyEvent = function() { // Log amount of skippedaccounts - if (this.info.skippedaccounts.length > 0) logger("info", `Skipped Accounts: ${this.info.skippedaccounts.length}/${Object.keys(this.data.logininfo).length}\n`, true); + if (this.info.skippedaccounts.length > 0) logger("info", `Skipped Accounts: ${this.info.skippedaccounts.length}/${this.data.logininfo.length}\n`, true); // Please star my repo :) @@ -175,13 +174,11 @@ Controller.prototype._readyEvent = function() { this.data.datafile.totallogintime = round(this.data.datafile.totallogintime, 2); this.data.datafile.firststart = false; - fs.writeFile(srcdir + "/data/data.json", JSON.stringify(this.data.datafile, null, 4), err => { // Write changes - if (err) logger("error", "change this.data.datafile to false error: " + err); - }); + this.data.writeDatafileToDisk(); // Set progress bar to 100% if one is active - if (logger.getProgressBar()) logger.increaseProgressBar((100 / Object.keys(this.data.logininfo).length) / 3); + if (logger.getProgressBar()) logger.increaseProgressBar((100 / this.data.logininfo.length) / 3); // Print startup complete message and erase it after 5 sec diff --git a/src/controller/helpers/friendlist.js b/src/controller/helpers/friendlist.js index deca36b1..174f7a26 100644 --- a/src/controller/helpers/friendlist.js +++ b/src/controller/helpers/friendlist.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 10.07.2023 13:10:14 + * Last Modified: 19.10.2023 19:00:06 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -36,7 +36,7 @@ Controller.prototype.checkLastcommentDB = function(bot) { let obj = { id: e, - time: Date.now() - (this.data.config.commentcooldown * 60000) // Subtract commentcooldown so that the user is able to use the command instantly + time: Date.now() - (this.data.config.requestCooldown * 60000) // Subtract requestCooldown so that the user is able to use the command instantly }; this.data.lastCommentDB.insert(obj, (err) => { @@ -83,12 +83,12 @@ Controller.prototype.friendListCapacityCheck = function(bot, callback) { docs = docs.sort((a, b) => a.time - b.time); // Iterate over all docs until we find someone still on our friendlist that isn't an owner (since this func is called for each bot acc we don't need to iterate over the botobject) - docs.every((e, i) => { // Use every() so we can break with return false + docs.every(async (e, i) => { // Use every() so we can break with return false if (bot.user.myFriends[e.id] == 3 && !this.data.cachefile.ownerid.includes(e.id)) { // Check if friend and not owner let steamID = new SteamID(e.id); - // Unfriend user and send him/her a message // TODO: Maybe only do this from the main bot? - bot.sendChatMessage(bot, { userID: steamID.getSteamID64() }, this.data.lang.userunfriend.replace("forceFriendlistSpaceTime", this.data.advancedconfig.forceFriendlistSpaceTime)); + // Unfriend user and send them a message // TODO: Maybe only do this from the main bot? + bot.sendChatMessage(bot, { userID: steamID.getSteamID64() }, await this.data.getLang("userunfriend", { "forceFriendlistSpaceTime": this.data.advancedconfig.forceFriendlistSpaceTime }, steamID.getSteamID64())); bot.user.removeFriend(steamID); logger("info", `[Bot ${bot.index}] Force-Unfriended ${e.id} after being inactive for ${this.data.advancedconfig.forceFriendlistSpaceTime} days to keep 1 empty slot on the friendlist`); @@ -131,11 +131,11 @@ Controller.prototype._lastcommentUnfriendCheck = function() { docs.forEach((e, i) => { // Take action for all results setTimeout(() => { - this.getBots().forEach((f, j) => { + this.getBots().forEach(async (f, j) => { let thisbot = f.user; if (thisbot.myFriends[e.id] && thisbot.myFriends[e.id] == 3 && !this.data.cachefile.ownerid.includes(e.id)) { // Check if the targeted user is still friend and not an owner - if (j == 0) this.main.sendChatMessage(this.main, { userID: e.id }, this.data.lang.userforceunfriend.replace("unfriendtime", this.data.config.unfriendtime)); + if (j == 0) this.main.sendChatMessage(this.main, { userID: e.id }, await this.data.getLang("userforceunfriend", { "unfriendtime": this.data.config.unfriendtime }, e.id)); setTimeout(() => { thisbot.removeFriend(new SteamID(e.id)); // Unfriend user with each bot diff --git a/src/controller/helpers/getBots.js b/src/controller/helpers/getBots.js index ef8d2463..3a85498a 100644 --- a/src/controller/helpers/getBots.js +++ b/src/controller/helpers/getBots.js @@ -4,7 +4,7 @@ * Created Date: 02.05.2023 13:46:21 * Author: 3urobeat * - * Last Modified: 04.07.2023 19:38:06 + * Last Modified: 17.10.2023 18:26:25 * Modified By: 3urobeat * * Copyright (c) 2023 3urobeat @@ -15,8 +15,9 @@ */ +const Bot = require("../../bot/bot.js"); const Controller = require("../controller"); -const EStatus = require("../../bot/EStatus.js"); +const EStatus = Bot.EStatus; /** @@ -26,7 +27,7 @@ const EStatus = require("../../bot/EStatus.js"); * @returns {Array|object} An array or object if `mapToObject == true` containing all matching bot accounts. */ Controller.prototype.getBots = function(statusFilter, mapToObject) { - if (!statusFilter) statusFilter = EStatus.ONLINE; + if (statusFilter == null) statusFilter = EStatus.ONLINE; // Explicitly check for null so that filtering for OFFLINE (enum 0) works properly let accs = Object.values(this.bots); // Mark all bots as candidates @@ -38,4 +39,35 @@ Controller.prototype.getBots = function(statusFilter, mapToObject) { // Return result return accs; +}; + + +/** + * Retrieves bot accounts per proxy. This can be used to find the most and least used active proxies for example. + * @param {boolean} [filterOffline=false] Set to true to remove proxies which are offline. Make sure to call `checkAllProxies()` beforehand! + * @returns {Array.<{ bots: Array., proxy: string, proxyIndex: number, isOnline: boolean, lastOnlineCheck: number }>} Bot accounts mapped to their associated proxy + */ +Controller.prototype.getBotsPerProxy = function(filterOffline = false) { + + // Get all bot accounts + let accs = this.getBots("*"); + + // Prefill mappedProxies + let mappedProxies = []; + + this.data.proxies.forEach((e) => mappedProxies.push({ bots: [], ...e })); + + // Find associated proxies + accs.forEach((e) => { + let associatedProxy = mappedProxies[e.loginData.proxyIndex]; + + associatedProxy.bots.push(e); + }); + + // Filter inactive proxies if desired + if (filterOffline) mappedProxies = mappedProxies.filter((e) => e.isOnline); + + // Return result + return mappedProxies; + }; \ No newline at end of file diff --git a/src/controller/helpers/handleErrors.js b/src/controller/helpers/handleErrors.js index e1274d04..d98866f5 100644 --- a/src/controller/helpers/handleErrors.js +++ b/src/controller/helpers/handleErrors.js @@ -31,14 +31,16 @@ Controller.prototype._handleErrors = function() { // Known issue: This event listener doesn't seem to capture uncaught exceptions in functions. However if it is inside for example a setTimeout it suddently works. - process.on("uncaughtException", (reason) => { + process.on("uncaughtException", async (reason) => { // Try to fix error automatically by reinstalling all modules if (String(reason).includes("Error: Cannot find module")) { logger("", "", true); logger("info", "Cannot find module error detected. Trying to fix error by reinstalling modules...\n"); logger("debug", "uncaughtException " + reason.stack, true); - require("./npminteraction.js").reinstallAll(logger, (err, stdout) => { //eslint-disable-line + const npminteraction = await this.checkAndGetFile("./src/controller/helpers/npminteraction.js", logger, false, false); + + npminteraction.reinstallAll(logger, (err, stdout) => { //eslint-disable-line if (err) { logger("error", "I was unable to reinstall all modules. Please try running 'npm install --production' manually. Error: " + err); return this.stop(); diff --git a/src/controller/helpers/handleSteamIdResolving.js b/src/controller/helpers/handleSteamIdResolving.js index 18320eb1..e2e5ab25 100644 --- a/src/controller/helpers/handleSteamIdResolving.js +++ b/src/controller/helpers/handleSteamIdResolving.js @@ -4,7 +4,7 @@ * Created Date: 09.03.2022 12:58:17 * Author: 3urobeat * - * Last Modified: 08.07.2023 00:36:23 + * Last Modified: 26.09.2023 20:52:22 * Modified By: 3urobeat * * Copyright (c) 2022 3urobeat @@ -25,9 +25,10 @@ const Controller = require("../controller.js"); // I'm therefore taking Strings instead of SteamID.Type values for types now. /** - * Handles converting URLs to steamIDs, determining their type if unknown and checking if it matches your expectation + * Handles converting URLs to steamIDs, determining their type if unknown and checking if it matches your expectation. + * Note: You need to provide a full URL for discussions & curators. For discussions only type checking/determination is supported. * @param {string} str The profileID argument provided by the user - * @param {string} expectedIdType The type of SteamID expected ("profile", "group" or "sharedfile") or `null` if type should be assumed. + * @param {string} expectedIdType The type of SteamID expected ("profile", "group", "sharedfile", "discussion" or "curator") or `null` if type should be assumed. * @param {function(string|null, string|null, string|null): void} callback Called with `err` (String or null), `steamID64` (String or null), `idType` (String or null) parameters on completion */ Controller.prototype.handleSteamIdResolving = (str, expectedIdType, callback) => { @@ -66,7 +67,15 @@ Controller.prototype.handleSteamIdResolving = (str, expectedIdType, callback) => // My library doesn't have a check if exists function nor returns the steamID64 if I pass it into steamID64ToCustomUrl(). But since I don't want to parse the URL myself here I'm just gonna request the full obj and cut the id out of it steamIDResolver.steamID64ToFullInfo(str, (err, obj) => handleResponse(err, obj.steamID64[0])); - } else if (str.includes("steamcommunity.com/groups/")) { + } else if (str.includes("steamcommunity.com/discussions/forum") || /steamcommunity.com\/app\/.+\/discussions/g.test(str) || /steamcommunity.com\/groups\/.+\/discussions/g.test(str)) { + logger("debug", "handleSteamIdResolving: User provided discussion link..."); + + idType = "discussion"; + + if (expectedIdType && idType != expectedIdType) callback(`Received steamID of type ${idType} but expected ${expectedIdType}.`, null, null); + else callback(null, str, idType); + + } else if (str.includes("steamcommunity.com/groups/")) { // Must be below the discussion check because of the "groups/abc/discussion" regex above logger("debug", "handleSteamIdResolving: User provided group link..."); steamIDResolver.groupUrlToGroupID64(str, handleResponse); @@ -92,7 +101,22 @@ Controller.prototype.handleSteamIdResolving = (str, expectedIdType, callback) => else callback(null, str, idType); }); - } else { // Doesn't seem to be an URL + } else if (str.includes("store.steampowered.com/curator/")) { + logger("debug", "handleSteamIdResolving: User provided curator link..."); + + // Cut domain away + let split = str.replace("/?appid=", "").split("/"); // Remove any trailing app id, we don't exactly know what the user provided + if (split[split.length - 1] == "") split.pop(); // Remove trailing slash (which is now a space because of split("/")) + + str = split[split.length - 1].split("-")[0]; + + // Update idType + idType = "curator"; + + if (expectedIdType && idType != expectedIdType) callback(`Received steamID of type ${idType} but expected ${expectedIdType}.`, null, null); + else callback(null, str, idType); + + } else { // Doesn't seem to be an URL. We can ignore discussions as we need to provide an URL to SteamCommunity. // If user just provided the customURL part of the URL then try and figure out from the expected expectedIdType if this could be a profile or group customURL if (expectedIdType == "profile") { diff --git a/src/controller/helpers/misc.js b/src/controller/helpers/misc.js index c3c9d154..723f03db 100644 --- a/src/controller/helpers/misc.js +++ b/src/controller/helpers/misc.js @@ -4,7 +4,7 @@ * Created Date: 25.03.2023 14:02:56 * Author: 3urobeat * - * Last Modified: 08.07.2023 00:47:33 + * Last Modified: 08.10.2023 16:59:24 * Modified By: 3urobeat * * Copyright (c) 2023 3urobeat @@ -15,13 +15,14 @@ */ +const http = require("http"); const https = require("https"); /** * Implementation of a synchronous for loop in JS (Used as reference: https://whitfin.io/handling-synchronous-asynchronous-loops-javascriptnode-js/) * @param {number} iterations The amount of iterations - * @param {function(): void} func The function to run each iteration (Params: loop, index) + * @param {function(object, number): void} func The function to run each iteration (Params: loop, index) * @param {function(): void} exit This function will be called when the loop is finished */ module.exports.syncLoop = (iterations, func, exit) => { @@ -31,13 +32,15 @@ module.exports.syncLoop = (iterations, func, exit) => { // Construct loop object let loop = { next: function () { // Run next iteration + process.nextTick(() => { // Delay by one tick to fix weird infinite loop crash bug // Check if the next iteration is still allowed to run, otherwise stop by calling break if (currentIndex < iterations && !done) { func(loop, currentIndex); // Call function again with new index currentIndex++; } else { this.break(); - }1; + } + }); }, break: function () { // Break loop and call exit function done = true; @@ -90,12 +93,13 @@ module.exports.timeToString = (timestamp) => { /** - * Pings an URL to check if the service and this internet connection is working + * Pings a **https** URL to check if the service and this internet connection is working * @param {string} url The URL of the service to check - * @param {boolean} throwTimeout If true, the function will throw a timeout error if Steam can't be reached after 20 seconds + * @param {boolean} [throwTimeout=false] If true, the function will throw a timeout error if Steam can't be reached after 20 seconds + * @param {{ ip: string, port: number, username: string, password: string }} [proxy] Provide a proxy if the connection check should be made through a proxy instead of the local connection * @returns {Promise.<{ statusMessage: string, statusCode: number|null }>} Resolves on response code 2xx and rejects on any other response code. Both are called with parameter `response` (Object) which has a `statusMessage` (String) and `statusCode` (Number) key. `statusCode` is `null` if request failed. */ -module.exports.checkConnection = (url, throwTimeout) => { +module.exports.checkConnection = (url, throwTimeout = false, proxy) => { return new Promise((resolve, reject) => { // Start a 20 sec timeout to display an error when Steam can't be reached but also doesn't throw an error @@ -105,23 +109,91 @@ module.exports.checkConnection = (url, throwTimeout) => { timeoutTimeout = setTimeout(() => reject({ "statusMessage": "Timeout: Received no response within 20 seconds.", "statusCode": null }), 20000); } - https.get(url, function (res) { - if (throwTimeout) clearTimeout(timeoutTimeout); + // Use http and provide a proxy if requested - Credit: https://stackoverflow.com/a/49611762 + if (proxy) { // TODO: Missing authentication could perhaps cause errors here + let auth = "Basic " + Buffer.from(proxy.username + ":" + proxy.password).toString("base64"); // Construct autentication + + url = url.replace("https://", ""); // Remove preceding https:// from url + + // Connect to proxy server + http.request({ + host: proxy.ip, // IP address of proxy server + port: proxy.port, // Port of proxy server + method: "CONNECT", + path: url + ":443", // Some destination, add 443 port for https! + headers: { "Proxy-Authorization": auth }, + }).on("connect", (res, socket) => { + + if (res.statusCode === 200) { // Connected to proxy server, now ping the url + https.get({ + host: url, + path: "/", + agent: new https.Agent({ socket }), // Cannot use a default agent + }, (res) => { + resolve(res); + }); + } else { + reject({ "statusMessage": "Failed to connect to proxy server.", "statusCode": res.statusCode }); + } + + }).on("error", (err) => { + reject({ "statusMessage": "Failed to connect to proxy server. Error: " + err, "statusCode": null }); + }).end(); + + } else { + + https.get(url, (res) => { + if (throwTimeout) clearTimeout(timeoutTimeout); - if (res.statusCode >= 200 && res.statusCode < 300) resolve(res); - else reject(res); + if (res.statusCode >= 200 && res.statusCode < 300) resolve(res); + else reject(res); - }).on("error", function(err) { + }).on("error", (err) => { - if (throwTimeout) clearTimeout(timeoutTimeout); + if (throwTimeout) clearTimeout(timeoutTimeout); - reject({ "statusMessage": err.message, "statusCode": null }); - }); + reject({ "statusMessage": err.message, "statusCode": null }); + }); + } }); }; +/** + * Splits a HTTP proxy URL into its parts + * @param {string} url The HTTP proxy URL + * @returns {{ ip: string, port: number, username: string, password: string }} Object containing the proxy parts + */ +module.exports.splitProxyString = (url) => { // TODO: Missing authentication could perhaps cause errors here + + let obj = { ip: "", port: 0, username: "", password: "" }; + + if (!url) return obj; + + // Cut away http prefix + url = url.replace("http://", ""); + + // Split at @ to get username:pw and ip:port parts + url = url.split("@"); + + // Split both parts at : to separate the 4 different elements + let usernamePassword = url[0].split(":"); + let ipPort = url[1].split(":"); + + // Extract ip and port from ipPort and username and password from usernamePassword + obj.ip = ipPort[0]; + obj.port = ipPort[1]; + + obj.username = usernamePassword[0]; + obj.password = usernamePassword[1]; + + // Return result + return obj; + +}; + + /** * Helper function which attempts to cut Strings intelligently and returns all parts. It will attempt to not cut words & links in half. * It is used by the steamChatInteraction helper but can be used in plugins as well. diff --git a/src/controller/helpers/npminteraction.js b/src/controller/helpers/npminteraction.js index b1cf1066..dbeb1bc6 100644 --- a/src/controller/helpers/npminteraction.js +++ b/src/controller/helpers/npminteraction.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 08.07.2023 00:36:35 + * Last Modified: 29.09.2023 16:51:36 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -24,7 +24,7 @@ const { exec } = require("child_process"); // Wanted to do it with the npm packa * @param {function(string, string): void} logger The currently used logger function (real or fake, the caller decides) * @param {function(string|null, string|null): void} callback Called with `err` (String) and `stdout` (String) (npm response) parameters on completion */ -module.exports.reinstallAll = (logger, callback) => { +module.exports.reinstallAll = async (logger, callback) => { if (!fs.existsSync(srcdir + "/../node_modules")) { logger("info", "Creating node_modules folder..."); @@ -33,10 +33,14 @@ module.exports.reinstallAll = (logger, callback) => { logger("info", "Deleting node_modules folder content..."); } + // Check if package.json is missing + await require(srcdir + "/starter.js").checkAndGetFile("./package.json", logger, false, false); + + // Delete node_modules folder content fs.rm(srcdir + "/../node_modules", { recursive: true }, (err) => { if (err) return callback(err, null); - logger("info", "Running 'npm install --production'..."); + logger("info", "Running 'npm install --production'. This can take a moment, please wait..."); exec("npm install --production", { cwd: srcdir + "/.." }, (err, stdout) => { if (err) return callback(err, null); @@ -64,12 +68,12 @@ module.exports.update = (callback) => { * @param {function(string|null, string|null): void} callback Called with `err` (String) and `stdout` (String) (npm response) parameters on completion */ module.exports.updateFromPath = (path, callback) => { - logger("debug", `npminteraction update(): Running 'npm install --production' in '${path}'...`); + logger("debug", `npminteraction update(): Running 'npm install --production' in '${path}'. This can take a moment, please wait...`); exec("npm install --production", { cwd: path }, (err, stdout) => { if (err) return callback(err, null); - // Logger("info", `NPM Log:\n${stdout}`, true) //entire log (not using it rn to avoid possible confusion with vulnerabilities message) + // Logger("info", `NPM Log:\n${stdout}`, true) // Entire log (not using it rn to avoid possible confusion with vulnerabilities message) callback(null, stdout); }); diff --git a/src/controller/login.js b/src/controller/login.js index 1b64d689..1dccf018 100644 --- a/src/controller/login.js +++ b/src/controller/login.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 23.07.2023 13:50:31 + * Last Modified: 21.10.2023 12:52:52 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -46,9 +46,9 @@ Controller.prototype.login = function(firstLogin) { logger("", "", true); // Put one line above everything that will come to make the output cleaner /* ------------ Log comment related config settings: ------------ */ - let maxCommentsOverall = this.data.config.maxOwnerComments; // Define what the absolute maximum is which the bot is allowed to process. This should make checks shorter - if (this.data.config.maxComments > this.data.config.maxOwnerComments) maxCommentsOverall = this.data.config.maxComments; - logger("info", `Comment settings: commentdelay: ${this.data.config.commentdelay} | botaccountcooldown: ${this.data.config.botaccountcooldown} | maxCommentsOverall: ${maxCommentsOverall} | randomizeAcc: ${this.data.config.randomizeAccounts}`, false, true, logger.animation("loading")); + let maxRequestsOverall = this.data.config.maxOwnerRequests; // Define what the absolute maximum is which the bot is allowed to process. This should make checks shorter + if (this.data.config.maxRequests > this.data.config.maxOwnerRequests) maxRequestsOverall = this.data.config.maxRequests; + logger("info", `Comment settings: requestDelay: ${this.data.config.requestDelay} | botaccountcooldown: ${this.data.config.botaccountcooldown} | maxRequestsOverall: ${maxRequestsOverall} | randomizeAcc: ${this.data.config.randomizeAccounts}`, false, true, logger.animation("loading")); // Print whatsnew message if this is the first start with this version @@ -60,8 +60,8 @@ Controller.prototype.login = function(firstLogin) { let estimatedlogintime; // Only use "intelligent" evaluation method when the bot was started more than 5 times - if (this.data.datafile.timesloggedin < 5) estimatedlogintime = ((this.data.advancedconfig.loginDelay * (Object.keys(this.data.logininfo).length - 1 - this.info.skippedaccounts.length)) / 1000) + 5; // 5 seconds tolerance - else estimatedlogintime = ((this.data.datafile.totallogintime / this.data.datafile.timesloggedin) + (this.data.advancedconfig.loginDelay / 1000)) * (Object.keys(this.data.logininfo).length - this.info.skippedaccounts.length); + if (this.data.datafile.timesloggedin < 5) estimatedlogintime = ((this.data.advancedconfig.loginDelay * (this.data.logininfo.length - 1 - this.info.skippedaccounts.length)) / 1000) + 5; // 5 seconds tolerance + else estimatedlogintime = ((this.data.datafile.totallogintime / this.data.datafile.timesloggedin) + (this.data.advancedconfig.loginDelay / 1000)) * (this.data.logininfo.length - this.info.skippedaccounts.length); let estimatedlogintimeunit = "seconds"; if (estimatedlogintime > 60) { estimatedlogintime = estimatedlogintime / 60; estimatedlogintimeunit = "minutes"; } @@ -76,7 +76,7 @@ Controller.prototype.login = function(firstLogin) { else logger("debug", "Controller login(): Login requested, checking for any accounts currently offline..."); // Get array of all account names - let allAccounts = Object.keys(this.data.logininfo); + let allAccounts = this.data.logininfo.map((e) => e.accountName); // Filter accounts which were skipped allAccounts = allAccounts.filter(e => !this.info.skippedaccounts.includes(e)); @@ -93,9 +93,7 @@ Controller.prototype.login = function(firstLogin) { // Iterate over all accounts, use syncLoop() helper to make our job easier misc.syncLoop(allAccounts.length, (loop, i) => { - let k = this.data.logininfo[allAccounts[i]]; // Get logininfo for this account name - - // TODO: Check for connection loss timestamp when it is available and wait a bit before retrying a failed login. Maybe add reason why account was skipped to not reattempt steam guard login + let k = this.data.logininfo.find((e) => e.accountName == allAccounts[i]); // Get logininfo for this account name // Calculate wait time let waitTime = (this.info.lastLoginTimestamp + this.data.advancedconfig.loginDelay) - Date.now(); @@ -110,7 +108,7 @@ Controller.prototype.login = function(firstLogin) { if (!this.bots[k.accountName]) { logger("info", `Creating new bot object for ${k.accountName}...`, false, true, logger.animation("loading")); - this.bots[k.accountName] = new Bot(this, Object.keys(this.data.logininfo).indexOf(k.accountName)); // Create a new bot object for this account and store a reference to it + this.bots[k.accountName] = new Bot(this, k.index); // Create a new bot object for this account and store a reference to it } else { logger("debug", `Found existing bot object for ${k.accountName}! Reusing it...`, false, true, logger.animation("loading")); } @@ -121,9 +119,9 @@ Controller.prototype.login = function(firstLogin) { thisbot.loginData.logOnTries = 0; // Generate steamGuardCode with shared secret if one was provided - if (this.data.logininfo[k.accountName].sharedSecret) { + if (k.sharedSecret) { logger("debug", `Found shared_secret for bot${this.bots[k.accountName].index}! Generating AuthCode and adding it to logOnOptions...`); - this.data.logininfo[k.accountName].steamGuardCode = SteamTotp.generateAuthCode(this.data.logininfo[k.accountName].sharedSecret); + k.steamGuardCode = SteamTotp.generateAuthCode(k.sharedSecret); } // Login! diff --git a/src/data/data.json b/src/data/data.json index 353ac19b..024101b1 100644 --- a/src/data/data.json +++ b/src/data/data.json @@ -1,14 +1,14 @@ { - "version": "21306", - "versionstr": "2.13.6", + "version": "21400", + "versionstr": "2.14.0", "branch": "master", "filetostart": "./src/starter.js", "filetostarturl": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/master/src/starter.js", "mestr": "3urobeat", - "firststart": true, "aboutstr": "This bot was created by 3urobeat.\nGitHub: https://github.com/3urobeat/steam-comment-service-bot \nSteam: https://steamcommunity.com/id/3urobeat \nIf you like my work, any donation would be appreciated! https://github.com/sponsors/3urobeat", + "firststart": true, "compatibilityfeaturedone": false, - "whatsnew": "Reworked how commands accept and use IDs to greatly improve plugin support, fixed error when loading sharedfiles with incomplete breadcrumbs, fixed bug where the bot was waiting for a skipped account's object to be populated and more.", + "whatsnew": "Added support for commenting in discussions, following users/workshops/curators and setting specific games per account. Added a relogging system with proxy switching support, a multi-language system, fixed a lot of bugs and more. Read the full release notes on GitHub!", "timesloggedin": 0, "totallogintime": 0 } \ No newline at end of file diff --git a/src/data/fileStructure.json b/src/data/fileStructure.json new file mode 100644 index 00000000..7c9e53dc --- /dev/null +++ b/src/data/fileStructure.json @@ -0,0 +1,814 @@ +{ + "files": [ + { + "path": ".eslintrc.json", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/.eslintrc.json", + "checksum": "c720367ec1727b2d3af2c42c495d0865" + }, + { + "path": ".github/FUNDING.yml", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/.github/FUNDING.yml", + "checksum": "4cb774628491c79bb0ad9e16c0bbfa05" + }, + { + "path": ".github/ISSUE_TEMPLATE/bug_report.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/.github/ISSUE_TEMPLATE/bug_report.md", + "checksum": "324472c36f99a41a0eb39f6875e11a56" + }, + { + "path": ".github/ISSUE_TEMPLATE/feature_request.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/.github/ISSUE_TEMPLATE/feature_request.md", + "checksum": "434308eeb7670ecc4fa63908eb6f9f56" + }, + { + "path": ".github/ISSUE_TEMPLATE/help-wanted-question.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/.github/ISSUE_TEMPLATE/help-wanted-question.md", + "checksum": "d341d7c8a2428d5c4a495508d5400b7d" + }, + { + "path": ".github/workflows/codeql-analysis.yml", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/.github/workflows/codeql-analysis.yml", + "checksum": "0de892fce9e6024fcb412abb1a8dd9af" + }, + { + "path": ".gitignore", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/.gitignore", + "checksum": "3d4645a3de082c935a60323291a8fe79" + }, + { + "path": ".prettierrc", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/.prettierrc", + "checksum": "07f3a7f724dd909ded2f149eef15fcc6" + }, + { + "path": "CONTRIBUTING.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/CONTRIBUTING.md", + "checksum": "0d0ca37121b61957bd4ae48f2323d8ba" + }, + { + "path": "LICENSE", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/LICENSE", + "checksum": "db95b6e40dc7d26d8308b6b7375637b6" + }, + { + "path": "README.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/README.md", + "checksum": "400ce1d775f3786989e41d968b8e7295" + }, + { + "path": "docs/dev/README.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/dev/README.md", + "checksum": "6d4f0ed91ea9f2e5ffe84cf6d3d7297e" + }, + { + "path": "docs/dev/bot/bot.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/dev/bot/bot.md", + "checksum": "cb6e6d58e0e779126c26575540d9f43b" + }, + { + "path": "docs/dev/bot/events.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/dev/bot/events.md", + "checksum": "502c61cdf9a2ca6f4f2167bc23d6a40b" + }, + { + "path": "docs/dev/commands/commandHandler.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/dev/commands/commandHandler.md", + "checksum": "f1cb35c8f444af288f27726312f4902f" + }, + { + "path": "docs/dev/controller/controller.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/dev/controller/controller.md", + "checksum": "3d71003e4f28373ba0c3e11d631bfcd3" + }, + { + "path": "docs/dev/controller/events.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/dev/controller/events.md", + "checksum": "cab6f4fe3d8b9fa0374bf9aaa33c965d" + }, + { + "path": "docs/dev/controller/helpers.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/dev/controller/helpers.md", + "checksum": "20ba311dfc161c2e9334c31b4d6be45c" + }, + { + "path": "docs/dev/dataManager/dataManager.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/dev/dataManager/dataManager.md", + "checksum": "99f5cc5569781f8d8447a81e089402b1" + }, + { + "path": "docs/dev/introduction.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/dev/introduction.md", + "checksum": "759da9533bfa9e1ad2c8775858cfbfc1" + }, + { + "path": "docs/dev/pluginSystem/pluginSystem.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/dev/pluginSystem/pluginSystem.md", + "checksum": "9cb903e73df0ee932204acf8c9a37603" + }, + { + "path": "docs/dev/sessionHandler/sessionHandler.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/dev/sessionHandler/sessionHandler.md", + "checksum": "c87d568c641b990d35e744ed14724c59" + }, + { + "path": "docs/dev/starter.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/dev/starter.md", + "checksum": "f1972b94bc0815b195efbbaa75f9fdbd" + }, + { + "path": "docs/dev/updater/helpers.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/dev/updater/helpers.md", + "checksum": "65cc9b10b57efb00a6e4f906bcf87d24" + }, + { + "path": "docs/dev/updater/updater.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/dev/updater/updater.md", + "checksum": "ffd7746cebdb184536a2a7534d3b27dc" + }, + { + "path": "docs/wiki/README.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/README.md", + "checksum": "5b1cda5f2b5b1bb9b22f06e67d584c49" + }, + { + "path": "docs/wiki/adding_proxies.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/adding_proxies.md", + "checksum": "30ea0432eff5f6da2db6ac924f7ed17d" + }, + { + "path": "docs/wiki/advancedconfig_doc.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/advancedconfig_doc.md", + "checksum": "880891465a10d7047811413ba80953bf" + }, + { + "path": "docs/wiki/changelogs/CHANGELOG_v1.x.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/changelogs/CHANGELOG_v1.x.md", + "checksum": "aa891c1eb106086c370b29248e5bd9fd" + }, + { + "path": "docs/wiki/changelogs/CHANGELOG_v2.0.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/changelogs/CHANGELOG_v2.0.md", + "checksum": "3a465c6291b614fde177b15f2164bf00" + }, + { + "path": "docs/wiki/changelogs/CHANGELOG_v2.1.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/changelogs/CHANGELOG_v2.1.md", + "checksum": "4caa5cf9fcbaa458a11d7d623610a232" + }, + { + "path": "docs/wiki/changelogs/CHANGELOG_v2.10.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/changelogs/CHANGELOG_v2.10.md", + "checksum": "6c81e8e766d209b1cff96e545d080a94" + }, + { + "path": "docs/wiki/changelogs/CHANGELOG_v2.11.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/changelogs/CHANGELOG_v2.11.md", + "checksum": "7f45470590ae0f0feb69db740c1b2b57" + }, + { + "path": "docs/wiki/changelogs/CHANGELOG_v2.12.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/changelogs/CHANGELOG_v2.12.md", + "checksum": "ae99343a608ea84dd068c576345fcfdd" + }, + { + "path": "docs/wiki/changelogs/CHANGELOG_v2.13.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/changelogs/CHANGELOG_v2.13.md", + "checksum": "fe8747967709f11a994fce0631717a48" + }, + { + "path": "docs/wiki/changelogs/CHANGELOG_v2.14.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/changelogs/CHANGELOG_v2.14.md", + "checksum": "994078e01b7dba9748f597e7969935b3" + }, + { + "path": "docs/wiki/changelogs/CHANGELOG_v2.2.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/changelogs/CHANGELOG_v2.2.md", + "checksum": "08de82dbd4e7e80db454b080727af76e" + }, + { + "path": "docs/wiki/changelogs/CHANGELOG_v2.3.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/changelogs/CHANGELOG_v2.3.md", + "checksum": "3b00e95bde44245f6c351cbe870c19db" + }, + { + "path": "docs/wiki/changelogs/CHANGELOG_v2.4.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/changelogs/CHANGELOG_v2.4.md", + "checksum": "990934d9604376e9a66ae3f64f2d822c" + }, + { + "path": "docs/wiki/changelogs/CHANGELOG_v2.5.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/changelogs/CHANGELOG_v2.5.md", + "checksum": "2bc2ab5b985fed509572a0819b94f5cc" + }, + { + "path": "docs/wiki/changelogs/CHANGELOG_v2.6.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/changelogs/CHANGELOG_v2.6.md", + "checksum": "0f58ef16e02db1b29e77e4b3d6ecdfd4" + }, + { + "path": "docs/wiki/changelogs/CHANGELOG_v2.7.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/changelogs/CHANGELOG_v2.7.md", + "checksum": "81d17c16437b3a20183e1987028a83bc" + }, + { + "path": "docs/wiki/changelogs/CHANGELOG_v2.8.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/changelogs/CHANGELOG_v2.8.md", + "checksum": "9b936af13e0a736e0f2dd8c091cd7f58" + }, + { + "path": "docs/wiki/changelogs/CHANGELOG_v2.9.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/changelogs/CHANGELOG_v2.9.md", + "checksum": "9c4e977cabebadd8b820859d9dce90de" + }, + { + "path": "docs/wiki/commands_doc.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/commands_doc.md", + "checksum": "cff834e22781d488cbfe9c63f974f6be" + }, + { + "path": "docs/wiki/config_doc.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/config_doc.md", + "checksum": "aa90b71640a8950d2e12fd39c015f5ea" + }, + { + "path": "docs/wiki/contributing.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/contributing.md", + "checksum": "eca7a629a5f0627dd6c23187c0fe4d43" + }, + { + "path": "docs/wiki/creating_plugins.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/creating_plugins.md", + "checksum": "c40d369f9401cf5980c6e562495688e5" + }, + { + "path": "docs/wiki/customlang_doc.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/customlang_doc.md", + "checksum": "64995a70b774fbcec3dbc10eb1e8119a" + }, + { + "path": "docs/wiki/data_doc.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/data_doc.md", + "checksum": "b39bfe322bd05b166656c3e9e31bf663" + }, + { + "path": "docs/wiki/errors_doc.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/errors_doc.md", + "checksum": "50a5970494d2361939ed599830e9a2de" + }, + { + "path": "docs/wiki/integrating_into_your_app.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/integrating_into_your_app.md", + "checksum": "bf709e0fe29b8fbabd82aa0bb82e22f6" + }, + { + "path": "docs/wiki/setup_guide.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/setup_guide.md", + "checksum": "ec0468f2883394a8f059b74377808dd4" + }, + { + "path": "docs/wiki/steam_limitations.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/steam_limitations.md", + "checksum": "01d282693d1d04ebfcdc4957241592e0" + }, + { + "path": "docs/wiki/version_changelogs.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/docs/wiki/version_changelogs.md", + "checksum": "41b3dd13d210e3d1196727eae8b31802" + }, + { + "path": "package-lock.json", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/package-lock.json", + "checksum": "db7cdd56a8392cdf6fef204a219efb1f" + }, + { + "path": "package.json", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/package.json", + "checksum": "5d0abc6d4dd621aaa67b4adb1c49d4b2" + }, + { + "path": "scripts/README.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/scripts/README.md", + "checksum": "a76e44a490b21758d75999e21c1867a6" + }, + { + "path": "scripts/checkTranslationKeys.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/scripts/checkTranslationKeys.js", + "checksum": "b1249b82bd312cff9cd78c4bf7ed871d" + }, + { + "path": "scripts/generateFileStructure.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/scripts/generateFileStructure.js", + "checksum": "cd95c47ae5b169d7ad0fa59074736e92" + }, + { + "path": "scripts/langStringsChangeDetector.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/scripts/langStringsChangeDetector.js", + "checksum": "032e423242c502d5aa9281f47e33d14b" + }, + { + "path": "src/bot/EStatus.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/bot/EStatus.js", + "checksum": "70f1dcd89d9cfb06fe95fd14af5c932b" + }, + { + "path": "src/bot/bot.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/bot/bot.js", + "checksum": "9c696caf975f0362c57ffac73a522665" + }, + { + "path": "src/bot/events/debug.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/bot/events/debug.js", + "checksum": "f464a1f981aa889138a7461c8adb5053" + }, + { + "path": "src/bot/events/disconnected.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/bot/events/disconnected.js", + "checksum": "df91bbc7cb99e19b0263caf705f37277" + }, + { + "path": "src/bot/events/error.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/bot/events/error.js", + "checksum": "0e11ff92d7b261c63ce56c65263b37fe" + }, + { + "path": "src/bot/events/friendMessage.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/bot/events/friendMessage.js", + "checksum": "662a8e4e64ca270f0efcbb688fb517ed" + }, + { + "path": "src/bot/events/loggedOn.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/bot/events/loggedOn.js", + "checksum": "034a7e300486ba09afc59cd0e2b52d6b" + }, + { + "path": "src/bot/events/relationship.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/bot/events/relationship.js", + "checksum": "c59e096971df409c17b07cf25c2ed167" + }, + { + "path": "src/bot/events/webSession.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/bot/events/webSession.js", + "checksum": "5ee6dbedbfbdb48981a0a87ae45fc31e" + }, + { + "path": "src/bot/helpers/checkMsgBlock.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/bot/helpers/checkMsgBlock.js", + "checksum": "12c1bb15717c09e15e91594dde46c44c" + }, + { + "path": "src/bot/helpers/handleLoginTimeout.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/bot/helpers/handleLoginTimeout.js", + "checksum": "08d684f7e3ea347fdd31a88e94aa461b" + }, + { + "path": "src/bot/helpers/handleMissingGameLicenses.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/bot/helpers/handleMissingGameLicenses.js", + "checksum": "841bf2256855624b39a5370a67c09bc0" + }, + { + "path": "src/bot/helpers/handleRelog.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/bot/helpers/handleRelog.js", + "checksum": "a2412b836b5b1f1d904b53b65ebad3c0" + }, + { + "path": "src/bot/helpers/steamChatInteraction.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/bot/helpers/steamChatInteraction.js", + "checksum": "8fb070e6107bf3dc30f1655f555ab986" + }, + { + "path": "src/commands/commandHandler.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/commandHandler.js", + "checksum": "106babe27b983ea4323ef749e34567bf" + }, + { + "path": "src/commands/core/block.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/core/block.js", + "checksum": "3bc4016e65ec6d05115a94690cbfa9cf" + }, + { + "path": "src/commands/core/comment.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/core/comment.js", + "checksum": "17a0106bd432bd48bb3fe4b31f0b45d1" + }, + { + "path": "src/commands/core/favorite.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/core/favorite.js", + "checksum": "c04d5a5108404fe2c553ef1ebf9615bf" + }, + { + "path": "src/commands/core/follow.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/core/follow.js", + "checksum": "fc00572ec05fce0040ee0eb518e8fa1d" + }, + { + "path": "src/commands/core/friend.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/core/friend.js", + "checksum": "d8bd04d6886e7bacba67398ef399f956" + }, + { + "path": "src/commands/core/general.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/core/general.js", + "checksum": "e030175359d73498f73d4d5853670a98" + }, + { + "path": "src/commands/core/group.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/core/group.js", + "checksum": "a8ff4e7451d846612be0b7cb0844d0ac" + }, + { + "path": "src/commands/core/requests.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/core/requests.js", + "checksum": "18c4250b933248e4f720d7b26e290194" + }, + { + "path": "src/commands/core/settings.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/core/settings.js", + "checksum": "dd9920d634a6e16ec557c481709eb8cb" + }, + { + "path": "src/commands/core/system.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/core/system.js", + "checksum": "8ff8780ee9d53d8aa7a2e582cc34817a" + }, + { + "path": "src/commands/core/vote.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/core/vote.js", + "checksum": "c5f08b08c385d58c36040c3c94c7bdd8" + }, + { + "path": "src/commands/helpers/getCommentArgs.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/helpers/getCommentArgs.js", + "checksum": "3172adbc009ed93fcc42edb0582d7636" + }, + { + "path": "src/commands/helpers/getCommentBots.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/helpers/getCommentBots.js", + "checksum": "eab2934e073186ad6e84ea8c3b1070d1" + }, + { + "path": "src/commands/helpers/getFavoriteBots.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/helpers/getFavoriteBots.js", + "checksum": "448123c44d0cad61817147848471e60b" + }, + { + "path": "src/commands/helpers/getFollowArgs.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/helpers/getFollowArgs.js", + "checksum": "0b6c69516ccd3252150e0c43dd5d7478" + }, + { + "path": "src/commands/helpers/getFollowBots.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/helpers/getFollowBots.js", + "checksum": "8c6114bda954ef4b607cb3d5e2cda291" + }, + { + "path": "src/commands/helpers/getSharedfileArgs.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/helpers/getSharedfileArgs.js", + "checksum": "62b349a43320987dbad52582c662d27a" + }, + { + "path": "src/commands/helpers/getVoteBots.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/helpers/getVoteBots.js", + "checksum": "05693ef2a455eb2f6cd631e728b66506" + }, + { + "path": "src/commands/helpers/handleCommentSkips.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/helpers/handleCommentSkips.js", + "checksum": "e655f536e02eb5d49258a175965f103c" + }, + { + "path": "src/commands/helpers/handleFollowErrors.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/helpers/handleFollowErrors.js", + "checksum": "04b3bd7ca1886a82e188e9f79d650d54" + }, + { + "path": "src/commands/helpers/handleSharedfileErrors.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/commands/helpers/handleSharedfileErrors.js", + "checksum": "f465cd711557a8ce0d5790a969aca793" + }, + { + "path": "src/controller/controller.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/controller/controller.js", + "checksum": "3bc947b3df33b319add4c339e3f15a52" + }, + { + "path": "src/controller/events/ready.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/controller/events/ready.js", + "checksum": "de676915caebd1104ad694fa5e558171" + }, + { + "path": "src/controller/events/statusUpdate.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/controller/events/statusUpdate.js", + "checksum": "9b69dfb942a73cd5c794f685af8e05af" + }, + { + "path": "src/controller/events/steamGuardInput.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/controller/events/steamGuardInput.js", + "checksum": "dd3dbf7cbc552d46fb93e019d6a5184d" + }, + { + "path": "src/controller/helpers/friendlist.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/controller/helpers/friendlist.js", + "checksum": "115150407cf054d506eef7831e89022b" + }, + { + "path": "src/controller/helpers/getBots.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/controller/helpers/getBots.js", + "checksum": "f9239fdf6c149cedc79148e7a61dad41" + }, + { + "path": "src/controller/helpers/handleErrors.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/controller/helpers/handleErrors.js", + "checksum": "fbc07b473a36678f1efa80c0599ecb44" + }, + { + "path": "src/controller/helpers/handleSteamIdResolving.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/controller/helpers/handleSteamIdResolving.js", + "checksum": "e47fcc3f44e859835b8a9f7a332972eb" + }, + { + "path": "src/controller/helpers/logger.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/controller/helpers/logger.js", + "checksum": "fe031f28b56d5bad03ae3fdfdea7b75b" + }, + { + "path": "src/controller/helpers/misc.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/controller/helpers/misc.js", + "checksum": "af5f340609cdb8674df993e23b505c84" + }, + { + "path": "src/controller/helpers/npminteraction.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/controller/helpers/npminteraction.js", + "checksum": "7ffd76e47873baf32b9ae7f0a94e672a" + }, + { + "path": "src/controller/login.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/controller/login.js", + "checksum": "05987c04879a38d7e91f7a5edacd2084" + }, + { + "path": "src/data/ascii.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/data/ascii.js", + "checksum": "224d7d7d0aba4f32819838d19b4b8caf" + }, + { + "path": "src/data/lang/english.json", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/data/lang/english.json", + "checksum": "609fb752852f8401ba5972668b74aa3c" + }, + { + "path": "src/data/lang/russian.json", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/data/lang/russian.json", + "checksum": "44c2754b505bd61026848a9d4a09cbff" + }, + { + "path": "src/dataManager/dataCheck.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/dataManager/dataCheck.js", + "checksum": "b5828e6f9ddfa5c042a5cbd282a34aa9" + }, + { + "path": "src/dataManager/dataExport.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/dataManager/dataExport.js", + "checksum": "40fc7a96f0c6052bd94c04fc11776fac" + }, + { + "path": "src/dataManager/dataImport.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/dataManager/dataImport.js", + "checksum": "ce6267ece2a38897e40a5ce64c9dd00b" + }, + { + "path": "src/dataManager/dataIntegrity.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/dataManager/dataIntegrity.js", + "checksum": "24e166de510b5c3d85b3351f7da68713" + }, + { + "path": "src/dataManager/dataManager.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/dataManager/dataManager.js", + "checksum": "b5b8f046cb80752b54c01005b62ec078" + }, + { + "path": "src/dataManager/dataProcessing.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/dataManager/dataProcessing.js", + "checksum": "e3927776806e2df7cda77eace731232d" + }, + { + "path": "src/dataManager/helpers/checkProxies.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/dataManager/helpers/checkProxies.js", + "checksum": "3bc2af113f042ae303dc2f9de2db199c" + }, + { + "path": "src/dataManager/helpers/getLang.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/dataManager/helpers/getLang.js", + "checksum": "cfd94dad095b3eb3fe7b879643ac6a54" + }, + { + "path": "src/dataManager/helpers/getQuote.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/dataManager/helpers/getQuote.js", + "checksum": "5290d6405e9280f22013e7d9ff935a88" + }, + { + "path": "src/dataManager/helpers/handleCooldowns.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/dataManager/helpers/handleCooldowns.js", + "checksum": "078a375e1fd1ee91c76a4d76de34d16c" + }, + { + "path": "src/dataManager/helpers/handleExpiringTokens.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/dataManager/helpers/handleExpiringTokens.js", + "checksum": "7dcd29cf3d3e6ed985d636615248a4f9" + }, + { + "path": "src/dataManager/helpers/misc.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/dataManager/helpers/misc.js", + "checksum": "19a6b14ff0c46f18c0fb72ecbdb2b37d" + }, + { + "path": "src/dataManager/helpers/refreshCache.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/dataManager/helpers/refreshCache.js", + "checksum": "f9530c810fa48d6601a1295f5519adb3" + }, + { + "path": "src/dataManager/helpers/repairFile.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/dataManager/helpers/repairFile.js", + "checksum": "775e0befcf862abe0d8b935b344542fa" + }, + { + "path": "src/libraryPatches/CSteamDiscussion.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/libraryPatches/CSteamDiscussion.js", + "checksum": "6abfc176a3eac92d5d85edd7e2453b29" + }, + { + "path": "src/libraryPatches/CSteamSharedFile.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/libraryPatches/CSteamSharedFile.js", + "checksum": "9f155bb4bded0f93b388399f17e859a8" + }, + { + "path": "src/libraryPatches/EDiscussionType.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/libraryPatches/EDiscussionType.js", + "checksum": "4bcb39b2300966555b79988af1c8fe90" + }, + { + "path": "src/libraryPatches/README.md", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/libraryPatches/README.md", + "checksum": "2c4baad1e1b0fa93057af168d761cab6" + }, + { + "path": "src/libraryPatches/discussions.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/libraryPatches/discussions.js", + "checksum": "2a666f5734c6247e6f6cf8163ec90161" + }, + { + "path": "src/libraryPatches/helpers.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/libraryPatches/helpers.js", + "checksum": "41131b373095152eabb073b46f25472f" + }, + { + "path": "src/libraryPatches/sharedfiles.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/libraryPatches/sharedfiles.js", + "checksum": "a33395c4ab436fa534e404487d8e7925" + }, + { + "path": "src/pluginSystem/handlePluginData.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/pluginSystem/handlePluginData.js", + "checksum": "ae241fda64252cee0afd36c2f3268903" + }, + { + "path": "src/pluginSystem/loadPlugins.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/pluginSystem/loadPlugins.js", + "checksum": "683f03c09451de7a9ef93b8d37996a98" + }, + { + "path": "src/pluginSystem/pluginSystem.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/pluginSystem/pluginSystem.js", + "checksum": "69c2ae86e0509f67daecdd0ee8953ecf" + }, + { + "path": "src/sessions/events/sessionEvents.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/sessions/events/sessionEvents.js", + "checksum": "88c4ee8faab2d785b1943dbdeda8ac9d" + }, + { + "path": "src/sessions/helpers/handle2FA.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/sessions/helpers/handle2FA.js", + "checksum": "649d4f56aea9ba9042e9ab626a1f436c" + }, + { + "path": "src/sessions/helpers/handleCredentialsLoginError.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/sessions/helpers/handleCredentialsLoginError.js", + "checksum": "5a74b41057e2c269d1f8e251eb31ea8f" + }, + { + "path": "src/sessions/helpers/tokenStorageHandler.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/sessions/helpers/tokenStorageHandler.js", + "checksum": "b613b839ad476c6bc67d90904ed32a0c" + }, + { + "path": "src/sessions/sessionHandler.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/sessions/sessionHandler.js", + "checksum": "5fc33e7e7dc1ae8af292d129cd77d486" + }, + { + "path": "src/starter.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/starter.js", + "checksum": "2a54dbf7a601a91b65b3db55e81ece7c" + }, + { + "path": "src/updater/compatibility/2060.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/updater/compatibility/2060.js", + "checksum": "1f75b416d4795c4533400f25acd41d93" + }, + { + "path": "src/updater/compatibility/2070.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/updater/compatibility/2070.js", + "checksum": "65338b222f5a366251ad58bf1c7d15b3" + }, + { + "path": "src/updater/compatibility/2080.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/updater/compatibility/2080.js", + "checksum": "b7613953b7a5e8854ba29ab8936fecd7" + }, + { + "path": "src/updater/compatibility/2100.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/updater/compatibility/2100.js", + "checksum": "d9b1324e4253d4dc62baf25612376501" + }, + { + "path": "src/updater/compatibility/2103.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/updater/compatibility/2103.js", + "checksum": "62308e61461f6b9bd2c1689fe6f35962" + }, + { + "path": "src/updater/compatibility/2104.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/updater/compatibility/2104.js", + "checksum": "9e00bc6b65df8fa30b0b75ef8a5d5835" + }, + { + "path": "src/updater/compatibility/21100.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/updater/compatibility/21100.js", + "checksum": "50e5f5aa9cd297a2251ff2853b51e36f" + }, + { + "path": "src/updater/compatibility/21200.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/updater/compatibility/21200.js", + "checksum": "faf0ddc2b7d0453c0998fe47a40cffca" + }, + { + "path": "src/updater/compatibility/21300.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/updater/compatibility/21300.js", + "checksum": "033c91364f191758b8b55b9ac15825b5" + }, + { + "path": "src/updater/compatibility/21400.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/updater/compatibility/21400.js", + "checksum": "eb3f572a93c5a4163a3fbd5bfa42054c" + }, + { + "path": "src/updater/compatibility.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/updater/compatibility.js", + "checksum": "4a7a25a98c8ecc45488ca4e503295d5d" + }, + { + "path": "src/updater/helpers/checkForUpdate.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/updater/helpers/checkForUpdate.js", + "checksum": "e076828239bdafa56e6d15ec0a459c42" + }, + { + "path": "src/updater/helpers/createBackup.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/updater/helpers/createBackup.js", + "checksum": "bb861304088be3a6261a33c5c17ebb63" + }, + { + "path": "src/updater/helpers/customUpdateRules.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/updater/helpers/customUpdateRules.js", + "checksum": "f11376db8f60880a9e92755fc6477dce" + }, + { + "path": "src/updater/helpers/downloadUpdate.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/updater/helpers/downloadUpdate.js", + "checksum": "429959acf0bcbcda2b35b78f723c0e3d" + }, + { + "path": "src/updater/helpers/prepareUpdate.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/updater/helpers/prepareUpdate.js", + "checksum": "966fe663568e3cac9af4f3f770f4b690" + }, + { + "path": "src/updater/helpers/restoreBackup.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/updater/helpers/restoreBackup.js", + "checksum": "4b2f7486bac2595ba9b4669d902a7bdf" + }, + { + "path": "src/updater/updater.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/src/updater/updater.js", + "checksum": "76bb596b8ed3a050d07286a568b1590b" + }, + { + "path": "start.js", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/start.js", + "checksum": "351e75059b7221227c899256ef706de1" + }, + { + "path": "types/types.d.ts", + "url": "https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/beta-testing/types/types.d.ts", + "checksum": "9f2514ea4fcc1b47ba530b743e075444" + } + ] +} \ No newline at end of file diff --git a/src/data/lang/defaultlang.json b/src/data/lang/defaultlang.json deleted file mode 100644 index f3b7f6d3..00000000 --- a/src/data/lang/defaultlang.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "note": "This file contains nearly all messages the bot sends to users. If you want to modify messages, read here: https://github.com/3urobeat/steam-comment-service-bot/blob/master/docs/wiki/customlang_doc.md", - - "updaterautoupdatedisabled": "You have disabled the automatic updater. Would you like to update now?\nIf yes, please force an update using the command: cmdprefixupdate true", - - "commentcmdusageowner": "'cmdprefixcomment amount/\"all\" id' to request amount many comments (or the max possible amount).\nProvide a profile, group or sharedfile (screenshot, artwork or guide) ID if you'd like to comment somewhere else instead of on your profile.", - "commentcmdusageowner2": "'cmdprefixcomment 1 id'. Provide a profile, group or sharedfile (screenshot, artwork or guide) ID if you'd like to comment somewhere else instead of on your profile.", - "commentcmdusage": "'cmdprefixcomment amount/\"all\"' to request amount many comments (or the max possible amount) on your profile.", - "commentcmdusage2": "'cmdprefixcomment' to request a comment on your profile!", - "commentrequesttoohigh": "You can request max. maxRequestAmount comments.\nCommand usage: commentcmdusage", - "commentinvalidid": "This does not seem to be a valid profile, group or sharedfile (screenshot, artwork or guide) ID!\nPlease make sure that you either provide a full link, only the vanity or only the ID, pointing to an existing profile, group or sharedfile.\n\nCommand usage: commentcmdusage", - "commentprofileidowneronly": "Specifying a ID is only allowed for bot owners.\nIf you are a bot owner, please make sure that you have added your ID to the ownerid setting in the config.json.", - "commentmissingnumberofcomments": "Please specify how many comments out of maxRequestAmount you would like to request.\nCommand usage: commentcmdusage", - "commentzeroavailableaccs": "Sorry but there are currently not enough accounts available to fulfill your request. Please wait waittime and try again!", - "commentnotenoughavailableaccs": "Sorry but there are currently not enough accounts available to fulfill your request. Please wait waittime and try again or only request availablenow comments now.", - "commentnoaccounts": "Sorry but there are no accounts to fulfill this request. Please contact the bot administrator of this instance.\nUse the cmdprefixowner command to get information about who runs this instance.", - "commentnounlimitedaccs": "Sorry but there are no unlimited accounts which are needed to fulfill this request. Please contact the bot administrator of this instance.\nUse the cmdprefixowner command to get information about who runs this instance.", - "commentaddbotaccounts": "Please add these accounts and then request again: (limited accounts)", - "commentuserprofileprivate": "Your/the receiving profile seems to be private. Please edit the privacy settings to allow comments and try again!", - "commenterroroccurred": "Oops, an error occurred! I sadly wasn't able to comment.", - "commentprocessstarted": "Estimated wait time for numberOfComments comments: waittime.", - "commentfailedcmdreference": "To get detailed information why which comment failed please type 'cmdprefixfailed'. You can read what probably caused your error here: https://github.com/3urobeat/steam-comment-service-bot/blob/master/docs/wiki/errors_doc.md", - - "comment429stop": "Stopped comment process because all proxies had a HTTP 429 (IP cooldown) error. Please try again later. Failed: failedamount/numberOfComments", - "commentretrying": "failedamount/numberOfComments comments have failed. I'm going to retry the failed comments in untilStr. (Attempt thisattempt/maxattempt)", - "commentsuccess": "All comments have been sent. Failed: failedamount/numberOfComments\nIf you are a nice person then please comment on my profile too!", - - "votenoaccounts": "Sorry but there are no unlimited accounts available which haven't voted on this item yet.", - "voterequestless": "There are currently only availablenow bot accounts available which have not yet voted on this item.", - "votenotenoughavailableaccs": "Sorry but there are currently not enough accounts available which haven't voted on this item yet. Please wait waittime and try again or only request availablenow votes now.", - "voteprocessstarted": "Estimated wait time for numberOfVotes votes: waittime.", - "votesuccess": "All votes have been sent. Failed: failedamount/numberOfVotes", - - "favoritenoaccounts": "Sorry but there are no accounts available for this request on this id.", - "favoriterequestless": "There are currently only availablenow bot accounts available for this request on this id.", - "favoritenotenoughavailableaccs": "Sorry but there are currently not enough accounts available for this request on this id. Please wait waittime and try again or only request availablenow now.", - "favoriteprocessstarted": "Estimated wait time for numberOfFavs un-/favorites: waittime.", - "favoritesuccess": "All un-/favorites have been sent. Failed: failedamount/numberOfFavs", - - "useradded": "Hello there! Thanks for adding me!\nRequest a free comment with cmdprefixcomment\nType cmdprefixhelp for more commands or cmdprefixabout for more information!", - "userunfriend": "You have been unfriended for being inactive for forceFriendlistSpaceTime days as the friendlist was running low on space.\nIf you need me again, feel free to add me again!", - "userforceunfriend": "You have been unfriended for being inactive for unfriendtime days.\nIf you need me again, feel free to add me again!", - - "userspamblock": "You have been blocked for 90 seconds for spamming.", - "usernotfriend": "Please add me before using a command!", - "botnotready": "The bot is not completely started yet. Please wait a moment before using a command.", - "commandnotfound": "I don't know that command. Type cmdprefixhelp for more info.", - "commandowneronly": "This command is only available for owners.\nIf you are the botowner, make sure you added your ownerid to the config.json.\nIf this request originates from a plugin, make sure to pass the userID & ownerIDs parameters.", - "nouserid": "The command was called without a userID! Blocking the command as I'm either unable to apply cooldowns or the default behavior of this command cannot be used without one. This is a coding issue which must be fixed by a developer.", - "noidparam": "Please provide an ID!\nThe default behavior of this command might be unavailable in this context, for example when the command was used from outside the Steam Chat or the developer forgot to pass a userID to enable it.", - - "invalidnumber": "This does not seem to be a valid number!\n\nCommand usage: cmdusage", - "invalidprofileid": "This does not seem to be a valid ID or link or you provided the wrong ID type for this command!\nPlease make sure that you either provide a full link, only the vanity or only the ID, pointing to an existing profile, group or sharedfile.", - "invalidgroupid": "This is not a valid group id or group url! \nA groupid must look like this: '103582791464712227' \n...or a group url like this: 'https://steamcommunity.com/groups/3urobeatGroup'", - "invalidsharedfileid": "This does not seem to be a valid sharedfileID!\nPlease make sure that you either provide a full link: https://steamcommunity.com/sharedfiles/filedetails/?id=2980913451 ...or just the ID: 2980913451 ...which points to an existing screenshot, artwork or guide.\n\nCommand usage: cmdusage", - "errloadingsharedfile": "Sorry but an error occurred while loading the sharedfile you have provided: ", - "idisownererror": "You are not allowed to provide the ID of an owner!", - "idalreadyreceiving": "This user or id already has an active request! Please wait for it to be completed before requesting again.", - "idoncooldown": "You only recently started a request. Please wait the remaining remainingcooldown before starting another request.", - "requestaborted": "Your active request was manually aborted by either yourself or an owner.\nsuccessAmount/totalAmount have been sent successfully.", - - "reloadcmdreloaded": "Reloaded all commands and plugins.", - "activerelog": "The bot is currently waiting for the last active requests to be finished in order to relog accounts.\nPlease wait a few minutes and try again.", - "updatecmdforce": "Forcing an update from the branchname branch...", - "updatecmdcheck": "Checking for an update in the branchname branch...", - "restartcmdrestarting": "Restarting...", - "stopcmdstopping": "Stopping...", - - "helpcommandlist": "Command list:", - "helpcommentowner1": "Request x many or the max amount of comments (max maxOwnerComments). Provide a profileid to comment on a specific profile.", - "helpcommentowner2": "Request 1 comment (max amount with current settings). Provide a profile id to comment on a specific profile.", - "helpcommentuser1": "Request x many or the max amount of comments (max maxComments).", - "helpcommentuser2": "Request a comment on your profile!", - "helpping": "Get a pong and response time from Steam in ms.", - "helpinfo": "Get useful information about the bot and you.", - "helpabort": "Abort your own requested comment process.", - "helpabout": "Returns information about this bot, including a link to GitHub.", - "helpowner": "Get a link to the profile of the operator/host of this bot instance.", - "helpjoingroup": "Join my 'cmdprefixgroup'!", - "helpreadothercmdshere": "To keep this message short please read all other commands here:", - - "pingcmdmessage": "Pong! 🏓\nTime to steamcommunity.com/ping response: pingtimems", - "ownercmdnolink": "The operator of this bot didn't include a link to him/herself.)", - "ownercmdmsg": "Check out my owner's profile: (for more information about the bot type cmdprefixabout)", - "groupcmdnolink": "The botowner of this instance hasn't provided any group or the group doesn't exist.", - "groupcmdinvitesent": "I sent you an invite! Thanks for joining!", - "groupcmdinvitelink": "Join my group here: ", - "abortcmdnoprocess": "There is no active comment process running for this ID.\nIf you requested comments for another profile, group or sharedfile then please provide that ID as argument!", - "abortcmdsuccess": "Aborting your active comment process...", - - "resetcooldowncmdcooldowndisabled": "The cooldown is disabled in the config!", - "resetcooldowncmdglobalreset": "The cooldown of all bot accounts has been reset.", - "resetcooldowncmdsuccess": "profileid's cooldown has been cleared.", - - "settingscmdfailedread": "Failed to read config to output current settings: ", - "settingscmdcurrentsettings": "Current settings:", - "settingscmdblockedvalues": "enableevalcmd, ownerid and owner can't be changed via the settings command for security reasons. Please do it directly in the config file.", - "settingscmdkeynotfound": "I can't find this key in the config.", - "settingscmdsamevalue": "The requested key is already value.", - "settingscmdvaluetoobig": "Your new value is too big. (32-bit integer limit)\nPlease choose a smaller value.", - "settingscmdvaluechanged": "targetkey has been changed from oldvalue to newvalue.\nPlease remember that certain values might need a restart to take effect. You can do that by typing cmdprefixrestart.", - - "failedcmdnothingfound": "I can't remember any failed comments on your/that ID.\nIf you requested comments for another profile, group or sharedfile then please provide that ID as argument!", - "failedcmdmsg": "Your last request for 'steamID64' finished at 'requesttime' (GMT time) had these errors:", - - "sessionscmdnosessions": "There are currently no active sessions and no bot accounts on cooldown.", - "sessionscmdmsg": "There are currently amount active session(s):", - "mysessionscmdnosessions": "There are currently no active sessions that you have started.", - - "addfriendcmdacclimited": "Can't add friend profileid with bot0 because the bot account is limited.", - "addfriendcmdsuccess": "Adding friend profileid with all bot accounts... This will take ~estimatedtime seconds. Please check the log for potential errors.", - "unfriendcmdsuccess": "I am unfriending you with all bot accounts. This will take a moment...\nYou can send me a friend request again at any time.", - "unfriendidcmdsuccess": "Removed friend profileid from all bot accounts.", - - "unfriendallcmdabort": "Aborting unfriendall process if one is active...", - "unfriendallcmdpending": "Unfriending all people (except owners) with all bot accounts in 30 seconds...\nType 'cmdprefixunfriendall abort' to abort/stop the process.", - "unfriendallcmdstart": "Starting to unfriend everyone...", - - "joingroupcmdsuccess": "Joining group 'groupid' with all bot accounts...", - - "leavegroupcmdsuccess": "Leaving group 'groupid' with all bot accounts...", - - "leaveallgroupscmdabort": "Aborting leaveallgroups process if one is active...", - "leaveallgroupscmdpending": "Leaving all groups (except yourgroup and botsgroup) with all bot accounts in 15 seconds...\nType 'cmdprefixleaveallgroups abort' to abort/stop the process.", - "leaveallgroupscmdstart": "Starting to leave all groups...", - - "blockcmdsuccess": "Blocked profileid with all bot accounts.", - "unblockcmdsuccess": "Unblocked profileid with all bot accounts.", - - "evalcmdturnedoff": "The eval command has been turned off!", - "evalcmdlogininfoblock": "Your code includes 'logininfo'. In order to protect passwords this is not allowed.", - - "childbotmessage": "This is one account running in a bot cluster.\nPlease add the main bot and send him a cmdprefixhelp message.\nIf you want to check out what this is about, type: cmdprefixabout\nThis is the main bot account:" -} \ No newline at end of file diff --git a/src/data/lang/english.json b/src/data/lang/english.json new file mode 100644 index 00000000..d0aee007 --- /dev/null +++ b/src/data/lang/english.json @@ -0,0 +1,143 @@ +{ + "note": "This file contains nearly all messages the bot sends to users. If you want to modify messages, read here: https://github.com/3urobeat/steam-comment-service-bot/blob/master/docs/wiki/customlang_doc.md", + + "langname": "english", + + "updaterautoupdatedisabled": "You have disabled the automatic updater. Would you like to update now?\nIf yes, please force an update using the command: ${cmdprefix}update true", + + "commentcmdusageowner": "'${cmdprefix}comment amount/\"all\" id' to request amount many comments (or the max possible amount).\nProvide a profile, group, sharedfile (screenshot, artwork or guide) or discussion ID/URL if you'd like to comment somewhere else instead of on your profile.", + "commentcmdusageowner2": "'${cmdprefix}comment 1 id'. Provide a profile, group, sharedfile (screenshot, artwork or guide) or discussion ID/URL if you'd like to comment somewhere else instead of on your profile.", + "commentcmdusage": "'${cmdprefix}comment amount/\"all\"' to request amount many comments (or the max possible amount) on your profile.", + "commentcmdusage2": "'${cmdprefix}comment' to request a comment on your profile!", + "commentrequesttoohigh": "You can request max. ${maxRequestAmount} comments.\nCommand usage: ${commentcmdusage}", + "commentinvalidid": "This does not seem to be a valid profile, group, sharedfile (screenshot, artwork or guide) or discussion ID/URL!\nPlease make sure that you either provide a full link, only the vanity or ID, pointing to an existing element.\n\nCommand usage: ${commentcmdusage}", + "commentprofileidowneronly": "Specifying a ID is only allowed for bot owners.\nIf you are a bot owner, please make sure that you have added your ID to the ownerid setting in the config.json.", + "commentmissingnumberofcomments": "Please specify how many comments out of ${maxRequestAmount} you would like to request.\nCommand usage: ${commentcmdusage}", + "commentzeroavailableaccs": "Sorry but there are currently not enough accounts available to fulfill your request. Please wait ${waittime} and try again!", + "commentnotenoughavailableaccs": "Sorry but there are currently not enough accounts available to fulfill your request. Please wait ${waittime} and try again or only request ${availablenow} comments now.", + "commentnoaccounts": "Sorry but there are no accounts to fulfill this request. Please contact the bot administrator of this instance.\nUse the ${cmdprefix}owner command to get information about who runs this instance.", + "commentnounlimitedaccs": "Sorry but there are no unlimited accounts which are needed to fulfill this request. Please contact the bot administrator of this instance.\nUse the ${cmdprefix}owner command to get information about who runs this instance.", + "commentaddbotaccounts": "Please add these accounts and then request again: (limited accounts)", + "commentunsupportedtype": "Sorry but the comment command does not support this type of ID. Please provide a profile, group, sharedfile (screenshot, artwork or guide) or discussion ID/URL.", + "commentuserprofileprivate": "Your/the receiving profile seems to be private. Please edit the privacy settings to allow comments and try again!", + "commenterroroccurred": "Oops, an error occurred! I sadly wasn't able to comment.", + "commentprocessstarted": "Estimated wait time for ${numberOfComments} comments: ${waittime}.", + "commentfailedcmdreference": "To get detailed information why which comment failed please type '${cmdprefix}failed'. You can read what probably caused your error here: https://github.com/3urobeat/steam-comment-service-bot/blob/master/docs/wiki/errors_doc.md", + + "comment429stop": "Stopped comment process because all proxies had a HTTP 429 (IP cooldown) error. Please try again later. Failed: ${failedamount}/${numberOfComments}", + "commentretrying": "${failedamount}/${numberOfComments} comments have failed. I'm going to retry the failed comments in untilStr. (Attempt ${thisattempt}/${maxattempt})", + "commentsuccess": "All comments have been sent. Failed: ${failedamount}/${numberOfComments}\nIf you are a nice person then please comment on my profile too!", + + "genericnoaccounts": "Sorry but there are no accounts available for this request on this id.", + "genericrequestless": "There are currently only ${availablenow} bot accounts available for this request on this id.", + "genericnotenoughavailableaccs": "Sorry but there are currently not enough accounts available for this request on this id. Please wait ${waittime} and try again or only request ${availablenow} now.", + + "voteprocessstarted": "Estimated wait time for ${numberOfVotes} votes: ${waittime}.", + "votesuccess": "All votes have been sent. Failed: ${failedamount}/${numberOfVotes}", + + "favoriteprocessstarted": "Estimated wait time for ${numberOfFavs} un-/favorites: ${waittime}.", + "favoritesuccess": "All un-/favorites have been sent. Failed: ${failedamount}/${numberOfFavs}", + + "followprocessstarted": "Estimated wait time for ${totalamount} un-/follows: ${waittime}.", + "followsuccess": "All un-/follows have been sent. Failed: ${failedamount}/${totalamount}", + + "useradded": "Hello there! Thanks for adding me!\nRequest a free comment with ${cmdprefix}comment\nType ${cmdprefix}help for more commands or ${cmdprefix}about for more information!", + "userunfriend": "You have been unfriended for being inactive for ${forceFriendlistSpaceTime} days as the friendlist was running low on space.\nIf you need me again, feel free to add me again!", + "userforceunfriend": "You have been unfriended for being inactive for ${unfriendtime} days.\nIf you need me again, feel free to add me again!", + + "userspamblock": "You have been blocked for 90 seconds for spamming.", + "usernotfriend": "Please add me before using a command!", + "botnotready": "The bot is not completely started yet. Please wait a moment before using a command.", + "commandnotfound": "I don't know that command. Type ${cmdprefix}help for more info.", + "commandowneronly": "This command is only available for owners.\nIf you are the botowner, make sure you added your ownerid to the config.json.\nIf this request originates from a plugin, make sure to pass the userID & ownerIDs parameters.", + "nouserid": "The command was called without a userID! Blocking the command as I'm either unable to apply cooldowns or the default behavior of this command cannot be used without one. This is a coding issue which must be fixed by a developer.", + "noidparam": "Please provide an ID!\nThe default behavior of this command might be unavailable in this context, for example when the command was used from outside the Steam Chat or the developer forgot to pass a userID to enable it.", + + "invalidnumber": "This does not seem to be a valid number!\n\nCommand usage: ${cmdusage}", + "invalidprofileid": "This does not seem to be a valid ID or link or you provided the wrong ID type for this command!\nPlease make sure that you either provide a full link, only the vanity or ID, pointing to an existing element.", + "invalidgroupid": "This is not a valid group id or group url! \nA groupid must look like this: '103582791464712227' \n...or a group url like this: 'https://steamcommunity.com/groups/3urobeatGroup'", + "invalidsharedfileid": "This does not seem to be a valid sharedfileID!\nPlease make sure that you either provide a full link: https://steamcommunity.com/sharedfiles/filedetails/?id=2980913451 ...or just the ID: 2980913451 ...which points to an existing screenshot, artwork or guide.\n\nCommand usage: ${cmdusage}", + "errloadingsharedfile": "Sorry but an error occurred while loading the sharedfile you have provided: ", + "idisownererror": "You are not allowed to provide the ID of an owner!", + "idalreadyreceiving": "This user or id already has an active request! Please wait for it to be completed before requesting again.", + "idoncooldown": "You only recently started a request. Please wait the remaining ${remainingcooldown} before starting another request.", + "requestaborted": "Your active request was manually aborted by either yourself or an owner.\n${successAmount}/${totalAmount} have been sent successfully.", + + "reloadcmdreloaded": "Reloaded all commands and plugins.", + "activerelog": "The bot is currently waiting for the last active requests to be finished in order to relog accounts.\nPlease wait a few minutes and try again.", + "updatecmdforce": "Forcing an update from the branchname branch...", + "updatecmdcheck": "Checking for an update in the branchname branch...", + "restartcmdrestarting": "Restarting...", + "stopcmdstopping": "Stopping...", + + "helpcommandlist": "Commands:", + "helpcommentowner": "Request max ${maxOwnerRequests} of comments for your or another profile", + "helpcommentuser": "Request max ${maxRequests} of comments for your profile", + "helpvote": "Request max ${maxRequests} of sharedfile votes", + "helpfavorite": "Request max ${maxRequests} of sharedfile favs", + "helpfollow": "Request max ${maxRequests} of workshop followers", + "helpinfo": "Information about the bot and you", + "helpabort": "Abort your own request", + "helpabout": "Information about this project", + "helpowner": "Information about who runs this bot instance", + "helpreadothercmdshere": "30+ more commands are listed here:", + + "pingcmdmessage": "Pong! 🏓\nTime to steamcommunity.com/ping response: ${pingtime}ms", + "ownercmdnolink": "The operator of this bot didn't include a link to him/herself.)", + "ownercmdmsg": "Check out my owner's profile: (for more information about the bot type ${cmdprefix}about)", + "groupcmdnolink": "The botowner of this instance hasn't provided any group or the group doesn't exist.", + "groupcmdinvitesent": "I sent you an invite! Thanks for joining!", + "groupcmdinvitelink": "Join my group here: ", + "abortcmdnoprocess": "There is no active process running for this ID.\nIf you requested comments for another profile, group or sharedfile then please provide that ID/URL as argument!", + "abortcmdsuccess": "Aborting an active process for your ID...", + + "resetcooldowncmdcooldowndisabled": "The cooldown is disabled in the config!", + "resetcooldowncmdglobalreset": "The cooldown of all bot accounts has been reset.", + "resetcooldowncmdsuccess": "${profileid}'s cooldown has been cleared.", + + "langcmdsupported": "The bot supports these languages:", + "langcmdnotsupported": "This language is sadly not supported. Please choose a language from this list: ${supportedlangs}\n\nYou would like to contribute an unsupported language? Read here: ' https://github.com/3urobeat/steam-comment-service-bot/blob/master/docs/wiki/contributing.md#translating '", + "langcmdsuccess": "Your language has been changed!", + + "settingscmdfailedread": "Failed to read config to output current settings: ", + "settingscmdcurrentsettings": "Current settings:", + "settingscmdblockedvalues": "enableevalcmd, ownerid and owner can't be changed via the settings command for security reasons. Please do it directly in the config file.", + "settingscmdcouldnotconvert": "I was unable to convert your input. Please make sure your syntax is correct.\nError: ", + "settingscmdkeynotfound": "I can't find this key in the config.", + "settingscmdsamevalue": "The requested key is already ${value}.", + "settingscmdvaluetoobig": "Your new value is too big. (32-bit integer limit)\nPlease choose a smaller value.", + "settingscmdvaluereset": "The DataManager rejected this change with this warning:", + "settingscmdvaluechanged": "${targetkey} has been changed from ${oldvalue} to ${newvalue}.\nPlease remember that certain values might need a restart to take effect. You can do that by typing ${cmdprefix}restart.", + + "failedcmdnothingfound": "I can't remember any failed comments on your/that ID.\nIf you requested comments for another profile, group, sharedfile or discussion then please provide that ID/URL as argument!", + "failedcmdmsg": "Your last request for '${steamID64}' finished at '${requesttime}' (GMT time) had these errors:", + + "sessionscmdnosessions": "There are currently no active sessions and no bot accounts on cooldown.", + "sessionscmdmsg": "There are currently ${amount} active session(s):", + "mysessionscmdnosessions": "There are currently no active sessions that you have started.", + + "addfriendcmdacclimited": "Can't add friend ${profileid} with bot0 because the bot account is limited.", + "addfriendcmdsuccess": "Adding friend ${profileid} with all bot accounts... This will take ~estimatedtime seconds. Please check the log for potential errors.", + "unfriendcmdsuccess": "I am unfriending you with all bot accounts. This will take a moment...\nYou can send me a friend request again at any time.", + "unfriendidcmdsuccess": "Removed friend ${profileid} from all bot accounts.", + + "unfriendallcmdabort": "Aborting unfriendall process if one is active...", + "unfriendallcmdpending": "Unfriending all people (except owners) with all bot accounts in 30 seconds...\nType '${cmdprefix}unfriendall abort' to abort/stop the process.", + "unfriendallcmdstart": "Starting to unfriend everyone...", + + "joingroupcmdsuccess": "Joining group '${groupid}' with all bot accounts...", + + "leavegroupcmdsuccess": "Leaving group '${groupid}' with all bot accounts...", + + "leaveallgroupscmdabort": "Aborting leaveallgroups process if one is active...", + "leaveallgroupscmdpending": "Leaving all groups (except yourgroup and botsgroup) with all bot accounts in 15 seconds...\nType '${cmdprefix}leaveallgroups abort' to abort/stop the process.", + "leaveallgroupscmdstart": "Starting to leave all groups...", + + "blockcmdsuccess": "Blocked ${profileid} with all bot accounts.", + "unblockcmdsuccess": "Unblocked ${profileid} with all bot accounts.", + + "evalcmdturnedoff": "The eval command has been turned off!", + "evalcmdlogininfoblock": "Your code includes 'logininfo'. In order to protect passwords this is not allowed.", + + "childbotmessage": "This is one account running in a bot cluster.\nPlease add the main bot and send him a ${cmdprefix}help message.\nIf you want to check out what this is about, type: ${cmdprefix}about\nThis is the main bot account:" +} \ No newline at end of file diff --git a/src/data/lang/russian.json b/src/data/lang/russian.json index e56e4c21..6e1c193f 100644 --- a/src/data/lang/russian.json +++ b/src/data/lang/russian.json @@ -1,61 +1,67 @@ { "note": "Этот файл содержит почти все сообщения, которые бот посылает пользователям. Если вы хотите изменить сообщения, прочтите эту страницу: https://github.com/3urobeat/steam-comment-service-bot/blob/master/docs/wiki/customlang_doc.md", - "updaterautoupdatedisabled": "Вы отключили автоматическое обновление. Хотите ли вы обновить его сейчас?\nЕсли да, пожалуйста, запустите обновление с помощью команды: cmdprefixupdate true", - - "commentcmdusageowner": "«cmdprefixcomment кол-во/\"all\" id» для запроса amount комментариев (или максимально amount).\nУкажите ID профиля, группы или общего файла (скриншот, иллюстрация или руководство), если вы хотите комментировать не на своем профиле, а где-то ещё.", - "commentcmdusageowner2": "«cmdprefixcomment 1 id». Укажите ID профиля, группы или общего файла (скриншота, иллюстрации или руководства), если вы хотите комментировать не в своём профиле, а где-то ещё.", - "commentcmdusage": "«cmdprefixcomment кол-во/\"all\"» для запроса amount комментариев (или максимально amount) на вашем профиле.", - "commentcmdusage2": "«cmdprefixcomment», чтобы запросить комментарий к своему профилю!", - "commentrequesttoohigh": "Вы можете запросить максимум комментариев maxRequestAmount.\nИспользование команды: commentcmdusage", - "commentinvalidid": "Это не похоже на действительный ID профиля, группы или общего файла (скриншот, иллюстрация или руководство).\nПожалуйста, убедитесь, что вы предоставили либо полную ссылку, либо только пустое значение, либо только ID, указывающий на существующий профиль, группу или общий файл.\n\nИспользование команды: commentcmdusage", + "langname": "russian", + + "updaterautoupdatedisabled": "Вы отключили автоматическое обновление. Хотите ли вы обновить его сейчас?\nЕсли да, пожалуйста, запустите обновление с помощью команды: ${cmdprefix}update true", + + "commentcmdusageowner": "«${cmdprefix}comment кол-во/\"all\" id» для запроса amount комментариев (или максимально amount).\nУкажите ID профиля, группы, общего файла (скриншот, иллюстрация или руководство) или ID/URL обсуждения, если вы хотите комментировать не на своем профиле, а где-то ещё.", + "commentcmdusageowner2": "«${cmdprefix}comment 1 id». Укажите ID профиля, группы, общего файла (скриншота, иллюстрации или руководства) или ID/URL обсуждения, если вы хотите комментировать не в своём профиле, а где-то ещё.", + "commentcmdusage": "«${cmdprefix}comment кол-во/\"all\"» для запроса amount комментариев (или максимально amount) на вашем профиле.", + "commentcmdusage2": "«${cmdprefix}comment», чтобы запросить комментарий к своему профилю!", + "commentrequesttoohigh": "Вы можете запросить максимум комментариев ${maxRequestAmount}.\nИспользование команды: ${commentcmdusage}", + "commentinvalidid": "Это не похоже на действительный ID профиля, группы, общего файла (скриншот, иллюстрация или руководство) или ID/URL обсуждения!\nПожалуйста, убедитесь, что вы предоставили либо полную ссылку, либо только пустое значение, либо только ID, указывающий на существующий элемент.\n\nИспользование команды: ${commentcmdusage}", "commentprofileidowneronly": "Указание ID разрешено только для владельцев ботов.\nЕсли вы являетесь владельцем бота, пожалуйста, убедитесь, что вы добавили свой ID в параметр ownerid в config.json.", - "commentmissingnumberofcomments": "Пожалуйста, укажите, сколько комментариев из maxRequestAmount вы хотите запросить.\nИспользование команды: commentcmdusage", - "commentzeroavailableaccs": "Извините, но в настоящее время нет достаточного количества аккаунтов для выполнения вашего запроса. Пожалуйста, подождите waittime и повторите попытку!", - "commentnotenoughavailableaccs": "Извините, но в настоящее время нет достаточного количества аккаунтов для выполнения вашего запроса. Пожалуйста, подождите waittime и повторите попытку или запросите availablenow комментариев.", - "commentnoaccounts": "Извините, но для выполнения этого запроса нет аккаунтов. Пожалуйста, свяжитесь с администратором бота этого экземпляра.\nИспользуйте команду cmdprefixowner, чтобы получить информацию о том, кто управляет этим экземпляром.", - "commentnounlimitedaccs": "Извините, но нет неограниченных аккаунтов, которые необходимы для выполнения этого запроса. Пожалуйста, свяжитесь с администратором бота этого экземпляра.\nИспользуйте команду cmdprefixowner, чтобы получить информацию о том, кто управляет этим экземпляром.", + "commentmissingnumberofcomments": "Пожалуйста, укажите, сколько комментариев из ${maxRequestAmount} вы хотите запросить.\nИспользование команды: ${commentcmdusage}", + "commentzeroavailableaccs": "Извините, но в настоящее время нет достаточного количества аккаунтов для выполнения вашего запроса. Пожалуйста, подождите ${waittime} и повторите попытку!", + "commentnotenoughavailableaccs": "Извините, но в настоящее время нет достаточного количества аккаунтов для выполнения вашего запроса. Пожалуйста, подождите ${waittime} и повторите попытку или запросите ${availablenow} комментариев.", + "commentnoaccounts": "Извините, но для выполнения этого запроса нет аккаунтов. Пожалуйста, свяжитесь с администратором бота этого экземпляра.\nИспользуйте команду ${cmdprefix}owner, чтобы получить информацию о том, кто управляет этим экземпляром.", + "commentnounlimitedaccs": "Извините, но нет неограниченных аккаунтов, которые необходимы для выполнения этого запроса. Пожалуйста, свяжитесь с администратором бота этого экземпляра.\nИспользуйте команду ${cmdprefix}owner, чтобы получить информацию о том, кто управляет этим экземпляром.", "commentaddbotaccounts": "Пожалуйста, добавьте эти аккаунты, а затем запросите снова: (ограниченные аккаунты)", + "commentunsupportedtype": "Извините, но команда comment не поддерживает данный тип ID. Пожалуйста предоставьте профиль, группу, общий файл (скриншот, иллюстрация или руководство) или ID/URL обсуждения.", "commentuserprofileprivate": "Ваш/полученный профиль кажется скрытым. Пожалуйста, измените настройки приватности, чтобы разрешить комментарии, и повторите попытку!", "commenterroroccurred": "Опаньки, произошла ошибка! К сожалению, я не смог её прокомментировать.", - "commentprocessstarted": "Примерное время ожидания для numberOfComments комментариев: waittime.", - "commentfailedcmdreference": "Чтобы получить подробную информацию о том, почему комментарий не сработал, введите «cmdprefixfailed». Вы можете прочитать, что, вероятно, вызвало вашу ошибку, здесь: https://github.com/3urobeat/steam-comment-service-bot/blob/master/docs/wiki/errors_doc.md", - - "comment429stop": "Остановлен процесс комментирования, поскольку все прокси-серверы выдали ошибку HTTP 429 (восстановление IP). Пожалуйста, повторите попытку позже. Не удалось: failedamount/numberOfComments", - "commentretrying": "failedamount/numberOfComments комментариев были неудачными. Я собираюсь повторить попытку неудачных комментариев в untilStr. (Попытка thisattempt/maxattempt)", - "commentsuccess": "Все комментарии были отправлены. Не удалось: failedamount/numberOfComments\nЕсли вы хороший человек, то, пожалуйста, прокомментируйте и мой профиль!", - - "votenoaccounts": "Извините, но нет неограниченных аккаунтов, которые ещё не голосовали по этому пункту.", - "voterequestless": "В настоящее время доступно availablenow бот-аккаунтов, которые ещё не голосовали по этому пункту.", - "votenotenoughavailableaccs": "Извините, но в настоящее время недостаточно доступных аккаунтов, которые ещё не голосовали по этому пункту. Пожалуйста, подождите waittime и повторите попытку или запросите availablenow доступных сейчас.", - "voteprocessstarted": "Примерное время ожидания для numberOfVotes голосов: waittime.", - "votesuccess": "Все голоса были отправлены. Не удалось: failedamount/numberOfVotes", - - "favoritenoaccounts": "Извините, но для данного запроса нет доступных аккаунтов на этом ID.", - "favoriterequestless": "Сейчас для этого запроса на этом ID доступны только availablenow бот-аккаунтов.", - "favoritenotenoughavailableaccs": "Извините, но в настоящее время для этого запроса не хватает аккаунтов на этом ID. Пожалуйста, подождите waittime и повторите попытку, или запросите availablenow доступных сейчас.", - "favoriteprocessstarted": "Примерное время ожидания для numberOfFavs добавлений/удалений в избранное: waittime.", - "favoritesuccess": "Все добавления/удаления в избранное были отправлены. Не удалось: failedamount/numberOfFavs", + "commentprocessstarted": "Примерное время ожидания для ${numberOfComments} комментариев: ${waittime}.", + "commentfailedcmdreference": "Чтобы получить подробную информацию о том, почему комментарий не сработал, введите «${cmdprefix}failed». Вы можете прочитать, что, вероятно, вызвало вашу ошибку, здесь: https://github.com/3urobeat/steam-comment-service-bot/blob/master/docs/wiki/errors_doc.md", - "useradded": "Здравствуйте! Спасибо, что добавили меня!\nЗапросите бесплатный комментарий с помощью cmdprefixcomment\nВведите cmdprefixhelp для получения дополнительных команд или cmdprefixabout для получения дополнительной информации!", - "userunfriend": "Вы были удалены из друзей за неактивность в течение ForceFriendlistSpaceTime дн., так как в списке друзей было мало места.\nЕсли я вам снова понадоблюсь, добавляйте меня снова!", - "userforceunfriend": "Вы были удалены из друзей за неактивность в течение unfriendtime дн.\nЕсли я вам снова понадоблюсь, не стесняйтесь добавить меня снова!", + "comment429stop": "Остановлен процесс комментирования, поскольку все прокси-серверы выдали ошибку HTTP 429 (восстановление IP). Пожалуйста, повторите попытку позже. Не удалось: ${failedamount}/${numberOfComments}", + "commentretrying": "${failedamount}/${numberOfComments} комментариев были неудачными. Я собираюсь повторить попытку неудачных комментариев в untilStr. (Попытка ${thisattempt}/${maxattempt})", + "commentsuccess": "Все комментарии были отправлены. Не удалось: ${failedamount}/${numberOfComments}\nЕсли вы хороший человек, то, пожалуйста, прокомментируйте и мой профиль!", + + "genericnoaccounts": "Извините, но для данного запроса нет доступных аккаунтов на этом ID.", + "genericrequestless": "Сейчас для этого запроса на этом ID доступны только ${availablenow} бот-аккаунтов.", + "genericnotenoughavailableaccs": "Извините, но в настоящее время для этого запроса не хватает аккаунтов на этом ID. Пожалуйста, подождите ${waittime} и повторите попытку, или запросите ${availablenow} доступных сейчас.", + + "voteprocessstarted": "Примерное время ожидания для ${numberOfVotes} голосов: ${waittime}.", + "votesuccess": "Все голоса были отправлены. Не удалось: ${failedamount}/${numberOfVotes}", + + "favoriteprocessstarted": "Примерное время ожидания для ${numberOfFavs} добавлений/удалений в избранное: ${waittime}.", + "favoritesuccess": "Все добавления/удаления в избранное были отправлены. Не удалось: ${failedamount}/${numberOfFavs}", + + "followprocessstarted": "Примерное время ожидания для ${totalamount} подписок/отписок: ${waittime}.", + "followsuccess": "Все подписки/отписки были отправлены. Не удалось: ${failedamount}/${totalamount}", + + "useradded": "Здравствуйте! Спасибо, что добавили меня!\nЗапросите бесплатный комментарий с помощью ${cmdprefix}comment\nВведите ${cmdprefix}help для получения дополнительных команд или ${cmdprefix}about для получения дополнительной информации!", + "userunfriend": "Вы были удалены из друзей за неактивность в течение ${forceFriendlistSpaceTime} дн., так как в списке друзей было мало места.\nЕсли я вам снова понадоблюсь, добавляйте меня снова!", + "userforceunfriend": "Вы были удалены из друзей за неактивность в течение ${unfriendtime} дн.\nЕсли я вам снова понадоблюсь, не стесняйтесь добавить меня снова!", "userspamblock": "Вы были заблокированы на 90 секунд за спам.", "usernotfriend": "Пожалуйста, добавьте меня, прежде чем использовать команду!", "botnotready": "Бот ещё не полностью запущен. Пожалуйста, подождите немного, прежде чем использовать команду.", - "commandnotfound": "Я не знаю этой команды. Введите cmdprefixhelp для получения дополнительной информации.", - "commandowneronly": "Эта команда доступна только для владельцев.\nЕсли вы являетесь владельцем бота, убедитесь, что вы добавили свой ownerid в config.json.", + "commandnotfound": "Я не знаю этой команды. Введите ${cmdprefix}help для получения дополнительной информации.", + "commandowneronly": "Эта команда доступна только для владельцев.\nЕсли вы являетесь владельцем бота, убедитесь, что вы добавили свой ownerid в config.json.\nЕсли этот запрос вызван плагином, обязательно передайте параемтры userID и ownerIDs.", + "nouserid": "Эта команда была вызвана без ID пользователя! Блокирую команду, т.к. либо невозможно применить время восстановления, либо поведение по умолчанию этой команды не может быть использовано без него. Это проблема в коде, которая должна быть исправлена разработчиком.", + "noidparam": "Предоставьте ID!\nПоведение по умолчанию этой команды может быть недоступна в этом контексте, напимер когда команда была использована вне чата Steam, или разработчик забыл передать userID чтобы её включить.", - "invalidnumber": "Это не похоже на действительное число!\n\nИспользование команды: cmdusage", - "invalidprofileid": "Похоже, что это не действительный ID или ссылка, или вы указали неправильный тип ID для этой команды!\nПожалуйста, убедитесь, что вы указали либо полную ссылку, либо только пустое значение, либо только ID, указывающий на существующий профиль, группу или общий файл.", + "invalidnumber": "Это не похоже на действительное число!\n\nИспользование команды: ${cmdusage}", + "invalidprofileid": "Похоже, что это не действительный ID или ссылка, или вы указали неправильный тип ID для этой команды!\nПожалуйста, убедитесь, что вы указали либо полную ссылку, либо только пустое значение, либо только ID, указывающий на существующий элемент.", "invalidgroupid": "Это не правильный ID группы или ссылка группы! \ngroupid должен выглядеть следующим образом: «103582791464712227» \n...или ссылка группы должна выглядеть следующим образом: «https://steamcommunity.com/groups/3urobeatGroup»", - "invalidsharedfileid": "Похоже, что это не действительный ID общего файла!\nПожалуйста, убедитесь, что вы указали либо полную ссылку: https://steamcommunity.com/sharedfiles/filedetails/?id=2980913451 ...либо только ID: 2980913451 ...который указывает на существующий скриншот, иллюстрацию или руководство.\n\nИспользование команды: cmdusage", + "invalidsharedfileid": "Похоже, что это не действительный ID общего файла!\nПожалуйста, убедитесь, что вы указали либо полную ссылку: https://steamcommunity.com/sharedfiles/filedetails/?id=2980913451 ...либо только ID: 2980913451 ...который указывает на существующий скриншот, иллюстрацию или руководство.\n\nИспользование команды: ${cmdusage}", "errloadingsharedfile": "Извините, но при загрузке предоставленного вами файла sharedfile произошла ошибка: ", "idisownererror": "Вам не разрешается предоставлять ID владельца!", "idalreadyreceiving": "У этого пользователя или ID уже есть активный запрос! Пожалуйста, подождите, пока он будет завершён, прежде чем запрашивать снова.", - "idoncooldown": "Вы только недавно начали запрос. Пожалуйста, подождите оставшиеся remainingcooldown, прежде чем начинать другой запрос.", - "requestaborted": "Ваш активный запрос был вручную прерван вами или владельцем.\nsuccessAmount/totalAmount были отправлены успешно.", + "idoncooldown": "Вы только недавно начали запрос. Пожалуйста, подождите оставшиеся ${remainingcooldown}, прежде чем начинать другой запрос.", + "requestaborted": "Ваш активный запрос был вручную прерван вами или владельцем.\n${successAmount}/${totalAmount} были отправлены успешно.", "reloadcmdreloaded": "Перезагружены все команды и плагины.", "activerelog": "В настоящее время бот ожидает завершения последних активных запросов, чтобы заново войти.\nПожалуйста, подождите несколько минут и повторите попытку.", @@ -64,69 +70,74 @@ "restartcmdrestarting": "Перезагрузка...", "stopcmdstopping": "Остановка...", - "helpcommandlist": "Список команд:", - "helpcommentowner1": "Запросить x или максимальное количество комментариев (максимум maxOwnerComments). Укажите profileid, чтобы прокомментировать конкретный профиль.", - "helpcommentowner2": "Запросить 1 комментарий (максимально amount при текущих настройках). Укажите ID профиля, чтобы прокомментировать конкретный профиль.", - "helpcommentuser1": "Запросить x или максимальное количество комментариев (максимум maxComments).", - "helpcommentuser2": "Запросить комментарий к своему профилю!", - "helpping": "Получить пинг и время отклика от Steam в мс.", + "helpcommandlist": "Команды:", + "helpcommentowner": "Запросить максимум ${maxOwnerRequests} комментариев для вашего или другого профиля.", + "helpcommentuser": "Запросить максимум ${maxRequests}) комментариев для вашего профиля.", + "helpvote": "Запросить максимум ${maxRequests}) голосов на общих файлах", + "helpfavorite": "Запросить максимум ${maxRequests}) избранных на общих файлах", + "helpfollow": "Запросить максимум ${maxRequests}) подписчиков в мастерской", "helpinfo": "Получить полезную информацию о боте и о себе.", - "helpabort": "Прервать процесс комментирования по собственному запросу.", - "helpabout": "Выдать информацию об этом боте, включая ссылку на GitHub.", - "helpowner": "Получить ссылку на профиль оператора/организатора данного экземпляра бота.", - "helpjoingroup": "Присоединяйтесь к моей «cmdprefixgroup»!", - "helpreadothercmdshere": "Чтобы сделать это сообщение коротким, пожалуйста, прочитайте все остальные команды здесь:", + "helpabort": "Прервать ваш процесс", + "helpabout": "Информация о боте", + "helpowner": "Информация про оператора/организатора данного экземпляра бота", + "helpreadothercmdshere": "Ещё 30+ команд здесь:", - "pingcmdmessage": "Понг! 🏓\nВремя ответа steamcommunity.com/: pingtimems", + "pingcmdmessage": "Понг! 🏓\nВремя ответа steamcommunity.com/: ${pingtime}ms", "ownercmdnolink": "Оператор этого бота не указал ссылку на себя).", - "ownercmdmsg": "Посмотрите профиль моего владельца: (для получения дополнительной информации о боте введите cmdprefixabout)", + "ownercmdmsg": "Посмотрите профиль моего владельца: (для получения дополнительной информации о боте введите ${cmdprefix}about)", "groupcmdnolink": "Ботовладелец данного экземпляра не предоставил никакой группы или группа не существует.", "groupcmdinvitesent": "Я отправил вам приглашение! Спасибо, что присоединились!", "groupcmdinvitelink": "Присоединяйтесь к моей группе здесь: ", - "abortcmdnoprocess": "Для этого ID нет действующего процесса комментирования.\nЕсли вы запрашивали комментарии для другого профиля, группы или общего файла, укажите этот ID в качестве аргумента!", - "abortcmdsuccess": "Прерывание действующего процесса комментирования...", + "abortcmdnoprocess": "Для этого ID нет действующего процесса.\nЕсли вы запрашивали комментарии для другого профиля, группы или общего файла, укажите этот ID/URL в качестве аргумента!", + "abortcmdsuccess": "Прерывание одного действующего процесса для вашего ID...", "resetcooldowncmdcooldowndisabled": "В конфигурации отключено время восстановления!", "resetcooldowncmdglobalreset": "Время восстановления всех аккаунтов ботов было сброшено.", - "resetcooldowncmdsuccess": "Время восстановления profileid было сброшено.", + "resetcooldowncmdsuccess": "Время восстановления ${profileid} было сброшено.", + + "langcmdsupported": "Бот поддерживает следующие языки:", + "langcmdnotsupported": "К сожалению, данный язык не поддерживается. Пожалуйста, выберите язык из этого списка: ${supportedlangs}\n\nВы бы хотели добавить поддержку другого языка? Читайте здесь: ' https://github.com/3urobeat/steam-comment-service-bot/blob/master/docs/wiki/contributing.md#translating '", + "langcmdsuccess": "Язык успешно изменён!", "settingscmdfailedread": "Не удалось прочитать конфигурацию для вывода текущих настроек: ", "settingscmdcurrentsettings": "Текущие настройки:", "settingscmdblockedvalues": "enableevalcmd, ownerid и owner не могут быть изменены через команду settings по соображениям безопасности. Пожалуйста, сделайте это непосредственно в конфигурационном файле.", + "settingscmdcouldnotconvert": "Не получилось сконвертиовать ваш ввод. Убедитесь, что ввод синтаксически правильный.\nОшибка: ", "settingscmdkeynotfound": "Не могу найти этот ключ в конфигурации.", - "settingscmdsamevalue": "Запрашиваемый ключ уже является value.", + "settingscmdsamevalue": "Запрашиваемый ключ уже является ${value}.", "settingscmdvaluetoobig": "Ваше новое value слишком велико. (Ограничение на 32-разрядное целое число)\nПожалуйста, выберите меньшее value.", - "settingscmdvaluechanged": "targetkey был изменен со oldvalue на newvalue.\nПомните, что для вступления в силу некоторых значений может потребоваться перезапуск. Вы можете сделать это, введя cmdprefixrestart.", + "settingscmdvaluereset": "DataManager отклонил это изменение со следующим предупреждением:", + "settingscmdvaluechanged": "${targetkey} был изменен со ${oldvalue} на ${newvalue}.\nПомните, что для вступления в силу некоторых значений может потребоваться перезапуск. Вы можете сделать это, введя ${cmdprefix}restart.", - "failedcmdnothingfound": "Я не могу вспомнить ни одного неудачного комментария на вашем/этом ID.\nЕсли вы запрашивали комментарии для другого профиля, группы или общего файла, пожалуйста, укажите этот ID в качестве аргумента!", - "failedcmdmsg": "Ваш последний запрос «steamID64», выполненный в «requesttime» (время GMT), содержал эти ошибки:", + "failedcmdnothingfound": "Я не могу вспомнить ни одного неудачного комментария на вашем/этом ID.\nЕсли вы запрашивали комментарии для другого профиля, группы, общего файла или обсуждения, пожалуйста, укажите этот ID/URL в качестве аргумента!", + "failedcmdmsg": "Ваш последний запрос «${steamID64}», выполненный в «${requesttime}» (время GMT), содержал эти ошибки:", "sessionscmdnosessions": "В настоящее время нет активных сессий и нет бот-аккаунтов в режиме ожидания.", - "sessionscmdmsg": "В настоящее время есть amount активных сессий:", + "sessionscmdmsg": "В настоящее время есть ${amount} активных сессий:", "mysessionscmdnosessions": "В настоящее время нет активных сессий, которые вы начали.", - "addfriendcmdacclimited": "Невозможно добавить profileid в друзья с помощью bot0, потому что аккаунт бота ограничен.", - "addfriendcmdsuccess": "Добавление profileid в друзья ко всем аккаунтам ботов... Это займёт ~estimatedtime сек. Пожалуйста, проверьте журнал на наличие возможных ошибок.", + "addfriendcmdacclimited": "Невозможно добавить ${profileid} в друзья с помощью bot0, потому что аккаунт бота ограничен.", + "addfriendcmdsuccess": "Добавление ${profileid} в друзья ко всем аккаунтам ботов... Это займёт ~estimatedtime сек. Пожалуйста, проверьте журнал на наличие возможных ошибок.", "unfriendcmdsuccess": "Я удаляю вас из друзей со всех аккаунтов ботов. Это займёт некоторое время...\nВы можете отправить мне запрос в друзья в любое время.", - "unfriendidcmdsuccess": "Удалён profileid из друзей из всех аккаунтов ботов.", + "unfriendidcmdsuccess": "Удалён ${profileid} из друзей из всех аккаунтов ботов.", "unfriendallcmdabort": "Прерывание процесса удаления из списка друзей, если он активен...", - "unfriendallcmdpending": "Удаление всех людей (кроме владельцев) со всех аккаунтов бота за 30 секунд...\nВведите «cmdprefixunfriendall abort» для прерывания/остановки процесса.", + "unfriendallcmdpending": "Удаление всех людей (кроме владельцев) со всех аккаунтов бота за 30 секунд...\nВведите «${cmdprefix}unfriendall abort» для прерывания/остановки процесса.", "unfriendallcmdstart": "Начинаю всех удалять из друзей...", - "joingroupcmdsuccess": "Вступление в группу «groupid» со всеми аккаунтами ботов...", + "joingroupcmdsuccess": "Вступление в группу «${groupid}» со всеми аккаунтами ботов...", - "leavegroupcmdsuccess": "Покидаю группу «groupid» со всеми аккаунтами ботов...", + "leavegroupcmdsuccess": "Покидаю группу «${groupid}» со всеми аккаунтами ботов...", "leaveallgroupscmdabort": "Прерывание процесса покидания группы, если он активен...", - "leaveallgroupscmdpending": "Выход из всех групп (кроме yourgroup и botsgroup) со всеми аккаунтами ботов через 15 секунд...\nВведите «cmdprefixleaveallgroups abort» для прерывания/остановки процесса.", + "leaveallgroupscmdpending": "Выход из всех групп (кроме yourgroup и botsgroup) со всеми аккаунтами ботов через 15 секунд...\nВведите «${cmdprefix}leaveallgroups abort» для прерывания/остановки процесса.", "leaveallgroupscmdstart": "Начинаю покидать все группы...", - "blockcmdsuccess": "profileid заблокирован во всех бот-аккаунтах.", - "unblockcmdsuccess": "profileid разблокирован во всех бот-аккаунтах.", + "blockcmdsuccess": "${profileid} заблокирован во всех бот-аккаунтах.", + "unblockcmdsuccess": "${profileid} разблокирован во всех бот-аккаунтах.", "evalcmdturnedoff": "Команда eval была отключена!", "evalcmdlogininfoblock": "Ваш код включает «logininfo». В целях защиты паролей это запрещено.", - "childbotmessage": "Это один аккаунт, работающий в группе ботов.\nПожалуйста, добавьте главного бота и отправьте ему сообщение cmdprefixhelp.\nЕсли вы хотите проверить, в чём дело, введите: cmdprefixabout" -} \ No newline at end of file + "childbotmessage": "Это один аккаунт, работающий в группе ботов.\nПожалуйста, добавьте главного бота и отправьте ему сообщение ${cmdprefix}help.\nЕсли вы хотите проверить, в чём дело, введите: ${cmdprefix}about\nЭто главный аккаунт бота:" +} diff --git a/src/data/userSettings.db b/src/data/userSettings.db new file mode 100644 index 00000000..e69de29b diff --git a/src/dataManager/dataCheck.js b/src/dataManager/dataCheck.js index d4c544c6..57b83df5 100644 --- a/src/dataManager/dataCheck.js +++ b/src/dataManager/dataCheck.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 26.07.2023 15:43:26 + * Last Modified: 21.10.2023 12:55:46 * Modified By: 3urobeat * * Copyright (c) 2023 3urobeat @@ -15,7 +15,6 @@ */ -const fs = require("fs"); const os = require("os"); const steamIdResolver = require("steamid-resolver"); @@ -24,7 +23,7 @@ const DataManager = require("./dataManager.js"); /** * Checks currently loaded data for validity and logs some recommendations for a few settings. - * @returns {Promise.} Resolves promise when all checks have finished. If promise is rejected you should terminate the application or reset the changes. Reject is called with a String specifying the failed check. + * @returns {Promise.} Resolves with `null` when all settings have been accepted, or with a string containing reasons if a setting has been reset. On reject you should terminate the application. It is called with a String specifying the failed check. */ DataManager.prototype.checkData = function() { return new Promise((resolve, reject) => { @@ -35,6 +34,8 @@ DataManager.prototype.checkData = function() { this.controller.info.startupWarnings = 0; // Reset value to start fresh if this module should be integrated into a plugin or something like that + let resolveMsg = ""; // Collects all warnings from value resets to resolve with them at the end + // Display warning/notice if user is running in beta mode. Don't count this to startupWarnings if (this.datafile.branch == "beta-testing") { @@ -50,7 +51,7 @@ DataManager.prototype.checkData = function() { } - // Check config for default value leftovers and remove myself from config on different computer + // Check config for default value leftovers when the bot is not running on my machines if ((process.env.LOGNAME !== "tomg") || (os.hostname() !== "Toms-PC" && os.hostname() !== "Toms-Server" && os.hostname() !== "Toms-Thinkpad")) { let write = false; @@ -58,60 +59,50 @@ DataManager.prototype.checkData = function() { if (this.config.ownerid.includes("76561198260031749")) { this.config.ownerid.splice(this.config.ownerid.indexOf("76561198260031749"), 1); write = true; } if (this.config.ownerid.includes("3urobeat")) { this.config.ownerid.splice(this.config.ownerid.indexOf("3urobeat"), 1); write = true; } - // Moin Tom, solltest du in der Zukunft noch einmal auf dieses Projekt zurückschauen, dann hoffe ich dass du etwas sinnvolles mit deinem Leben gemacht hast. (08.06.2020) - // Dieses Projekt war das erste Projekt welches wirklich ein wenig Aufmerksamkeit bekommen hat. (1,5k Aufrufe in den letzten 14 Tagen auf GitHub, 1,3k Aufrufe auf mein YouTube Tutorial, 15k Aufrufe auf ein Tutorial zu meinem Bot von jemand fremden) - // Das Projekt hat schon bis jetzt viel Zeit in Anspruch genommen, die ersten Klausuren nach der Corona Pandemie haben bisschen darunter gelitten. All der Code ist bis auf einzelne, markierte Schnipsel selbst geschrieben. Node Version zum aktuellen Zeitpunkt: v12.16.3 - // Kleines Update: Das Repo hat letztens (am 17.03.2023) die 100 Sterne geknackt! - - if (write) { - // Get arrays on one line - let stringifiedconfig = JSON.stringify(this.config, function(k, v) { // Credit: https://stackoverflow.com/a/46217335/12934162 - if (v instanceof Array) return JSON.stringify(v); - return v; - }, 4) - .replace(/"\[/g, "[") - .replace(/\]"/g, "]") - .replace(/\\"/g, '"') - .replace(/""/g, '""'); - - fs.writeFile("./config.json", stringifiedconfig, (err) => { - if (err) logger("error", "Error cleaning config.json: " + err, true); - }); - } + if (write) this.writeConfigToDisk(); } // Check config values: - this.config.maxComments = Math.round(this.config.maxComments); // Round maxComments number every time to avoid user being able to set weird numbers (who can comment 4.8 times? right - no one) - this.config.maxOwnerComments = Math.round(this.config.maxOwnerComments); + this.config.maxRequests = Math.round(this.config.maxRequests); // Round maxRequests number every time to avoid user being able to set weird numbers (who can comment 4.8 times? right - no one) + this.config.maxOwnerRequests = Math.round(this.config.maxOwnerRequests); - let maxCommentsOverall = this.config.maxOwnerComments; // Define what the absolute maximum is which the bot is allowed to process. This should make checks shorter - if (this.config.maxComments > this.config.maxOwnerComments) maxCommentsOverall = this.config.maxComments; + let maxRequestsOverall = this.config.maxOwnerRequests; // Define what the absolute maximum is which the bot is allowed to process. This should make checks shorter + if (this.config.maxRequests > this.config.maxOwnerRequests) maxRequestsOverall = this.config.maxRequests; - if (Object.keys(this.logininfo).length == 0) { // Check real quick if logininfo is empty + if (this.logininfo.length == 0) { // Check real quick if logininfo is empty logWarn("error", `${logger.colors.fgred}Your accounts.txt or logininfo.json file doesn't seem to contain any valid login credentials! Aborting...`, true); return reject(new Error("No logininfo found!")); } - if (this.config.maxOwnerComments < 1) { - logWarn("info", `${logger.colors.fgred}Your maxOwnerComments value in config.json can't be smaller than 1! Automatically setting it to 1...`, true); - this.config.maxOwnerComments = 1; + if (this.config.maxOwnerRequests < 1) { + logWarn("info", `${logger.colors.fgred}Your maxOwnerRequests value in config.json can't be smaller than 1! Automatically setting it to 1...`, true); + resolveMsg += "Your maxOwnerRequests value in config.json can't be smaller than 1! Automatically setting it to 1...\n"; + this.config.maxOwnerRequests = 1; + } + if (this.config.requestDelay <= 500) { + logWarn("warn", `${logger.colors.fgred}Your requestDelay is set to a way too low value!\n Using a requestDelay of 500ms or less will result in an instant cooldown from Steam and therefore a failed comment request.\n Automatically setting it to the default value of 15 seconds...`, true); + resolveMsg += "Your requestDelay is set to a way too low value!\n Using a requestDelay of 500ms or less will result in an instant cooldown from Steam and therefore a failed comment request.\n Automatically setting it to the default value of 15 seconds...\n"; + this.config.requestDelay = 15000; } - if (this.config.commentdelay <= 500) { - logWarn("warn", `${logger.colors.fgred}Your commentdelay is set to a way too low value!\n Using a commentdelay of 500ms or less will result in an instant cooldown from Steam and therefore a failed comment request.\n Automatically setting it to the default value of 15 seconds...`, true); - this.config.commentdelay = 15000; + if (this.config.requestDelay / (maxRequestsOverall / 2) < 1250) { + logWarn("warn", `${logger.colors.fgred}You have raised maxRequests or maxOwnerRequests but I would recommend to raise the requestDelay further. Not increasing the requestDelay further raises the probability to get cooldown errors from Steam.`, true); } - if (this.config.commentdelay / (maxCommentsOverall / 2) < 1250) { - logWarn("warn", `${logger.colors.fgred}You have raised maxComments or maxOwnerComments but I would recommend to raise the commentdelay further. Not increasing the commentdelay further raises the probability to get cooldown errors from Steam.`, true); + if (this.config.requestDelay * maxRequestsOverall > 2147483647) { // Check for 32-bit integer limit for commentcmd timeout + logWarn("error", `${logger.colors.fgred}Your maxRequests and/or maxOwnerRequests and/or requestDelay value in the config are too high.\n Please lower these values so that 'requestDelay * maxRequests' is not bigger than 2147483647 (32-bit integer limit).\n\nThis will otherwise cause an error when trying to comment. Aborting...\n`, true); + this.config.requestDelay = 15000; + return reject(new Error("requestDelay times maxRequests exceeds 32bit integer limit!")); } - if (this.config.commentdelay * maxCommentsOverall > 2147483647) { // Check for 32-bit integer limit for commentcmd timeout - logWarn("error", `${logger.colors.fgred}Your maxComments and/or maxOwnerComments and/or commentdelay value in the config are too high.\n Please lower these values so that 'commentdelay * maxComments' is not bigger than 2147483647 (32-bit integer limit).\n\nThis will otherwise cause an error when trying to comment. Aborting...\n`, true); - return reject(new Error("Commentdelay times maxcomments exceeds 32bit integer limit!")); + if (this.config.randomizeAccounts && this.logininfo.length <= 5 && maxRequestsOverall > this.logininfo.length * 2) { + logWarn("warn", `${logger.colors.fgred}I wouldn't recommend using randomizeAccounts with 5 or less accounts when each account can/has to comment multiple times. The chance of an account getting a cooldown is higher.\n Please make sure your requestDelay is set adequately to reduce the chance of this happening.`, true); } - if (this.config.randomizeAccounts && Object.keys(this.logininfo).length <= 5 && maxCommentsOverall > Object.keys(this.logininfo).length * 2) { - logWarn("warn", `${logger.colors.fgred}I wouldn't recommend using randomizeAccounts with 5 or less accounts when each account can/has to comment multiple times. The chance of an account getting a cooldown is higher.\n Please make sure your commentdelay is set adequately to reduce the chance of this happening.`, true); + if (!Object.keys(this.lang).includes(this.config.defaultLanguage.toLowerCase())) { + logWarn("warn", `${logger.colors.fgred}You've set an unsupported language as defaultLanguage in your config.json. Please choose one of the following: ${Object.keys(this.lang).join(", ")}.\n Defaulting to English...`, true); + resolveMsg += `You've set an unsupported language as defaultLanguage in your config.json. Please choose one of the following: ${Object.keys(this.lang).join(", ")}. Defaulting to English...\n`; + this.config.defaultLanguage = "english"; } if (this.advancedconfig.loginDelay < 500) { // Don't allow a logindelay below 500ms logWarn("error", `${logger.colors.fgred}I won't allow a logindelay below 500ms as this will probably get you blocked by Steam nearly instantly. I recommend setting it to 2500.\n If you are using one proxy per account you might try setting it to 500 (on your own risk!). Aborting...`, true); + this.advancedconfig.loginDelay = 2500; return reject(new Error("Logindelay is set below 500ms!")); } if (this.advancedconfig.lastQuotesSize >= this.quotes) { // Force clear lastQuotes array if we have less or equal amount of quotes to choose from than lastQuotesSize to avoid infinite loop @@ -120,9 +111,10 @@ DataManager.prototype.checkData = function() { // Check language for too long strings and display warning. This will of course not catch replacements that happen at runtime but it's better than nothing - Object.keys(this.lang).forEach((e) => { - let val = this.lang[e]; - if (val.length > 500) logWarn("warn", `Your language string '${e}' is ${val.length} chars long! I will need to cut in parts to send it in the Steam Chat! Please consider reducing it to less than 500 chars.`, true); + Object.values(this.lang).forEach((translation) => { + Object.keys(translation).forEach((e) => { + if (translation[e].length > 500) logWarn("warn", `Your language string '${e}' of '${translation.langname}' is ${translation[e].length} chars long! I will need to cut in parts to send it in the Steam Chat! Please consider reducing it to less than 500 chars.`, true); + }); }); @@ -172,6 +164,6 @@ DataManager.prototype.checkData = function() { // Resolve promise if this point was reached logger("debug", "DataManager checkData(): All checks ran successfully! Resolving promise..."); - resolve(); + resolve(resolveMsg || null); }); }; \ No newline at end of file diff --git a/src/dataManager/dataImport.js b/src/dataManager/dataImport.js index f5d4b305..889350a1 100644 --- a/src/dataManager/dataImport.js +++ b/src/dataManager/dataImport.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 26.07.2023 17:07:58 + * Last Modified: 21.10.2023 13:01:03 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -96,7 +96,7 @@ DataManager.prototype._importFromDisk = async function () { if (_this.datafile && _this.datafile.firststart) { logger("", logger.colors.fgred + "\n--------------------------------------" + logger.colors.reset, true); logger("", `${logger.colors.fgcyan}Hey!${logger.colors.reset} It seems like this is your first start and you made a formatting mistake in your '${logger.colors.fgcyan}config.json${logger.colors.reset}' file. Because of this I'm sadly ${logger.colors.fgcyan}unable to load${logger.colors.reset} the file.`, true); - logger("", `You can stop the bot now by pressing ${logger.colors.fgcyan}CTRL+C${logger.colors.reset} to fix the issue. Please make sure that you exactly follow the format of the provided 'config.json' when filling in your settings.`, true); + logger("", `You can stop the bot now by pressing ${logger.colors.fgcyan}CTRL+C${logger.colors.reset} and fix the issue. Please make sure that you exactly follow the format of the provided 'config.json' when filling in your settings.`, true); logger("", `Take a look at the default config here and pay attention to every ${logger.colors.fgcyan}"${logger.colors.reset} and ${logger.colors.fgcyan},${logger.colors.reset} as you most likely forgot one of them: ${logger.colors.fgcyan}${logger.colors.underscore}https://github.com/3urobeat/steam-comment-service-bot/blob/master/config.json${logger.colors.reset}`, true); logger("", `You can also take a look at this blog post to learn more about JSON formatting: ${logger.colors.fgcyan}${logger.colors.underscore}https://stackoverflow.blog/2022/06/02/a-beginners-guide-to-json-the-data-format-for-the-internet/${logger.colors.reset}`, true); logger("", logger.colors.fgred + "--------------------------------------\n" + logger.colors.reset, true); @@ -138,7 +138,7 @@ DataManager.prototype._importFromDisk = async function () { function loadLoginInfo() { return new Promise((resolve) => { - let logininfo = {}; + let logininfo = []; // Check accounts.txt first so we can ignore potential syntax errors in logininfo if (fs.existsSync("./accounts.txt")) { @@ -147,24 +147,21 @@ DataManager.prototype._importFromDisk = async function () { if (data.length > 0 && data[0].startsWith("//Comment")) data = data.slice(1); // Remove comment from array if (data != "") { - logininfo = {}; // Set empty object - data.forEach((e) => { + data.forEach((e, i) => { if (e.length < 2) return; // If the line is empty ignore it to avoid issues like this: https://github.com/3urobeat/steam-comment-service-bot/issues/80 e = e.split(":"); e[e.length - 1] = e[e.length - 1].replace("\r", ""); // Remove Windows next line character from last index (which has to be the end of the line) - // Format logininfo object and use accountName as key to allow the order to change - logininfo[e[0]] = { + logininfo.push({ + index: i, accountName: e[0], password: e[1], sharedSecret: e[2], - steamGuardCode: null, - machineName: `${_this.datafile.mestr}'s Comment Bot`, // For steam-user - deviceFriendlyName: `${_this.datafile.mestr}'s Comment Bot`, // For steam-session - }; + steamGuardCode: null + }); }); - logger("info", `Found ${Object.keys(logininfo).length} accounts in accounts.txt, not checking for logininfo.json...`, false, true, logger.animation("loading")); + logger("info", `Found ${logininfo.length} accounts in accounts.txt, not checking for logininfo.json...`, false, true, logger.animation("loading")); return resolve(logininfo); } @@ -176,30 +173,30 @@ DataManager.prototype._importFromDisk = async function () { if (fs.existsSync("./logininfo.json")) { delete require.cache[require.resolve(srcdir + "/../logininfo.json")]; // Delete cache to enable reloading data - logininfo = require(srcdir + "/../logininfo.json"); - - // Reformat to use new logininfo object structure and use accountName as key instead of bot0 etc to allow the order to change - Object.keys(logininfo).forEach((k) => { - logininfo[logininfo[k][0]] = { - accountName: logininfo[k][0], - password: logininfo[k][1], - sharedSecret: logininfo[k][2], - steamGuardCode: null, - machineName: `${_this.datafile.mestr}'s Comment Bot`, // For steam-user - deviceFriendlyName: `${_this.datafile.mestr}'s Comment Bot`, // For steam-session - }; - - delete logininfo[k]; // Remove old entry + let logininfoFile = require(srcdir + "/../logininfo.json"); + + // Reformat to use new logininfo object structure + Object.keys(logininfoFile).forEach((k, i) => { + logininfo.push({ + index: i, + accountName: logininfoFile[k][0], + password: logininfoFile[k][1], + sharedSecret: logininfoFile[k][2], + steamGuardCode: null + }); }); } - logger("info", `Found ${Object.keys(logininfo).length} accounts in logininfo.json...`, false, true, logger.animation("loading")); + logger("info", `Found ${logininfo.length} accounts in logininfo.json...`, false, true, logger.animation("loading")); resolve(logininfo); } catch (err) { logger("error", "It seems like you made a mistake in your logininfo.json. Please check if your Syntax looks exactly like in the example/template and try again.\n " + err, true); return _this.controller.stop(); } + + // Create empty accounts.txt file if neither exist + if (!fs.existsSync("./accounts.txt")) _this._pullNewFile("accounts.txt", "./accounts.txt", () => {}, true); // Ignore resolve() param }); } @@ -208,13 +205,13 @@ DataManager.prototype._importFromDisk = async function () { let proxies = []; // When the file is just created there can't be proxies in it (this bot doesn't support magic) if (!fs.existsSync("./proxies.txt")) { - logger("info", "Creating proxies.txt file as it doesn't exist yet...", false, true, logger.animation("loading")); + logger("info", "Creating empty proxies.txt file because it doesn't exist...", false, true, logger.animation("loading")); + + _this.proxies = []; + _this.writeProxiesToDisk(); - fs.writeFile(srcdir + "/../proxies.txt", "", (err) => { - if (err) logger("error", "error creating proxies.txt file: " + err); - else logger("info", "Successfully created proxies.txt file.", false, true, logger.animation("loading")); - }); } else { + // File does seem to exist so now we can try and read it proxies = fs.readFileSync("./proxies.txt", "utf8").split("\n"); proxies = proxies.filter((str) => str != ""); // Remove empty lines @@ -223,6 +220,11 @@ DataManager.prototype._importFromDisk = async function () { if (_this.advancedconfig.useLocalIP) proxies.unshift(null); // Add no proxy (local ip) if useLocalIP is true + // Restructure array into array of objects + proxies.forEach((e, i) => { + proxies[i] = { proxyIndex: i, proxy: e, isOnline: true, lastOnlineCheck: 0 }; + }); + // Check if no proxies were found (can only be the case when useLocalIP is false) if (proxies.length == 0) { logger("", "", true); @@ -273,16 +275,23 @@ DataManager.prototype._importFromDisk = async function () { function loadLanguage() { return new Promise((resolve) => { try { - delete require.cache[require.resolve(srcdir + "/data/lang/defaultlang.json")]; // Delete cache to enable reloading data + let obj = {}; - resolve(require(srcdir + "/data/lang/defaultlang.json")); + delete require.cache[require.resolve(srcdir + "/data/lang/english.json")]; // Delete cache to enable reloading data + delete require.cache[require.resolve(srcdir + "/data/lang/russian.json")]; // Delete cache to enable reloading data + + obj["english"] = require(srcdir + "/data/lang/english.json"); + obj["russian"] = require(srcdir + "/data/lang/russian.json"); + + resolve(obj); } catch (err) { if (err) { // Corrupted! logger("", "", true, true); // Pull the file directly from GitHub. - _this._pullNewFile("defaultlang.json", "./src/data/lang/defaultlang.json", resolve); + _this._pullNewFile("english.json", "./src/data/lang/english.json", resolve); // Only resolve for the default language + _this._pullNewFile("english.json", "./src/data/lang/russian.json", () => {}); } } }); @@ -293,7 +302,7 @@ DataManager.prototype._importFromDisk = async function () { // Check before trying to import if the user even created the file if (fs.existsSync(srcdir + "/../customlang.json")) { let customlang; - let customlangkeys = 0; + let customlangkeys; // Try importing customlang.json try { @@ -306,21 +315,36 @@ DataManager.prototype._importFromDisk = async function () { resolve(_this.lang); // Resolve with default lang object } - // Overwrite values in lang object with values from customlang - Object.keys(customlang).forEach((e, i) => { - if (e != "" && e != "note") { - _this.lang[e] = customlang[e]; // Overwrite each defaultlang key with a corresponding customlang key if one is set + // Instantly resolve if nothing was found + if (Object.keys(customlang).length == 0) resolve(_this.lang); - customlangkeys++; - } + // Overwrite values in each lang object with values from customlang + Object.keys(customlang).forEach((lang, langIteration) => { + customlangkeys = 0; // Reset for this language + + // Check if valid language was provided + if (_this.lang[lang]) { - if (i == Object.keys(customlang).length - 1) { - // Check for last iteration - if (customlangkeys > 0) logger("info", `${customlangkeys} customlang key imported!`, false, true, logger.animation("loading")); - else logger("info", "No customlang keys found.", false, true, logger.animation("loading")); + Object.keys(customlang[lang]).forEach((e) => { // Note: No need to check for last iteration here as the loop does nothing asynchronous + if (e != "" && e != "note") { // Ignore empty strings and note + if (_this.lang[lang][e]) { + _this.lang[lang][e] = customlang[lang][e]; // Overwrite each english key with a corresponding customlang key if one is set - resolve(_this.lang); // Resolve lang object with our new keys + customlangkeys++; + } else { + logger("warn", `Customlang key '${e}' does not exist in language '${lang}'! You must update your customlang.json file. Ignoring this key...`, false, false, null, true); + } + } + }); + + if (customlangkeys > 0) logger("info", `${customlangkeys} customlang keys for language '${lang}' imported!`, false, true, logger.animation("loading")); + + } else { + logger("warn", `Language '${lang}' in customlang.json is not supported by the bot! You must update your customlang.json file. Ignoring this language...`, false, false, null, true); } + + // Resolve lang object with our new keys on the very last iteration + if (langIteration == Object.keys(customlang).length - 1) resolve(_this.lang); }); } else { logger("info", "No customlang.json file found...", false, true, logger.animation("loading")); @@ -346,6 +370,7 @@ DataManager.prototype._importFromDisk = async function () { this.lastCommentDB = new nedb({ filename: srcdir + "/data/lastcomment.db", autoload: true }); // Autoload this.ratingHistoryDB = new nedb({ filename: srcdir + "/data/ratingHistory.db", autoload: true }); this.tokensDB = new nedb({ filename: srcdir + "/data/tokens.db", autoload: true }); + this.userSettingsDB = new nedb({ filename: srcdir + "/data/userSettings.db", autoload: true }); // Check tokens.db every 24 hours for expired tokens to allow users to refresh them beforehand this._startExpiringTokensCheckInterval(); diff --git a/src/dataManager/dataIntegrity.js b/src/dataManager/dataIntegrity.js new file mode 100644 index 00000000..64fe05c1 --- /dev/null +++ b/src/dataManager/dataIntegrity.js @@ -0,0 +1,75 @@ +/* + * File: dataIntegrity.js + * Project: steam-comment-service-bot + * Created Date: 03.09.2023 09:52:15 + * Author: 3urobeat + * + * Last Modified: 05.09.2023 18:53:50 + * Modified By: 3urobeat + * + * Copyright (c) 2023 3urobeat + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. If not, see . + */ + + +const fs = require("fs"); +const path = require("path"); +const crypto = require("crypto"); + +const DataManager = require("./dataManager.js"); + + +/** + * Verifies the data integrity of every source code file in the project by comparing its checksum. + * This function is used to verify the integrity of every module loaded AFTER the controller & DataManager. Both of those need manual checkAndGetFile() calls to import, which is handled by the Controller. + * If an already loaded file needed to be recovered then the bot will restart to load these changes. + * @returns {Promise.} Resolves when all files have been checked and, if necessary, restored. Does not resolve if the bot needs to be restarted. + */ +DataManager.prototype.verifyIntegrity = function() { + return new Promise((resolve) => { + (async () => { // Lets us use await insidea Promise without creating an antipattern + + // Store all files which needed to be recovered to determine if we need to restart the bot + let invalidFiles = []; + + // Get fileStructure.json + const fileStructure = await this.checkAndGetFile("./src/data/fileStructure.json", logger, false, false); // Always forcing the latest version will lead to false-positives when user uses an older version + + // Generate a checksum for every file in fileStructure and compare them + let startDate = Date.now(); + + this.controller.misc.syncLoop(fileStructure.files.length, async (loop, i) => { + let e = fileStructure.files[i]; + + // Generate checksum for file if it exists, otherwise default to null + let filesum = fs.existsSync(e.path) ? crypto.createHash("md5").update(fs.readFileSync(e.path)).digest("hex") : null; + + if (filesum != e.checksum) { + logger("warn", `Checksum of file '${e.path}' does not match expectations! Restoring file...`, false, false, null, true); // Force print now + invalidFiles.push(e.path); + await this.checkAndGetFile("./" + e.path, logger, true, true); + } else { + // Logger("debug", `DataManager verifyIntegrity(): Successfully verified checksum of '${e.path}'`); + } + + loop.next(); + + }, () => { // Exit + + logger("debug", `DataManager verifyIntegrity(): Validating ${fileStructure.files.length} files took ${Date.now() - startDate}ms`); + + // Check if a file which has already been loaded was restored and restart the bot + if (invalidFiles.some((e) => Object.keys(require.cache).includes(path.resolve(e)))) { // If any file path, converted to absolute, is included in cache + logger("warn", "The application needs to restart as one of the restored files was already loaded. Restarting...", false, false, null, true); // Force print now + return this.controller.restart(); + } + + resolve(); + }); + + })(); + }); +}; \ No newline at end of file diff --git a/src/dataManager/dataManager.js b/src/dataManager/dataManager.js index e3732084..7f1030e7 100644 --- a/src/dataManager/dataManager.js +++ b/src/dataManager/dataManager.js @@ -4,7 +4,7 @@ * Created Date: 21.03.2023 22:34:51 * Author: 3urobeat * - * Last Modified: 26.07.2023 16:37:37 + * Last Modified: 21.10.2023 12:40:24 * Modified By: 3urobeat * * Copyright (c) 2023 3urobeat @@ -14,11 +14,12 @@ * You should have received a copy of the GNU General Public License along with this program. If not, see . */ -const fs = require("fs"); + const { default: Nedb } = require("@seald-io/nedb"); // eslint-disable-line const Controller = require("../controller/controller.js"); // eslint-disable-line + /** * Constructor - The dataManager system imports, checks, handles errors and provides a file updating service for all configuration files * @class @@ -53,9 +54,9 @@ const DataManager = function (controller) { this.advancedconfig = {}; /** - * Stores all language strings used for responding to a user. + * Stores all supported languages and their strings used for responding to a user. * All default strings have already been replaced with corresponding matches from `customlang.json`. - * @type {{[key: string]: string}} + * @type {{[key: string]: {[key: string]: string}}} */ this.lang = {}; @@ -67,7 +68,7 @@ const DataManager = function (controller) { /** * Stores all proxies provided via the `proxies.txt` file. - * @type {Array.} + * @type {Array.<{ proxy: string, proxyIndex: number, isOnline: boolean, lastOnlineCheck: number }>} */ this.proxies = []; @@ -79,55 +80,70 @@ const DataManager = function (controller) { /** * Stores the login information for every bot account provided via the `logininfo.json` or `accounts.txt` files. - * @type {{[key: string]: { accountName: string, password: string, sharedSecret?: string, steamGuardCode?: null, machineName?: string, deviceFriendlyName?: string }}} + * @type {Array.<{ index: number, accountName: string, password: string, sharedSecret?: string, steamGuardCode?: null, machineName?: string, deviceFriendlyName?: string }>} */ - this.logininfo = {}; + this.logininfo = []; /** * Database which stores the timestamp of the last request of every user. This is used to enforce `config.unfriendTime`. - * Document structure: { id: String, time: Number } + * Document structure: { id: string, time: Number } * @type {Nedb} */ this.lastCommentDB = {}; /** - * Database which stores information about which bot accounts have already voted on which sharedfiles. This allows us to filter without pinging Steam for every account on every request. - * Document structure: { id: String, accountName: String, type: String, time: Number } + * Database which stores information about which bot accounts have fulfilled one-time requests (vote, fav, follow). This allows us to filter without pinging Steam for every account on every request. + * Document structure: { id: string, accountName: string, type: string, time: Number } * @type {Nedb} */ this.ratingHistoryDB = {}; /** * Database which stores the refreshTokens for all bot accounts. - * Document structure: { accountName: String, token: String } + * Document structure: { accountName: string, token: string } * @type {Nedb} */ this.tokensDB = {}; + /** + * Database which stores user specific settings, for example the language set + * Document structure: { id: string, lang: string } + * @type {Nedb} + */ + this.userSettingsDB = {}; + // Stores a reference to the active handleExpiringTokens interval to prevent duplicates on reloads this._handleExpiringTokensInterval = null; - // Dynamically load all helper files - const loadHelpersFromFolder = (folder) => { - fs.readdirSync(folder).forEach(async (file) => { - if (!file.endsWith(".js")) return; +}; - const path = `${folder}/${file}`; - const getFile = await this.checkAndGetFile(path, controller.logger); - if (!getFile) logger("err", `Error! DataManager: Failed to load '${file}'!`); +/** + * Loads all DataManager helper files. This is done outside of the constructor to be able to await it. + * @returns {Promise.} Resolved when all files have been loaded + */ +DataManager.prototype._loadDataManagerFiles = function() { + return new Promise((resolve) => { + // The files need to be explicitly defined for restoring using checkAndGetFile to work + const helperPaths = [ + "dataCheck.js", "dataExport.js", "dataImport.js", "dataIntegrity.js", "dataProcessing.js", + "helpers/checkProxies.js", "helpers/getLang.js", "helpers/getQuote.js", "helpers/handleCooldowns.js", "helpers/handleExpiringTokens.js", "helpers/misc.js", "helpers/refreshCache.js", "helpers/repairFile.js" + ]; + + helperPaths.forEach(async (e, i) => { + const getFile = await this.checkAndGetFile("./src/dataManager/" + e, this.controller.logger); + if (!getFile) logger("err", `Error! DataManager: Failed to load '${e}'!`); + if (i + 1 == helperPaths.length) resolve(); }); - }; - - loadHelpersFromFolder("./src/dataManager"); - loadHelpersFromFolder("./src/dataManager/helpers"); + }); }; + /* -------- Register functions to let the IntelliSense know what's going on in helper files -------- */ /** * Checks currently loaded data for validity and logs some recommendations for a few settings. - * @returns {Promise.} Resolves promise when all checks have finished. If promise is rejected you should terminate the application or reset the changes. Reject is called with a String specifying the failed check. + * @returns {Promise.} Resolves promise when all checks have finished. If promise is rejected you should terminate the application or reset the changes. Reject is called with a string specifying the failed check. */ DataManager.prototype.checkData = function () {}; @@ -177,22 +193,54 @@ DataManager.prototype.writeQuotesToDisk = function() {}; */ DataManager.prototype._importFromDisk = async function () {}; +/** + * Verifies the data integrity of every source code file in the project by comparing its checksum. + * This function is used to verify the integrity of every module loaded AFTER the controller & DataManager. Both of those need manual checkAndGetFile() calls to import, which is handled by the Controller. + * If an already loaded file needed to be recovered then the bot will restart to load these changes. + * @returns {Promise.} Resolves when all files have been checked and, if necessary, restored. Does not resolve if the bot needs to be restarted. + */ +DataManager.prototype.verifyIntegrity = function() {}; + /** * Converts owners and groups imported from config.json to steam ids and updates cachefile. (Call this after dataImport and before dataCheck) */ DataManager.prototype.processData = async function() {}; +/** + * Checks if a proxy can reach steamcommunity.com and updates its isOnline and lastOnlineCheck + * @param {number} proxyIndex Index of the proxy to check in the DataManager proxies array + * @returns {boolean} True if the proxy can reach steamcommunity.com, false otherwise. + */ +DataManager.prototype.checkProxy = async function(proxyIndex) {}; // eslint-disable-line + +/** + * Checks all proxies if they can reach steamcommunity.com and updates their entries + * @param {number} [ignoreLastCheckedWithin=0] Ignore proxies that have already been checked in less than `ignoreLastCheckedWithin` ms + * @returns {Promise.} Resolves when all proxies have been checked + */ +DataManager.prototype.checkAllProxies = async function(ignoreLastCheckedWithin = 0) {}; // eslint-disable-line + +/** + * Retrieves a language string from one of the available language files and replaces keywords if desired. + * If a userID is provided it will lookup which language the user has set. If nothing is set, the default language set in the config will be returned. + * @param {string} str Name of the language string to be retrieved + * @param {{[key: string]: string}} [replace] Optional: Object containing keywords in the string to replace. Pass the keyword as key and the corresponding value to replace as value. + * @param {string} [userIDOrLanguage] Optional: ID of the user to lookup in the userSettings database. You can also pass the name of a supported language like "english" to get a specific language. + * @returns {Promise.} Returns a promise that resolves with the language string or `null` if it could not be found. + */ +DataManager.prototype.getLang = async function(str, replace = null, userIDOrLanguage = "") {}; // eslint-disable-line + /** * Gets a random quote * @param {Array} quotesArr Optional: Custom array of quotes to choose from. If not provided the default quotes set which was imported from the disk will be used. - * @returns {Promise.} Resolves with `quote` (String) + * @returns {Promise.} Resolves with `quote` (string) */ DataManager.prototype.getQuote = function (quotesArr = null) {}; // eslint-disable-line /** * Checks if a user ID is currently on cooldown and formats human readable lastRequestStr and untilStr strings. * @param {string} id ID of the user to look up - * @returns {Promise.<{ lastRequest: number, until: number, lastRequestStr: string, untilStr: string }|null>} Resolves with object containing `lastRequest` (Unix timestamp of the last interaction received), `until` (Unix timestamp of cooldown end), `lastRequestStr` (How long ago as String), `untilStr` (Wait until as String). If id wasn't found, `null` will be returned. + * @returns {Promise.<{ lastRequest: number, until: number, lastRequestStr: string, untilStr: string }|null>} Resolves with object containing `lastRequest` (Unix timestamp of the last interaction received), `until` (Unix timestamp of cooldown end), `lastRequestStr` (How long ago as string), `untilStr` (Wait until as string). If id wasn't found, `null` will be returned. */ DataManager.prototype.getUserCooldown = function (id) {}; // eslint-disable-line @@ -209,7 +257,7 @@ DataManager.prototype.setUserCooldown = function (id, timestamp) {}; // eslint-d DataManager.prototype._startExpiringTokensCheckInterval = () => {}; /** - * Internal: Asks user if he/she wants to refresh the tokens of all expiring accounts when no active request was found and relogs them + * Internal: Asks user if they want to refresh the tokens of all expiring accounts when no active request was found and relogs them * @param {object} expiring Object of botobject entries to ask user for */ DataManager.prototype._askForGetNewToken = function (expiring) {}; // eslint-disable-line @@ -250,7 +298,8 @@ DataManager.prototype._restoreBackup = function (name, filepath, cacheentry, onl * @param {function(any): void} resolve Your promise to resolve when file was pulled * @param {boolean} noRequire Optional: Set to true if resolve() should not be called with require(file) as param */ -DataManager.prototype._pullNewFile = async function (name, filepath, resolve) {}; // eslint-disable-line +DataManager.prototype._pullNewFile = async function (name, filepath, resolve, noRequire) {}; // eslint-disable-line + // Export our freshly baked bread module.exports = DataManager; diff --git a/src/dataManager/dataProcessing.js b/src/dataManager/dataProcessing.js index e14c3d1d..7ad2b720 100644 --- a/src/dataManager/dataProcessing.js +++ b/src/dataManager/dataProcessing.js @@ -4,7 +4,7 @@ * Created Date: 27.03.2023 21:34:45 * Author: 3urobeat * - * Last Modified: 04.07.2023 19:59:57 + * Last Modified: 05.09.2023 19:05:06 * Modified By: 3urobeat * * Copyright (c) 2023 3urobeat @@ -15,7 +15,6 @@ */ -const fs = require("fs"); const SteamID = require("steamid"); const steamIdResolver = require("steamid-resolver"); // My own library, cool right? @@ -161,8 +160,6 @@ DataManager.prototype.processData = async function() { // Process all three, then update cache.json await Promise.all([yourgroup(), botsgroup(), owners()]); - fs.writeFile(srcdir + "/data/cache.json", JSON.stringify(this.cachefile, null, 4), err => { - if (err) logger("error", `DataManager processData(): Error updating cache.json: ${err}`); - }); + this.writeCachefileToDisk(); }; \ No newline at end of file diff --git a/src/dataManager/helpers/checkProxies.js b/src/dataManager/helpers/checkProxies.js new file mode 100644 index 00000000..bf0eaf34 --- /dev/null +++ b/src/dataManager/helpers/checkProxies.js @@ -0,0 +1,64 @@ +/* + * File: checkProxies.js + * Project: steam-comment-service-bot + * Created Date: 09.10.2023 21:08:13 + * Author: 3urobeat + * + * Last Modified: 14.10.2023 10:41:56 + * Modified By: 3urobeat + * + * Copyright (c) 2023 3urobeat + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. If not, see . + */ + + +const DataManager = require("../dataManager"); + + +/** + * Checks if a proxy can reach steamcommunity.com and updates its isOnline and lastOnlineCheck + * @param {number} proxyIndex Index of the proxy to check in the DataManager proxies array + * @returns {boolean} True if the proxy can reach steamcommunity.com, false otherwise. + */ +DataManager.prototype.checkProxy = async function(proxyIndex) { + let { checkConnection, splitProxyString } = this.controller.misc; + + let thisProxy = this.proxies[proxyIndex]; + + // Check connection using checkConnection helper + await checkConnection("https://steamcommunity.com", true, thisProxy.proxy != null ? splitProxyString(thisProxy.proxy) : null) // Quick ternary to only split non-hosts + .then((res) => { + thisProxy.isOnline = res.statusCode >= 200 && res.statusCode < 300; // Check if response code is in 200 (OK) range + }) + .catch(() => { + thisProxy.isOnline = false; + }); + + thisProxy.lastOnlineCheck = Date.now(); + + // Return result of check above + return thisProxy.isOnline; +}; + + +/** + * Checks all proxies if they can reach steamcommunity.com and updates their entries + * @param {number} [ignoreLastCheckedWithin=0] Ignore proxies that have already been checked in less than `ignoreLastCheckedWithin` ms + * @returns {Promise.} Resolves when all proxies have been checked + */ +DataManager.prototype.checkAllProxies = async function(ignoreLastCheckedWithin = 0) { + let promiseArr = []; + + // Iterate over all proxies and call this.checkProxies(). We don't need any delay here as all requests go over different IPs + this.proxies.forEach((e) => { + if (ignoreLastCheckedWithin && ignoreLastCheckedWithin + e.lastOnlineCheck > Date.now()) return; // Ignore proxy if it has been checked recently + + promiseArr.push(this.checkProxy(e.proxyIndex)); + }); + + // Await all promises so this function resolves when all proxies have been checked + await Promise.all(promiseArr); +}; \ No newline at end of file diff --git a/src/dataManager/helpers/getLang.js b/src/dataManager/helpers/getLang.js new file mode 100644 index 00000000..5d0143ce --- /dev/null +++ b/src/dataManager/helpers/getLang.js @@ -0,0 +1,84 @@ +/* + * File: getLang.js + * Project: steam-comment-service-bot + * Created Date: 09.09.2023 12:35:10 + * Author: 3urobeat + * + * Last Modified: 10.09.2023 16:56:48 + * Modified By: 3urobeat + * + * Copyright (c) 2023 3urobeat + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. If not, see . + */ + + +const DataManager = require("../dataManager"); + + +/** + * Retrieves a language string from one of the available language files and replaces keywords if desired. + * If a userID is provided it will lookup which language the user has set. If nothing is set, the default language set in the config will be returned. + * @param {string} str Name of the language string to be retrieved + * @param {{[key: string]: string}} [replace] Optional: Object containing keywords in the string to replace. Pass the keyword as key and the corresponding value to replace as value. + * @param {string} [userIDOrLanguage] Optional: ID of the user to lookup in the userSettings database. You can also pass the name of a supported language like "english" to get a specific language. + * @returns {Promise.} Returns a promise that resolves with the language string or `null` if it could not be found. + */ +DataManager.prototype.getLang = async function(str, replace = null, userIDOrLanguage = "") { + + // Figure out which language to choose + let lang = this.lang[this.config.defaultLanguage.toLowerCase()]; // Set default + + if (userIDOrLanguage) { + if (Object.keys(this.lang).includes(userIDOrLanguage.toLowerCase())) { // Update if a supported language was passed + logger("debug", "DataManager getLang(): Supported specific language requested: " + userIDOrLanguage); + + lang = this.lang[userIDOrLanguage.toLowerCase()]; + + } else { // Search for user in database if this is an ID + + let res = await this.userSettingsDB.findOneAsync({ "id": userIDOrLanguage }, {}); + + if (res) { + lang = this.lang[res.lang]; + + logger("debug", `DataManager getLang(): Request for userID '${userIDOrLanguage}' resulted in '${res.lang}'`); + } else { + logger("debug", `DataManager getLang(): Unsupported language or userID '${userIDOrLanguage}' was provided, using default '${this.config.defaultLanguage}'...`); + } + } + } + + + // Retrieve the requested string + let langStr = lang[str]; + + if (!langStr) { + logger("err", `getLang(): I was unable to find the string '${str}' in the language file '${lang.langname}'!`); + return null; + } + + + // Modify the string if replace was passed + if (replace) { + Object.keys(replace).forEach((e) => { + // Add ${ prefix and } suffix to e + let rawPattern = "${" + e + "}"; + + // Skip iteration and display warning if the string does not contain the specified keyword + if (!langStr.includes(rawPattern)) return logger("warn", `getLang(): The string '${str}' of language '${lang.langname}' does not contain the provided keyword '${rawPattern}'!`); + + // Build regex pattern to dynamically replace all occurrences below. Escape rawPattern before concatenating to avoid special char issues later on + let regex = new RegExp(rawPattern.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"), "g"); // Regex credit: https://stackoverflow.com/a/17886301 + + langStr = langStr.replace(regex, replace[e]); + }); + } + + + // Resolve with the resulting string + return langStr; + +}; \ No newline at end of file diff --git a/src/dataManager/helpers/handleCooldowns.js b/src/dataManager/helpers/handleCooldowns.js index 47ef6e0c..38a3e8e5 100644 --- a/src/dataManager/helpers/handleCooldowns.js +++ b/src/dataManager/helpers/handleCooldowns.js @@ -4,7 +4,7 @@ * Created Date: 13.04.2023 17:58:23 * Author: 3urobeat * - * Last Modified: 10.07.2023 12:47:30 + * Last Modified: 19.10.2023 19:00:06 * Modified By: 3urobeat * * Copyright (c) 2023 3urobeat @@ -58,7 +58,7 @@ DataManager.prototype.getUserCooldown = function(id) { lastReq = Number(Math.round(lastReq+"e"+2)+"e-"+2); // Limit lastReq value to two decimals // Format untilStr - let until = Math.abs(((Date.now() - doc.time) / 1000) - (this.config.commentcooldown * 60)); + let until = Math.abs(((Date.now() - doc.time) / 1000) - (this.config.requestCooldown * 60)); let untilUnit = "seconds"; if (until > 60) { @@ -76,7 +76,7 @@ DataManager.prototype.getUserCooldown = function(id) { until = Number(Math.round(until+"e"+2)+"e-"+2); // Limit until value to two decimals obj.lastRequest = doc.time; - obj.until = doc.time + (this.config.commentcooldown * 60000); + obj.until = doc.time + (this.config.requestCooldown * 60000); obj.lastRequestStr = `${lastReq} ${lastReqUnit}`; obj.untilStr = `${until} ${untilUnit}`; diff --git a/src/dataManager/helpers/handleExpiringTokens.js b/src/dataManager/helpers/handleExpiringTokens.js index fd45aa36..dfe7af47 100644 --- a/src/dataManager/helpers/handleExpiringTokens.js +++ b/src/dataManager/helpers/handleExpiringTokens.js @@ -4,7 +4,7 @@ * Created Date: 14.10.2022 14:58:25 * Author: 3urobeat * - * Last Modified: 26.07.2023 16:53:28 + * Last Modified: 23.09.2023 13:06:53 * Modified By: 3urobeat * * Copyright (c) 2022 3urobeat @@ -19,70 +19,94 @@ const DataManager = require("../dataManager.js"); /** - * Internal: Checks tokens.db every 24 hours for refreshToken expiration in <=7 days, logs warning and sends botowner a Steam msg + * Internal: Checks tokens.db every 24 hours for refreshToken expiration in <=31 days and attempts to renew. + * If this fails and the token expires in <=7 days, it logs a warning and sends the botowner a Steam msg + * + * Note: This function should be redundant as SteamUser now automatically attempts to renew refreshTokens when `renewRefreshTokens` is enabled. */ DataManager.prototype._startExpiringTokensCheckInterval = function() { let _this = this; /* eslint-disable-next-line jsdoc/require-jsdoc */ - function scanDatabase() { + async function scanDatabase() { logger("debug", "DataManager detectExpiringTokens(): Scanning tokens.db for expiring tokens..."); let expiring = {}; let expired = {}; - _this.tokensDB.find({}, (err, docs) => { // Find all documents - docs.forEach((e, i) => { // Check every document - let tokenObj = _this.decodeJWT(e.token); + // Get all tokens & bots + let docs = await _this.tokensDB.findAsync({}); + if (docs.length == 0) return; - // Check acc if no error occurred (Code lookin funky cuz I can't use return here as the last iteration check would otherwise abort) - if (tokenObj) { - // Check if token expires in <= 7 days and add it to counter - if (tokenObj.exp * 1000 <= Date.now() + 604800000) { - let thisbot = _this.controller.getBots("*", true)[e.accountName]; + let bots = _this.controller.getBots("*", true); - // Only continue if a bot object and therefore corresponding credentials exists - Another nested check because we still can't use return here. - if (thisbot) { - expiring[e.accountName] = thisbot; // Push the bot object of the expiring account to our object + // Loop over all docs and attempt to renew their token. Notify the bot owners if Steam did not issue a new one + _this.controller.misc.syncLoop(docs.length, async (loop, i) => { + let e = docs[i]; + let tokenObj = _this.decodeJWT(e.token); + let thisbot = bots[e.accountName]; - // Check if token already expired and push to expired obj as well to show separate warning message - if (tokenObj.exp * 1000 <= Date.now()) expired[e.accountName] = thisbot; - } - } - } else { - logger("warn", `Failed to check when the login token for account '${e.accountName}' is going to expire!`); - } + // Check if decoding failed + if (!tokenObj) { + logger("warn", `Failed to check when the login token for account '${e.accountName}' is going to expire!`); + loop.next(); + return; + } - // Check if this was the last iteration and display message if at least one account was found - if (i + 1 == docs.length && Object.keys(expiring).length > 0) { - let msg; + // Skip iteration if token does not expire in <=31 days or if the corresponding bot object does not exist (aka no login credentials are currently available) + if (tokenObj.exp * 1000 > Date.now() + 2.6784e+6 || !thisbot) return loop.next(); - // Make it fancy and define different messages depending on how many accs were found - if (Object.keys(expiring).length > 1) msg = `The login tokens of ${Object.keys(expiring).length} accounts are expiring in less than 7 days`; - else msg = `The login token of account '${e.accountName}' is expiring in less than 7 days`; - // Mention how many accounts already expired - if (Object.keys(expired).length > 1) msg += ` and ${logger.colors.fgred}${Object.keys(expired).length} accounts have already expired!${logger.colors.reset}\nRestarting will force you to type in their Steam Guard Codes`; // Append - else if (Object.keys(expired).length == 1) msg = `The login token of account '${e.accountName}' ${logger.colors.fgred}has expired!${logger.colors.reset} Restarting will force you to type in the Steam Guard Code`; // Overwrite + // Attempt to renew the token automatically and check if it succeeded + let newToken = await thisbot.sessionHandler.attemptTokenRenew(); - // Log warning and message owners - logger("", `${logger.colors.fgred}Warning:`); - logger("", msg + "!", true); + tokenObj = _this.decodeJWT(newToken); - _this.cachefile.ownerid.forEach((e, i) => { - setTimeout(() => { - // eslint-disable-next-line no-control-regex - _this.controller.main.sendChatMessage(_this.controller.main, { userID: e }, msg.replace(/\x1B\[[0-9]+m/gm, "") + "!\nHead over to the terminal to refresh the token(s) now if you wish."); // Remove color codes from string - }, 1500 * i); - }); - // Check for active requests before asking for relog - _this._askForGetNewToken(expiring); - } + // Check if renew was successful in logindelay ms to avoid multiple fast renewals getting us blocked + setTimeout(() => { + if (tokenObj.exp * 1000 > Date.now() + 604800000) return loop.next(); // Skip to next iteration if either the renew was successful or if the token is not yet expiring in <=7 days + + // Always push to expiring and also to expired if token already expired + expiring[e.accountName] = thisbot; + + if (tokenObj.exp * 1000 <= Date.now()) expired[e.accountName] = thisbot; + + loop.next(); + }, _this.advancedconfig.logindelay); + + }, () => { // Loop exit function + + // Ignore if all tokens have been automatically renewed + if (Object.keys(expiring).length == 0) return; + + let msg; + + // Make it fancy and define different messages depending on how many accs were found + if (Object.keys(expiring).length > 1) msg = `The login tokens of ${Object.keys(expiring).length} accounts are expiring in less than 7 days`; + else msg = `The login token of account '${Object.values(expiring)[0].accountName}' is expiring in less than 7 days`; + + // Mention how many accounts already expired + if (Object.keys(expired).length > 1) msg += ` and ${logger.colors.fgred}${Object.keys(expired).length} accounts have already expired!${logger.colors.reset}\nRestarting will force you to type in their Steam Guard Codes`; // Append + else if (Object.keys(expired).length == 1) msg = `The login token of account '${Object.values(expiring)[0].accountName}' ${logger.colors.fgred}has expired!${logger.colors.reset} Restarting will force you to type in the Steam Guard Code`; // Overwrite + + // Log warning and message owners + logger("", `${logger.colors.fgred}Warning:`); + logger("", msg + "!", true); + + _this.cachefile.ownerid.forEach((e, i) => { + setTimeout(() => { + // eslint-disable-next-line no-control-regex + _this.controller.main.sendChatMessage(_this.controller.main, { userID: e }, msg.replace(/\x1B\[[0-9]+m/gm, "") + "!\nHead over to the terminal to refresh the token(s) now if you wish."); // Remove color codes from string + }, 1500 * i); }); + + // Check for active requests before asking for relog + _this._askForGetNewToken(expiring); }); } + // Clear existing interval if there is one if (this._handleExpiringTokensInterval) clearInterval(this._handleExpiringTokensInterval); @@ -99,7 +123,7 @@ DataManager.prototype._startExpiringTokensCheckInterval = function() { /** - * Internal: Asks user if he/she wants to refresh the tokens of all expiring accounts when no active request was found and relogs them + * Internal: Asks user if they want to refresh the tokens of all expiring accounts when no active request was found and relogs them * @param {object} expiring Object of botobject entries to ask user for */ DataManager.prototype._askForGetNewToken = function(expiring) { diff --git a/src/dataManager/helpers/misc.js b/src/dataManager/helpers/misc.js index 632b1dd6..a9312f1d 100644 --- a/src/dataManager/helpers/misc.js +++ b/src/dataManager/helpers/misc.js @@ -4,7 +4,7 @@ * Created Date: 24.03.2023 18:58:55 * Author: 3urobeat * - * Last Modified: 04.07.2023 19:58:05 + * Last Modified: 11.09.2023 19:49:25 * Modified By: 3urobeat * * Copyright (c) 2023 3urobeat @@ -24,7 +24,6 @@ const DataManager = require("../dataManager.js"); * @returns {Promise.} Called with the greatest timestamp (Number) found */ DataManager.prototype.getLastCommentRequest = function(steamID64 = null) { - return new Promise((resolve) => { let searchFor = {}; // Get all documents @@ -41,7 +40,6 @@ DataManager.prototype.getLastCommentRequest = function(steamID64 = null) { }); }); - }; diff --git a/src/dataManager/helpers/refreshCache.js b/src/dataManager/helpers/refreshCache.js index 8afc12f5..f558a7be 100644 --- a/src/dataManager/helpers/refreshCache.js +++ b/src/dataManager/helpers/refreshCache.js @@ -4,7 +4,7 @@ * Created Date: 29.03.2023 17:44:47 * Author: 3urobeat * - * Last Modified: 02.07.2023 19:07:41 + * Last Modified: 05.09.2023 19:05:14 * Modified By: 3urobeat * * Copyright (c) 2023 3urobeat @@ -14,11 +14,12 @@ * You should have received a copy of the GNU General Public License along with this program. If not, see . */ -const fs = require("fs"); + const SteamID = require("steamid"); const DataManager = require("../dataManager"); + /** * Refreshes Backups in cache.json with new data */ @@ -51,7 +52,5 @@ DataManager.prototype.refreshCache = function () { this.cachefile["datajson"] = this.datafile; // Write changes to file - fs.writeFile(srcdir + "/data/cache.json", JSON.stringify(this.cachefile, null, 4), (err) => { - if (err) logger("error", "error writing file backups to cache.json: " + err); - }); + this.writeCachefileToDisk(); }; diff --git a/src/libraryPatches/CSteamDiscussion.js b/src/libraryPatches/CSteamDiscussion.js new file mode 100644 index 00000000..deceb9fb --- /dev/null +++ b/src/libraryPatches/CSteamDiscussion.js @@ -0,0 +1,205 @@ +const Cheerio = require('cheerio'); +const SteamID = require('steamid'); + +const SteamCommunity = require('steamcommunity'); +const Helpers = require('../../node_modules/steamcommunity/components/helpers.js'); + +const EDiscussionType = require("./EDiscussionType.js"); + + +/** + * Scrape a discussion's DOM to get all available information + * @param {string} url - SteamCommunity url pointing to the discussion to fetch + * @param {function} callback - First argument is null/Error, second is object containing all available information + */ +SteamCommunity.prototype.getSteamDiscussion = function(url, callback) { + // Construct object holding all the data we can scrape + let discussion = { + id: null, + type: null, + appID: null, + forumID: null, + gidforum: null, // This is some id used as parameter 2 in post requests + topicOwner: null, // This is some id used as parameter 1 in post requests + author: null, + postedDate: null, + title: null, + content: null, + commentsAmount: null, // I originally wanted to fetch all comments by default but that would have been a lot of potentially unused data + answerCommentIndex: null + }; + + // Get DOM of discussion + this.httpRequestGet(url, (err, res, body) => { + if (err) { + callback(err); + return; + } + + try { + + /* --------------------- Preprocess output --------------------- */ + + // Load output into cheerio to make parsing easier + let $ = Cheerio.load(body); + + // Get breadcrumbs once. Depending on the type of discussion, it either uses "forum" or "group" breadcrumbs + let breadcrumbs = $(".forum_breadcrumbs").children(); + + if (breadcrumbs.length == 0) breadcrumbs = $(".group_breadcrumbs").children(); + + // Steam redirects us to the forum page if the discussion does not exist which we can detect by missing breadcrumbs + if (!breadcrumbs[0]) { + callback(new Error('Discussion not found')); + return; + } + + + /* --------------------- Find and map values --------------------- */ + + // Determine type from URL as some checks will deviate, depending on the type + if (url.includes("steamcommunity.com/discussions/forum")) discussion.type = EDiscussionType.Forum; + if (/steamcommunity.com\/app\/.+\/discussions/g.test(url)) discussion.type = EDiscussionType.App; + if (/steamcommunity.com\/groups\/.+\/discussions/g.test(url)) discussion.type = EDiscussionType.Group; + + + // Get appID from breadcrumbs if this discussion is associated to one + if (discussion.type == EDiscussionType.App) { + let appIdHref = breadcrumbs[0].attribs["href"].split("/"); + + discussion.appID = appIdHref[appIdHref.length - 1]; + } + + + // Get forumID from breadcrumbs + let forumIdHref; + + if (discussion.type == EDiscussionType.Group) { // Groups have an extra breadcrumb so we need to shift by 2 + forumIdHref = breadcrumbs[4].attribs["href"].split("/"); + } else { + forumIdHref = breadcrumbs[2].attribs["href"].split("/"); + } + + discussion.forumID = forumIdHref[forumIdHref.length - 2]; + + + // Get id, gidforum and topicOwner. The first is used in the URL itself, the other two only in post requests + let gids = $(".forum_paging > .forum_paging_controls").attr("id").split("_"); + + discussion.id = gids[4]; + discussion.gidforum = gids[3]; + discussion.topicOwner = gids[2]; + + + // Find postedDate and convert to timestamp + let posted = $(".topicstats > .topicstats_label:contains(\"Date Posted:\")").next().text(); + + discussion.postedDate = Helpers.decodeSteamTime(posted.trim()); + + + // Find commentsAmount + discussion.commentsAmount = Number($(".topicstats > .topicstats_label:contains(\"Posts:\")").next().text()); + + + // Get discussion title & content + discussion.title = $(".forum_op > .topic").text().trim(); + discussion.content = $(".forum_op > .content").text().trim(); + + + // Find comment marked as answer + let hasAnswer = $(".commentthread_answer_bar") + + if (hasAnswer.length != 0) { + let answerPermLink = hasAnswer.next().children(".forum_comment_permlink").text().trim(); + + // Convert comment id to number, remove hashtag and subtract by 1 to make it an index + discussion.answerCommentIndex = Number(answerPermLink.replace("#", "")) - 1; + } + + + // Find author and convert to SteamID object + let authorLink = $(".authorline > .forum_op_author").attr("href"); + + Helpers.resolveVanityURL(authorLink, (err, data) => { // This request takes <1 sec + if (err) { + callback(err); + return; + } + + discussion.author = new SteamID(data.steamID); + + // Make callback when ID was resolved as otherwise owner will always be null + callback(null, new CSteamDiscussion(this, discussion)); + }); + + } catch (err) { + callback(err, null); + } + }, "steamcommunity"); +} + + +/** + * Constructor - Creates a new Discussion object + * @class + * @param {SteamCommunity} community + * @param {{ id: string, appID: string, forumID: string, author: SteamID, postedDate: Object, title: string, content: string, commentsAmount: number }} data + */ +function CSteamDiscussion(community, data) { + /** + * @type {SteamCommunity} + */ + this._community = community; + + // Clone all the data we received + Object.assign(this, data); +} + + +/** + * Scrapes a range of comments from this discussion + * @param {number} startIndex - Index (0 based) of the first comment to fetch + * @param {number} endIndex - Index (0 based) of the last comment to fetch + * @param {function} callback - First argument is null/Error, second is array containing the requested comments + */ +CSteamDiscussion.prototype.getComments = function(startIndex, endIndex, callback) { + this._community.getDiscussionComments(`https://steamcommunity.com/app/${this.appID}/discussions/${this.forumID}/${this.id}`, startIndex, endIndex, callback); +}; + + +/** + * Posts a comment to this discussion's comment section + * @param {String} message - Content of the comment to post + * @param {function} callback - Takes only an Error object/null as the first argument + */ +CSteamDiscussion.prototype.postComment = function(message, callback) { + this._community.postDiscussionComment(this.topicOwner, this.gidforum, this.id, message, callback); +}; + + +/** + * Delete a comment from this discussion's comment section + * @param {String} gidcomment - ID of the comment to delete + * @param {function} callback - Takes only an Error object/null as the first argument + */ +CSteamDiscussion.prototype.deleteComment = function(gidcomment, callback) { + this._community.deleteDiscussionComment(this.topicOwner, this.gidforum, this.id, gidcomment, callback); +}; + + +/** + * Subscribes to this discussion's comment section + * @param {function} callback - Takes only an Error object/null as the first argument + */ +CSteamDiscussion.prototype.subscribe = function(callback) { + this._community.subscribeDiscussionComments(this.topicOwner, this.gidforum, this.id, callback); +}; + + +/** + * Unsubscribes from this discussion's comment section + * @param {function} callback - Takes only an Error object/null as the first argument + */ +CSteamDiscussion.prototype.unsubscribe = function(callback) { + this._community.unsubscribeDiscussionComments(this.topicOwner, this.gidforum, this.id, callback); +}; diff --git a/src/libraryPatches/CSteamSharedFile.js b/src/libraryPatches/CSteamSharedFile.js index a1a3eca2..b490a884 100644 --- a/src/libraryPatches/CSteamSharedFile.js +++ b/src/libraryPatches/CSteamSharedFile.js @@ -32,6 +32,11 @@ SteamCommunity.prototype.getSteamSharedFile = function(sharedFileId, callback) { // Get DOM of sharedfile this.httpRequestGet(`https://steamcommunity.com/sharedfiles/filedetails/?id=${sharedFileId}&l=english`, (err, res, body) => { // Request page in english so that the Posted scraping below works + if (err) { + callback(err); + return; + } + try { /* --------------------- Preprocess output --------------------- */ diff --git a/src/libraryPatches/EDiscussionType.js b/src/libraryPatches/EDiscussionType.js new file mode 100644 index 00000000..b6c28bb7 --- /dev/null +++ b/src/libraryPatches/EDiscussionType.js @@ -0,0 +1,13 @@ +/** + * @enum EDiscussionType + */ +module.exports = { + "Forum": 0, + "App": 1, + "Group": 2, + + // Value-to-name mapping for convenience + "0": "Forum", + "1": "App", + "2": "Group" +}; \ No newline at end of file diff --git a/src/libraryPatches/README.md b/src/libraryPatches/README.md index 97f42257..d8f820eb 100644 --- a/src/libraryPatches/README.md +++ b/src/libraryPatches/README.md @@ -10,15 +10,17 @@ The original library which is installed: https://github.com/DoctorMcKay/node-ste   These are the patches being applied: -- Re-enable primaryGroup profile setting: [#287](https://github.com/DoctorMcKay/node-steamcommunity/pull/287) & [#307](https://github.com/DoctorMcKay/node-steamcommunity/pull/307) - Add sharedfiles voteUp & voteDown support - Fix resolving vanity for private profiles returning error: [#315](https://github.com/DoctorMcKay/node-steamcommunity/pull/315) - Fix sharedfile data scraping failing as a non-english page was returned by Steam: [#316](https://github.com/DoctorMcKay/node-steamcommunity/pull/316) - Fix scraping sharedfile type failing when incomplete breadcrumb was returned: [#316](https://github.com/DoctorMcKay/node-steamcommunity/pull/316) +- Add discussions support: [#319](https://github.com/DoctorMcKay/node-steamcommunity/pull/319) These patches have been applied in the past: - Add full sharedfiles support: [#306](https://github.com/DoctorMcKay/node-steamcommunity/pull/306) - Fix resolving vanity returning error: [#314](https://github.com/DoctorMcKay/node-steamcommunity/pull/314) +- Re-enable primaryGroup profile setting: [#287](https://github.com/DoctorMcKay/node-steamcommunity/pull/287) & [#307](https://github.com/DoctorMcKay/node-steamcommunity/pull/307) +- Add user/workshop & curator follow & unfollow support: [#320](https://github.com/DoctorMcKay/node-steamcommunity/pull/320)   diff --git a/src/libraryPatches/discussions.js b/src/libraryPatches/discussions.js new file mode 100644 index 00000000..97b745fe --- /dev/null +++ b/src/libraryPatches/discussions.js @@ -0,0 +1,321 @@ +const Cheerio = require('cheerio'); + +const SteamCommunity = require('steamcommunity'); +const Helpers = require('../../node_modules/steamcommunity/components/helpers.js'); + + +/** + * Scrapes a range of comments from a Steam discussion + * @param {url} url - SteamCommunity url pointing to the discussion to fetch + * @param {number} startIndex - Index (0 based) of the first comment to fetch + * @param {number} endIndex - Index (0 based) of the last comment to fetch + * @param {function} callback - First argument is null/Error, second is array containing the requested comments + */ +SteamCommunity.prototype.getDiscussionComments = function(url, startIndex, endIndex, callback) { + this.httpRequestGet(url + "?l=en", async (err, res, body) => { + + if (err) { + callback("Failed to load discussion: " + err, null); + return; + } + + + // Load output into cheerio to make parsing easier + let $ = Cheerio.load(body); + + let paging = $(".forum_paging > .forum_paging_summary").children(); + + /** + * Stores every loaded page inside a Cheerio instance + * @type {{[key: number]: cheerio.Root}} + */ + let pages = { + 0: $ + }; + + + // Determine amount of comments per page and total. Update endIndex if null to get all comments + let commentsPerPage = Number(paging[4].children[0].data); + let totalComments = Number(paging[5].children[0].data) + + if (endIndex == null || endIndex > totalComments - 1) { // Make sure to check against null as the index 0 would cast to false + endIndex = totalComments - 1; + } + + + // Save all pages that need to be fetched in order to get the requested comments + let firstPage = Math.trunc(startIndex / commentsPerPage); // Index of the first page that needs to be fetched + let lastPage = Math.trunc(endIndex / commentsPerPage); + let promises = []; + + for (let i = firstPage; i <= lastPage; i++) { + if (i == 0) continue; // First page is already in pages object + + promises.push(new Promise((resolve) => { + setTimeout(() => { // Delay fetching a bit to reduce the risk of Steam blocking us + + this.httpRequestGet(url + "?l=en&ctp=" + (i + 1), (err, res, body) => { + try { + pages[i] = Cheerio.load(body); + resolve(); + } catch (err) { + return callback("Failed to load comments page: " + err, null); + } + }, "steamcommunity"); + + }, 250 * i); + })); + } + + await Promise.all(promises); // Wait for all pages to be fetched + + + // Fill comments with content of all comments + let comments = []; + + for (let i = startIndex; i <= endIndex; i++) { + let $ = pages[Math.trunc(i / commentsPerPage)]; + + let thisComment = $(`.forum_comment_permlink:contains("#${i + 1}")`).parent(); + let thisCommentID = thisComment.attr("id").replace("comment_", ""); + + // Note: '>' inside the cheerio selectors didn't work here + let authorContainer = thisComment.children(".commentthread_comment_content").children(".commentthread_comment_author").children(".commentthread_author_link"); + let commentContainer = thisComment.children(".commentthread_comment_content").children(`#comment_content_${thisCommentID}`); + + + // Prepare comment text by finding all existing blockquotes, formatting them and adding them infront each other. Afterwards handle the text itself + let commentText = ""; + let blockQuoteSelector = ".bb_blockquote"; + let children = commentContainer.children(blockQuoteSelector); + + for (let i = 0; i < 10; i++) { // I'm not sure how I could dynamically check the amount of nested blockquotes. 10 is prob already too much to stay readable + if (children.length > 0) { + let thisQuoteText = ""; + + thisQuoteText += children.children(".bb_quoteauthor").text() + "\n"; // Get quote header and add a proper newline + + // Replace
's with newlines to get a proper output + let quoteWithNewlines = children.first().find("br").replaceWith("\n"); + + thisQuoteText += quoteWithNewlines.end().contents().filter(function() { return this.type === 'text' }).text().trim(); // Get blockquote content without child content - https://stackoverflow.com/a/23956052 + if (i > 0) thisQuoteText += "\n-------\n"; // Add spacer + + commentText = thisQuoteText + commentText; // Concat quoteText to the start of commentText as the most nested quote is the first one inside the comment chain itself + + // Go one level deeper + children = children.children(blockQuoteSelector); + + } else { + + commentText += "\n\n-------\n\n"; // Add spacer + break; + } + } + + let quoteWithNewlines = commentContainer.first().find("br").replaceWith("\n"); // Replace
's with newlines to get a proper output + + commentText += quoteWithNewlines.end().contents().filter(function() { return this.type === 'text' }).text().trim(); // Add comment content without child content - https://stackoverflow.com/a/23956052 + + + comments.push({ + index: i, + commentId: thisCommentID, + commentLink: `${url}#c${thisCommentID}`, + authorLink: authorContainer.attr("href"), // I did not call 'resolveVanityURL()' here and convert to SteamID to reduce the amount of potentially unused Steam pings + postedDate: Helpers.decodeSteamTime(authorContainer.children(".commentthread_comment_timestamp").text().trim()), + content: commentText.trim() + }); + } + + + // Callback our result + callback(null, comments); + + }, "steamcommunity"); +}; + +/** + * Posts a comment to a discussion + * @param {String} topicOwner - ID of the topic owner + * @param {String} gidforum - GID of the discussion's forum + * @param {String} discussionId - ID of the discussion + * @param {String} message - Content of the comment to post + * @param {function} callback - Takes only an Error object/null as the first argument + */ +SteamCommunity.prototype.postDiscussionComment = function(topicOwner, gidforum, discussionId, message, callback) { + this.httpRequestPost({ + "uri": `https://steamcommunity.com/comment/ForumTopic/post/${topicOwner}/${gidforum}/`, + "form": { + "comment": message, + "count": 15, + "sessionid": this.getSessionID(), + "extended_data": '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1}}', + "feature2": discussionId, + "json": 1 + }, + "json": true + }, function(err, response, body) { + if (!callback) { + return; + } + + if (err) { + callback(err); + return; + } + + if (body.success) { + callback(null); + } else { + callback(new Error(body.error)); + } + }, "steamcommunity"); +}; + +/** + * Deletes a comment from a discussion + * @param {String} topicOwner - ID of the topic owner + * @param {String} gidforum - GID of the discussion's forum + * @param {String} discussionId - ID of the discussion + * @param {String} gidcomment - ID of the comment to delete + * @param {function} callback - Takes only an Error object/null as the first argument + */ +SteamCommunity.prototype.deleteDiscussionComment = function(topicOwner, gidforum, discussionId, gidcomment, callback) { + this.httpRequestPost({ + "uri": `https://steamcommunity.com/comment/ForumTopic/delete/${topicOwner}/${gidforum}/`, + "form": { + "gidcomment": gidcomment, + "count": 15, + "sessionid": this.getSessionID(), + "extended_data": '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1}}', + "feature2": discussionId, + "json": 1 + }, + "json": true + }, function(err, response, body) { // Steam does not seem to return any errors here even when trying to delete a non-existing comment but let's check the response anyway + if (!callback) { + return; + } + + if (err) { + callback(err); + return; + } + + if (body.success) { + callback(null); + } else { + callback(new Error(body.error)); + } + }, "steamcommunity"); +}; + +/** + * Subscribes to a discussion's comment section + * @param {String} topicOwner - ID of the topic owner + * @param {String} gidforum - GID of the discussion's forum + * @param {String} discussionId - ID of the discussion + * @param {function} callback - Takes only an Error object/null as the first argument + */ +SteamCommunity.prototype.subscribeDiscussionComments = function(topicOwner, gidforum, discussionId, callback) { + this.httpRequestPost({ + "uri": `https://steamcommunity.com/comment/ForumTopic/subscribe/${topicOwner}/${gidforum}/`, + "form": { + "count": 15, + "sessionid": this.getSessionID(), + "extended_data": '{"topic_permissions":{"can_view":1,"can_post":1,"can_reply":1}}', + "feature2": discussionId, + "json": 1 + }, + "json": true + }, function(err, response, body) { + if (!callback) { + return; + } + + if (err) { + callback(err); + return; + } + + if (body.success && body.success != SteamCommunity.EResult.OK) { + callback(Helpers.eresultError(body.success)); + return; + } + + callback(null); + }, "steamcommunity"); +}; + +/** + * Unsubscribes from a discussion's comment section + * @param {String} topicOwner - ID of the topic owner + * @param {String} gidforum - GID of the discussion's forum + * @param {String} discussionId - ID of the discussion + * @param {function} callback - Takes only an Error object/null as the first argument + */ +SteamCommunity.prototype.unsubscribeDiscussionComments = function(topicOwner, gidforum, discussionId, callback) { + this.httpRequestPost({ + "uri": `https://steamcommunity.com/comment/ForumTopic/unsubscribe/${topicOwner}/${gidforum}/`, + "form": { + "count": 15, + "sessionid": this.getSessionID(), + "extended_data": '{}', // Unsubscribing does not require any data here + "feature2": discussionId, + "json": 1 + }, + "json": true + }, function(err, response, body) { + if (!callback) { + return; + } + + if (err) { + callback(err); + return; + } + + if (body.success && body.success != SteamCommunity.EResult.OK) { + callback(Helpers.eresultError(body.success)); + return; + } + + callback(null); + }, "steamcommunity"); +}; + +/** + * Sets an amount of comments per page + * @param {String} value - 15, 30 or 50 + * @param {function} callback - Takes only an Error object/null as the first argument + */ +SteamCommunity.prototype.setDiscussionCommentsPerPage = function(value, callback) { + if (!["15", "30", "50"].includes(value)) value = "50"; // Check for invalid setting + + this.httpRequestPost({ + "uri": `https://steamcommunity.com/forum/0/0/setpreference`, + "form": { + "preference": "topicrepliesperpage", + "value": value, + "sessionid": this.getSessionID(), + }, + "json": true + }, function(err, response, body) { // Steam does not seem to return any errors for this request + if (!callback) { + return; + } + + if (err) { + callback(err); + return; + } + + if (body.success && body.success != SteamCommunity.EResult.OK) { + callback(Helpers.eresultError(body.success)); + return; + } + + callback(null); + }, "steamcommunity"); +}; \ No newline at end of file diff --git a/src/libraryPatches/profile.js b/src/libraryPatches/profile.js deleted file mode 100644 index ac931a9d..00000000 --- a/src/libraryPatches/profile.js +++ /dev/null @@ -1,143 +0,0 @@ -const Cheerio = require('cheerio'); -const SteamID = require('steamid'); - -const SteamCommunity = require('steamcommunity'); - -SteamCommunity.PrivacyState = { - "Private": 1, - "FriendsOnly": 2, - "Public": 3 -}; - -/** - * Don't look at me, I'm just a placeholder - * @param {*} settings - * @param {*} callback - */ -SteamCommunity.prototype.editProfile = function(settings, callback) { - var self = this; - this._myProfile('edit/info', null, function(err, response, body) { - if (err || response.statusCode != 200) { - if (callback) { - callback(err || new Error('HTTP error ' + response.statusCode)); - } - - return; - } - - var $ = Cheerio.load(body); - var existingSettings = $('#profile_edit_config').data('profile-edit'); - if (!existingSettings || !existingSettings.strPersonaName) { - if (callback) { - callback(new Error('Malformed response')); - } - - return; - } - - var values = { - sessionID: self.getSessionID(), - type: 'profileSave', - weblink_1_title: '', - weblink_1_url: '', - weblink_2_title: '', - weblink_2_url: '', - weblink_3_title: '', - weblink_3_url: '', - personaName: existingSettings.strPersonaName, - real_name: existingSettings.strRealName, - summary: existingSettings.strSummary, - country: existingSettings.LocationData.locCountryCode, - state: existingSettings.LocationData.locStateCode, - city: existingSettings.LocationData.locCityCode, - customURL: existingSettings.strCustomURL, - json: 1 - }; - - for (var i in settings) { - if(!settings.hasOwnProperty(i)) { - continue; - } - - switch(i) { - case 'name': - values.personaName = settings[i]; - break; - - case 'realName': - values.real_name = settings[i]; - break; - - case 'summary': - values.summary = settings[i]; - break; - - case 'country': - values.country = settings[i]; - break; - - case 'state': - values.state = settings[i]; - break; - - case 'city': - values.city = settings[i]; - break; - - case 'customURL': - values.customURL = settings[i]; - break; - - case 'primaryGroup': - if(typeof settings[i] === 'object' && settings[i].getSteamID64) { - values.primary_group_steamid = settings[i].getSteamID64(); - } else { - values.primary_group_steamid = new SteamID(settings[i]).getSteamID64(); - } - - break; - - // These don't work right now - /* - case 'background': - // The assetid of our desired profile background - values.profile_background = settings[i]; - break; - - case 'featuredBadge': - // Currently, game badges aren't supported - values.favorite_badge_badgeid = settings[i]; - break; - */ - // TODO: profile showcases - } - } - - self._myProfile('edit', values, function(err, response, body) { - if (settings.customURL) { - delete self._profileURL; - } - - if (!callback) { - return; - } - - if (err || response.statusCode != 200) { - callback(err || new Error('HTTP error ' + response.statusCode)); - return; - } - - try { - var json = JSON.parse(body); - if (!json.success || json.success != 1) { - callback(new Error(json.errmsg || 'Request was not successful')); - return; - } - - callback(null); - } catch (ex) { - callback(ex); - } - }); - }); -}; \ No newline at end of file diff --git a/src/libraryPatches/sharedfiles.js b/src/libraryPatches/sharedfiles.js index b4f63107..6dd5cfcd 100644 --- a/src/libraryPatches/sharedfiles.js +++ b/src/libraryPatches/sharedfiles.js @@ -1,4 +1,7 @@ const SteamCommunity = require('steamcommunity'); +const EResult = SteamCommunity.EResult; +const SteamID = require('steamid'); +const Helpers = require("../../node_modules/steamcommunity/components/helpers.js"); /** * Downvotes a sharedfile @@ -10,14 +13,26 @@ SteamCommunity.prototype.voteDownSharedFile = function(sid, callback) { "uri": "https://steamcommunity.com/sharedfiles/votedown", "form": { "id": sid, + "json": "1", "sessionid": this.getSessionID() - } + }, + "json": true }, function(err, response, body) { if (!callback) { return; } - callback(null || err); + if (err) { + callback(err); + return; + } + + if (body.success && body.success != SteamCommunity.EResult.OK) { + callback(Helpers.eresultError(body.success)); + return; + } + + callback(null); }, "steamcommunity"); }; @@ -31,13 +46,64 @@ SteamCommunity.prototype.voteUpSharedFile = function(sid, callback) { "uri": "https://steamcommunity.com/sharedfiles/voteup", "form": { "id": sid, + "json": "1", "sessionid": this.getSessionID() + }, + "json": true + }, function(err, response, body) { + if (!callback) { + return; + } + + if (err) { + callback(err); + return; + } + + if (body.success && body.success != SteamCommunity.EResult.OK) { + callback(Helpers.eresultError(body.success)); + return; } + + callback(null); + }, "steamcommunity"); +}; + +/** + * Posts a comment to a sharedfile + * @param {SteamID | String} userID - ID of the user associated to this sharedfile + * @param {String} sharedFileId - ID of the sharedfile + * @param {String} message - Content of the comment to post + * @param {function} callback - Takes only an Error object/null as the first argument + */ +SteamCommunity.prototype.postSharedFileComment = function(userID, sharedFileId, message, callback) { + if (typeof userID === "string") { + userID = new SteamID(userID); + } + + this.httpRequestPost({ + "uri": `https://steamcommunity.com/comment/PublishedFile_Public/post/${userID.toString()}/${sharedFileId}/`, + "form": { + "comment": message, + "count": 10, + "json": 1, + "sessionid": this.getSessionID() + }, + "json": true }, function(err, response, body) { if (!callback) { return; } - callback(null || err); + if (err) { + callback(err); + return; + } + + if (body.success) { + callback(null); + } else { + callback(new Error(body.error)); + } }, "steamcommunity"); }; \ No newline at end of file diff --git a/src/pluginSystem/handlePluginData.js b/src/pluginSystem/handlePluginData.js index d2a80742..239f1bb0 100644 --- a/src/pluginSystem/handlePluginData.js +++ b/src/pluginSystem/handlePluginData.js @@ -4,7 +4,7 @@ * Created Date: 04.06.2023 17:52:51 * Author: 3urobeat * - * Last Modified: 07.07.2023 15:25:16 + * Last Modified: 15.09.2023 16:28:07 * Modified By: 3urobeat * * Copyright (c) 2023 3urobeat @@ -14,10 +14,12 @@ * You should have received a copy of the GNU General Public License along with this program. If not, see . */ + const fs = require("fs"); const PluginSystem = require("./pluginSystem.js"); + /** * Gets the path holding all data of a plugin. If no folder exists yet, one will be created * @param {string} pluginName Name of your plugin @@ -33,6 +35,7 @@ PluginSystem.prototype.getPluginDataPath = function (pluginName) { return path; }; + /** * Loads a file from your plugin data folder. The data will remain unprocessed. Use `loadPluginConfig()` instead if you want to load your plugin config. * @param {string} pluginName Name of your plugin @@ -58,6 +61,7 @@ PluginSystem.prototype.loadPluginData = function (pluginName, filename) { }); }; + /** * Writes a file to your plugin data folder. The data will remain unprocessed. Use `writePluginConfig()` instead if you want to write your plugin config. * @param {string} pluginName Name of your plugin @@ -84,6 +88,7 @@ PluginSystem.prototype.writePluginData = function (pluginName, filename, data) { }); }; + /** * Deletes a file in your plugin data folder if it exists. * @param {string} pluginName Name of your plugin @@ -113,6 +118,7 @@ PluginSystem.prototype.deletePluginData = function (pluginName, filename) { }); }; + /** * Loads your plugin config from the filesystem or creates a new one based on the default config provided by your plugin. The JSON data will be processed to an object. * @param {string} pluginName Name of your plugin diff --git a/src/pluginSystem/pluginSystem.js b/src/pluginSystem/pluginSystem.js index f613ef3f..c4c1b089 100644 --- a/src/pluginSystem/pluginSystem.js +++ b/src/pluginSystem/pluginSystem.js @@ -4,7 +4,7 @@ * Created Date: 19.03.2023 13:34:27 * Author: 3urobeat * - * Last Modified: 07.07.2023 12:43:16 + * Last Modified: 15.09.2023 16:32:05 * Modified By: 3urobeat * * Copyright (c) 2023 3urobeat @@ -14,10 +14,12 @@ * You should have received a copy of the GNU General Public License along with this program. If not, see . */ + const Controller = require("../controller/controller.js"); // eslint-disable-line const CommandHandler = require("../commands/commandHandler.js"); // eslint-disable-line const Bot = require("../../src/bot/bot.js"); // eslint-disable-line + /** * @typedef Plugin Documentation of the Plugin structure for IntelliSense support * @type {object} @@ -28,6 +30,7 @@ const Bot = require("../../src/bot/bot.js"); // eslint-disable-line * @property {function(Bot, function(string): void): void} steamGuardInput Controller steamGuardInput event */ + /** * Constructor - The plugin system loads all plugins and provides functions for plugins to hook into * @class @@ -59,6 +62,7 @@ const PluginSystem = function (controller) { // The plugin system loads all plugins and provides functions for plugins to hook into module.exports = PluginSystem; + /** * Reloads all plugins and calls ready event after ~2.5 seconds. */ @@ -96,6 +100,7 @@ PluginSystem.prototype.reloadPlugins = function () { }, 3000); }; + /* -------- Register functions to let the IntelliSense know what's going on in helper files -------- */ /** @@ -160,7 +165,7 @@ PluginSystem.prototype.loadPluginConfig = function (pluginName) {}; // eslint-di PluginSystem.prototype.writePluginConfig = function (pluginName, pluginConfig) {}; // eslint-disable-line /** * Integrates changes made to the config to the users config - * @param {string} pluginName + * @param {string} pluginName Name of your plugin * @returns {Record} the config */ PluginSystem.prototype.aggregatePluginConfig = function (pluginName) {}; // eslint-disable-line diff --git a/src/sessions/sessionHandler.js b/src/sessions/sessionHandler.js index ce0bdb75..5c8b0578 100644 --- a/src/sessions/sessionHandler.js +++ b/src/sessions/sessionHandler.js @@ -4,7 +4,7 @@ * Created Date: 09.10.2022 12:47:27 * Author: 3urobeat * - * Last Modified: 08.07.2023 00:36:45 + * Last Modified: 05.10.2023 19:34:45 * Modified By: 3urobeat * * Copyright (c) 2022 3urobeat @@ -108,6 +108,7 @@ SessionHandler.prototype._resolvePromise = function(token) { this.bot.loginData.waitingFor2FA = false; // Allow handleLoginTimeout to work again this.getTokenPromise(token); + }; @@ -134,6 +135,53 @@ SessionHandler.prototype._attemptCredentialsLogin = function() { }; +/** + * Attempts to renew the refreshToken used for the current session. Whether a new token will actually be issued is at the discretion of Steam. + * @returns {Promise.} Returns a promise which resolves with `true` if Steam issued a new token, `false` otherwise. Rejects if no token is stored in the database. + */ +SessionHandler.prototype.attemptTokenRenew = function() { + return new Promise((resolve, reject) => { + + // Init new session + this.session = new SteamSession.LoginSession(SteamSession.EAuthTokenPlatformType.SteamClient, { httpProxy: this.bot.loginData.proxy }); + + // Get and set current refresh token + this._getTokenFromStorage(async (storedToken) => { + if (!storedToken) return reject(new Error("There is no token stored for this account")); + + this.session.refreshToken = storedToken; + + // Attempt to renew token + this.session.renewRefreshToken() + .then((res) => { + if (!res) { + logger("warn", `[${this.bot.logPrefix}] Failed to renew refresh token: Steam did not issue a new one`); + resolve(res); + return; + } + + let newToken = this.session.refreshToken; + let jwtObj = this.controller.data.decodeJWT(newToken); // Decode the token we've found + let validUntilStr = `${(new Date(jwtObj.exp * 1000)).toISOString().replace(/T/, " ").replace(/\..+/, "")} (GMT time)`; + + logger("info", `[${this.bot.logPrefix}] Successfully renewed refresh token! It is now valid until '${validUntilStr}'!`); + + // Save token to the database + this._saveTokenToStorage(newToken); + + resolve(res); + }) + .catch((err) => { + logger("error", `[${this.bot.logPrefix}] Failed to renew refresh token! Error: ${err}`); + + reject(err); + }); + }); + + }); +}; + + /* ------------ Reference helper functions to let the IntelliSense know about them ------------ */ /** diff --git a/src/starter.js b/src/starter.js index 441b1beb..a1ca540f 100644 --- a/src/starter.js +++ b/src/starter.js @@ -4,7 +4,7 @@ * Created Date: 10.07.2021 10:26:00 * Author: 3urobeat * - * Last Modified: 05.07.2023 10:48:48 + * Last Modified: 29.09.2023 16:42:05 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -14,11 +14,13 @@ * You should have received a copy of the GNU General Public License along with this program. If not, see . */ + // This file is called by start.js (which can't be updated). // This file can be updated but is not recommended as changes can't be applied cleanly without a manual restart. // It handles spawning and killing a child process which the bot runs in. -// Please ignore the code quality, this file is designed to be pretty fail safe. +// This file might look a bit weird - it is designed to hopefully be mostly failsafe. + /* ---------------- First, import the core lib fs and define a few vars which will be used later ---------------- */ const fs = require("fs"); @@ -40,7 +42,8 @@ global.srcdir = __dirname; process.argv[3] = 0; // Exec Arguments passed to the child process. Add --inspect here to enable the node.js debugger -const execArgs = [ "--max-old-space-size=2048", "--optimize-for-size" ]; +const execArgs = [ "--max-old-space-size=2048", "--optimize-for-size", /* "--inspect" */ ]; + /* -------- Now, provide functions for attaching/detaching event listeners to the parent and child process -------- */ @@ -51,6 +54,7 @@ const execArgs = [ "--max-old-space-size=2048", "--optimize-for-size" ]; function attachParentListeners(callback) { let logafterrestart = []; + /* ------------ Make a fake logger to use when the lib isn't loaded yet: ------------ */ logger = (type, str) => { // Make a "fake" logger function in order to be able to log the error message when the user forgot to run 'npm install' @@ -64,23 +68,26 @@ function attachParentListeners(callback) { logger.animation = () => {}; // Just to be sure that no error occurs when trying to call this function without the real logger being present logger.detachEventListeners = () => {}; + /* ------------ Add unhandled rejection catches: ------------ */ - handleUnhandledRejection = (reason) => { - // Should keep the bot at least from crashing + handleUnhandledRejection = (reason) => { // Should keep the bot at least from crashing logger("error", `Unhandled Rejection Error! Reason: ${reason.stack}`, true); }; - handleUncaughtException = (reason) => { - // Try to fix error automatically by reinstalling all modules + handleUncaughtException = async (reason) => { // Try to fix error automatically by reinstalling all modules if (String(reason).includes("Error: Cannot find module")) { logger("", "", true); if (global.extdata) logger("info", "Cannot find module error detected. Trying to fix error by reinstalling modules...\n"); // Check if extdata has been imported as workaround for hiding this message for new users to avoid confusion (because extdata.firststart can't be checked yet) - require("./controller/helpers/npminteraction.js").reinstallAll(logger, (err, stdout) => { // eslint-disable-line + const npminteraction = await module.exports.checkAndGetFile("./src/controller/helpers/npminteraction.js", logger, false, false); + + npminteraction.reinstallAll(logger, (err, stdout) => { // eslint-disable-line if (err) { logger("error", "I was unable to reinstall all modules. Please try running 'npm install --production' manually. Error: " + err); process.exit(1); + } else { + // Logger("info", `NPM Log:\n${stdout}`, true) //entire log (not using it rn to avoid possible confusion with vulnerabilities message) logger("info", "Successfully reinstalled all modules."); @@ -92,18 +99,16 @@ function attachParentListeners(callback) { try { process.kill(childpid, "SIGKILL"); - } catch (err) {} //eslint-disable-line + } catch (err) {} // eslint-disable-line setTimeout(() => { - logger("info", "Restarting...", false, true); // Note (Known issue!): Restarting here causes the application to start the bot in this process rather than creating a child_process. I have no idea why but it doesn't seem to cause issues (I HOPE) and is fixed when the user restarts the bot. - require("../start.js").restart({ - logafterrestart: logafterrestart, - }); // Call restart function with argsobject + logger("info", "Restarting...", false, true); + require("../start.js").restart({ logafterrestart: logafterrestart }); // Call restart function with argsobject }, 2000); } }); - } else { - // Logging this message but still trying to fix it would probably confuse the user + + } else { // Only log error if we are not trying to fix it in an effort to not confuse the user logger("error", `Uncaught Exception Error! Reason: ${reason.stack}`, true); logger("", "", true); @@ -117,7 +122,8 @@ function attachParentListeners(callback) { }; process.on("unhandledRejection", handleUnhandledRejection); - process.on("uncaughtException", handleUncaughtException); + process.on("uncaughtException", handleUncaughtException); + /* ------------ Add exit event listener and import logger: ------------ */ // Attach exit event listener to display message in output & terminal when user stops the bot (before logger import so this runs before output-logger's exit event listener) @@ -134,8 +140,11 @@ function attachParentListeners(callback) { process.on("exit", parentExitEvent); + process.title = "CommentBot"; // Sets process title in task manager etc. + + // Import logger lib - cp = require("child_process"); + cp = require("child_process"); logger = require("output-logger"); // Look Mom, it's my own library! requestedKill = false; @@ -149,6 +158,7 @@ function attachParentListeners(callback) { exitmessage: "Goodbye!", }); + // Resume start/restart callback(); } @@ -163,8 +173,8 @@ function detachParentListeners() { logger.detachEventListeners(); if (handleUnhandledRejection) process.removeListener("unhandledRejection", handleUnhandledRejection); - if (handleUncaughtException) process.removeListener("uncaughtException", handleUncaughtException); - if (parentExitEvent) process.removeListener("exit", parentExitEvent); + if (handleUncaughtException) process.removeListener("uncaughtException", handleUncaughtException); + if (parentExitEvent) process.removeListener("exit", parentExitEvent); } @@ -200,6 +210,7 @@ function attachChildListeners() { logger("info", "Restarting...", false, true); require("../start.js").restart(argsobject); // Call restart function with argsobject }, 2000); + } else if (msg == "stop()") { requestedKill = true; @@ -225,7 +236,8 @@ function attachChildListeners() { }); } -/* ------- Provide function to get file if it doesn't exist. Export it as it will be used later by the bot as well as a failsafe ------- */ + +/* ------- Provide function to get a file if it doesn't exist. Export it to use it later as a failsafe from the bot process ------- */ /** * Checks if the needed file exists and gets it if it doesn't @@ -260,6 +272,7 @@ module.exports.checkAndGetFile = (file, logger, norequire = false, force = false } catch (err) {} //eslint-disable-line } + // Construct URL for restoring a file from GitHub let fileurl = `https://raw.githubusercontent.com/3urobeat/steam-comment-service-bot/${branch}/${file.slice(2, file.length)}`; // Remove the dot at the beginning of the file string @@ -267,7 +280,7 @@ module.exports.checkAndGetFile = (file, logger, norequire = false, force = false try { const https = require("https"); // Import two libs which will only be needed in this block - const path = require("path"); + const path = require("path"); let output = ""; @@ -275,50 +288,49 @@ module.exports.checkAndGetFile = (file, logger, norequire = false, force = false fs.mkdirSync(path.dirname(file), { recursive: true }); // Get the file - https - .get(fileurl, function (res) { - res.setEncoding("utf8"); + https.get(fileurl, function (res) { + res.setEncoding("utf8"); - res.on("data", function (chunk) { - output += chunk; - }); + res.on("data", function (chunk) { + output += chunk; + }); - // Write and test the file - res.on("end", () => { - fs.writeFile(file, output, (err) => { - if (err) { - logger("error", "checkAndGetFile() writeFile error: " + err); - resolve(undefined); - return; - } + // Write and test the file + res.on("end", () => { + fs.writeFile(file, output, (err) => { + if (err) { + logger("error", "checkAndGetFile() writeFile error: " + err); + resolve(undefined); + return; + } - if (norequire) resolve(file); + if (norequire) resolve(file); else resolve(require("." + file)); - }); }); - }) - .on("error", (err) => { - logger("error", `Fatal Error: File '${file}' is corrupted/missing and I can't restore it! Is your internet or GitHub down?\n ${err}`, true); - resolve(undefined); }); + }).on("error", (err) => { + logger("error", `Fatal Error: File '${file}' is corrupted/missing and I can't restore it! Is your internet or GitHub down?\n ${err}`, true); + resolve(undefined); + }); } catch (err) { logger("error", `Fatal Error: File ${file} is corrupted/missing and I can't restore it!\n ${err}`, true); resolve(undefined); } } + // Immediately get a new file if file doesn't exist or force is true if (!fs.existsSync(file) || force) { getNewFile(); - } else { - // ...otherwise check if file is intact if norequire is false + + } else { // ...otherwise check if file is intact if norequire is false if (norequire) { resolve(file); } else { try { - // Don't log debug msg for logger & handleErrors as they get loaded before the actual logger is loaded. This looks bad in the terminal, is kinda irrelevant and is logged even when logDebug is off - if (!file.includes("logger.js") && !file.includes("handleErrors.js")) logger("debug", `checkAndGetFile(): file ${file} exists, force and norequire are false. Testing integrity by requiring...`); // Ignore message for logger.js as it won't use the real logger yet + // Don't log debug msg for package, logger, handleErrors & npminteraction as they get loaded before the actual logger is loaded. This looks bad in the terminal, is kinda irrelevant and is logged even when logDebug is off + if (!file.includes("package.json") && !file.includes("logger.js") && !file.includes("handleErrors.js") && !file.includes("npminteraction.js")) logger("debug", `checkAndGetFile(): file ${file} exists, force and norequire are false. Testing integrity by requiring...`); // Ignore message for logger.js as it won't use the real logger yet let fileToLoad = require("." + file); @@ -333,6 +345,7 @@ module.exports.checkAndGetFile = (file, logger, norequire = false, force = false }); }; + /* ------------ Provide functions to start.js to start the bot: ------------ */ /** @@ -345,9 +358,7 @@ module.exports.run = () => { attachParentListeners(async () => { logger("info", "Starting process..."); - process.title = "CommentBot"; // Sets process title in task manager etc. - - // get file to start + // Get file to start let file = await this.checkAndGetFile("./src/controller/controller.js", logger, false, false); // We can call without norequire as process.argv[3] is set to 0 (see top of this file) to check controller.js for errors as well if (!file) return; @@ -358,6 +369,7 @@ module.exports.run = () => { }); }; + /** * Restart the application * @param {object} args The argument object that will be passed to `controller.restartargs()` @@ -368,8 +380,8 @@ module.exports.restart = async (args) => { logger("info", "Starting new process..."); try { - process.kill(args["pid"], "SIGKILL"); // Make sure the old child is dead - } catch (err) {} //eslint-disable-line + if (args["pid"]) process.kill(args["pid"], "SIGKILL"); // Make sure the old child is dead (if there was one) + } catch (err) {} // eslint-disable-line setTimeout(async () => { // Get file to start diff --git a/src/updater/compatibility.js b/src/updater/compatibility.js index 2969f45c..a6642b06 100644 --- a/src/updater/compatibility.js +++ b/src/updater/compatibility.js @@ -4,7 +4,7 @@ * Created Date: 04.05.2023 20:26:42 * Author: 3urobeat * - * Last Modified: 29.06.2023 22:35:03 + * Last Modified: 28.09.2023 18:33:22 * Modified By: 3urobeat * * Copyright (c) 2023 3urobeat @@ -37,8 +37,11 @@ module.exports.runCompatibility = async (controller) => { } - // List all files in compatibility directory - let list = fs.readdirSync("./src/updater/compatibility"); + // Initialize list with an empty array so the check below won't fail if the folder does not exist + let list = []; + + // List all files in compatibility directory if it exists + if (fs.existsSync("./src/updater/compatibility")) list = fs.readdirSync("./src/updater/compatibility"); // Try to find this version in list let match = list.find(e => e == controller.data.datafile.version.replace(/b[0-9]+/g, "") + ".js"); // Remove beta build from version so it still matches on every beta build | Old check used this regex pattern: str.match(/21200b[0-9]+/g) @@ -54,7 +57,7 @@ module.exports.runCompatibility = async (controller) => { } else { // Continue startup like normal if no file was found for this version - logger("debug", `Updater runCompatibility(): No compatibility feature was found for ${controller.data.datafile.version} in a list of ${list.length} files...`); + logger("debug", `Updater runCompatibility(): No compatibility feature was found for ${controller.data.datafile.version.replace(/b[0-9]+/g, "")} in a list of ${list.length} files...`); resolve(false); } diff --git a/src/updater/compatibility/21400.js b/src/updater/compatibility/21400.js new file mode 100644 index 00000000..9478a04b --- /dev/null +++ b/src/updater/compatibility/21400.js @@ -0,0 +1,77 @@ +/* + * File: 21400.js + * Project: steam-comment-service-bot + * Created Date: 28.09.2023 17:27:08 + * Author: 3urobeat + * + * Last Modified: 19.10.2023 19:28:48 + * Modified By: 3urobeat + * + * Copyright (c) 2023 3urobeat + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * You should have received a copy of the GNU General Public License along with this program. If not, see . + */ + + +const fs = require("fs"); + + +// Compatibility feature for upgrading to 2.14.0 +module.exports.run = (controller, resolve) => { + + // Convert customlang to new format + if (fs.existsSync(srcdir + "/../customlang.json")) { + try { + let customlang = require(srcdir + "/../customlang.json"); + + // Nest existing params into the default language if not done already + if (!customlang["english"]) { + customlang = { "english": customlang }; + + fs.writeFileSync(srcdir + "/../customlang.json", JSON.stringify(customlang, null, 4)); + } + } catch (err) { + logger("warn", "Compatibility feature 2.14: Failed to convert 'customlang.json'. Error: " + err); + } + } + + + let { config, advancedconfig } = controller.data; + + + // Config commentdelay, commentcooldown, maxComments & maxOwnerComments -> requestDelay, requestCooldown, maxRequests & maxOwnerRequests + if (config.commentdelay) config.requestDelay = config.commentdelay; + if (config.commentcooldown) config.requestCooldown = config.commentcooldown; + if (config.maxComments) config.maxRequests = config.maxComments; + if (config.maxOwnerComments) config.maxOwnerRequests = config.maxOwnerComments; + + delete config.commentdelay; + delete config.commentcooldown; + delete config.maxComments; + delete config.maxOwnerComments; + + controller.data.writeConfigToDisk(); + + + // Advancedconfig relogTimeout -> loginRetryTimeout + if (advancedconfig.relogTimeout != 900000) advancedconfig.loginRetryTimeout = advancedconfig.relogTimeout; // Only update loginRetryTimeout on update by checking if the old setting got transferred + + advancedconfig.relogTimeout = 900000; + + controller.data.writeAdvancedconfigToDisk(); + + + controller.data.datafile.compatibilityfeaturedone = true; // Set compatibilityfeaturedone to true, the bot would otherwise force another update + + controller.data.writeDatafileToDisk(); + + resolve(false); + +}; + +module.exports.info = { + "master": "21400", + "beta-testing": "21400b03" +}; \ No newline at end of file diff --git a/src/updater/helpers/createBackup.js b/src/updater/helpers/createBackup.js index 63619951..5e4b029d 100644 --- a/src/updater/helpers/createBackup.js +++ b/src/updater/helpers/createBackup.js @@ -4,7 +4,7 @@ * Created Date: 26.02.2022 16:54:03 * Author: 3urobeat * - * Last Modified: 04.07.2023 20:12:39 + * Last Modified: 02.09.2023 16:28:46 * Modified By: 3urobeat * * Copyright (c) 2022 3urobeat @@ -63,7 +63,7 @@ module.exports.run = () => { }); } - // Resolve when we are finished and not in a depper recursion level + // Resolve when we are finished and not in a deeper recursion level if (firstCall) resolve(); } diff --git a/src/updater/helpers/customUpdateRules.js b/src/updater/helpers/customUpdateRules.js index fc33fe07..61fb63e5 100644 --- a/src/updater/helpers/customUpdateRules.js +++ b/src/updater/helpers/customUpdateRules.js @@ -4,7 +4,7 @@ * Created Date: 22.02.2022 17:39:21 * Author: 3urobeat * - * Last Modified: 08.07.2023 00:36:58 + * Last Modified: 19.10.2023 19:14:37 * Modified By: 3urobeat * * Copyright (c) 2022 3urobeat @@ -37,10 +37,13 @@ module.exports.customUpdateRules = (compatibilityfeaturedone, oldconfig, oldadva let newconfig = require(srcdir + "/../config.json"); // Transfer every setting to the new config - Object.keys(newconfig).forEach(e => { - if (!Object.keys(oldconfig).includes(e)) return; // Config value seems to be new so don't bother trying to set it to something (which would probably be undefined anyway) + Object.keys(newconfig).forEach((e) => { + if (Object.keys(oldconfig).includes(e)) newconfig[e] = oldconfig[e]; // Transfer setting if oldconfig contains it + }); - newconfig[e] = oldconfig[e]; // Transfer setting + // Find and transfer removed config settings, the compatibility feature must process and delete them + Object.keys(oldconfig).forEach((e) => { + if (!Object.keys(newconfig).includes(e)) newconfig[e] = oldconfig[e]; // Transfer setting if newconfig does not contain it }); // Get arrays on one line @@ -66,10 +69,13 @@ module.exports.customUpdateRules = (compatibilityfeaturedone, oldconfig, oldadva let newadvancedconfig = require(srcdir + "/../advancedconfig.json"); // Transfer every setting to the new advancedconfig - Object.keys(newadvancedconfig).forEach(e => { - if (!Object.keys(oldadvancedconfig).includes(e)) return; // Config value seems to be new so don't bother trying to set it to something (which would probably be undefined anyway) + Object.keys(newadvancedconfig).forEach((e) => { + if (Object.keys(oldadvancedconfig).includes(e)) newadvancedconfig[e] = oldadvancedconfig[e]; // Transfer setting if oldconfig contains it + }); - newadvancedconfig[e] = oldadvancedconfig[e]; // Transfer setting + // Find and transfer removed advancedconfig settings, the compatibility feature must process and delete them + Object.keys(oldadvancedconfig).forEach((e) => { + if (!Object.keys(newadvancedconfig).includes(e)) newadvancedconfig[e] = oldadvancedconfig[e]; // Transfer setting if newconfig does not contain it }); // Get arrays on one line diff --git a/src/updater/helpers/prepareUpdate.js b/src/updater/helpers/prepareUpdate.js index 0f05ba63..5859aa7c 100644 --- a/src/updater/helpers/prepareUpdate.js +++ b/src/updater/helpers/prepareUpdate.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 09.07.2023 13:31:38 + * Last Modified: 02.09.2023 22:09:01 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -19,7 +19,7 @@ const Controller = require("../../controller/controller.js"); // eslint-disable- /** - * Wait for active requests and log off all bot accounts + * Waits for active requests to finish and logs off all bot accounts * @param {Controller} controller Reference to the controller object * @param {function(object, string): void} respondModule If defined, this function will be called with the result of the check. This allows to integrate checking for updates into commands or plugins. Passes resInfo and txt as parameters. * @param {import("../../commands/commandHandler.js").resInfo} resInfo Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). diff --git a/src/updater/updater.js b/src/updater/updater.js index bf9be4c0..c31ac2a9 100644 --- a/src/updater/updater.js +++ b/src/updater/updater.js @@ -4,7 +4,7 @@ * Created Date: 09.07.2021 16:26:00 * Author: 3urobeat * - * Last Modified: 09.07.2023 13:31:31 + * Last Modified: 10.09.2023 15:54:07 * Modified By: 3urobeat * * Copyright (c) 2021 3urobeat @@ -74,7 +74,7 @@ Updater.prototype.run = function(forceUpdate, respondModule, resInfo) { let checkForUpdate = await checkAndGetFile("./src/updater/helpers/checkForUpdate.js", logger, false, false); if (!checkForUpdate) return resolve(false); - checkForUpdate.check(this.data.datafile, null, forceUpdate, (updateFound, onlineData) => { + checkForUpdate.check(this.data.datafile, null, forceUpdate, async (updateFound, onlineData) => { // Check if no update was found and abort if (!updateFound) { @@ -101,7 +101,7 @@ Updater.prototype.run = function(forceUpdate, respondModule, resInfo) { respondModule(resInfo, `What's new: ${onlineData.whatsnew}`); // Instruct user to force update if disableAutoUpdate is true and stop here - if (this.data.advancedconfig.disableAutoUpdate && !forceUpdate) return respondModule(resInfo, this.data.lang.updaterautoupdatedisabled.replace(/cmdprefix/g, resInfo.cmdprefix)); + if (this.data.advancedconfig.disableAutoUpdate && !forceUpdate) return respondModule(resInfo, await this.data.getLang("updaterautoupdatedisabled", { "cmdprefix": resInfo.cmdprefix }, resInfo.userID)); } @@ -150,7 +150,7 @@ Updater.prototype.run = function(forceUpdate, respondModule, resInfo) { // Restart and indicate that the update failed resolve(true); - _this.controller.restart(JSON.stringify({ skippedaccounts: _this.controller.skippedaccounts, updateFailed: true })); + _this.controller.restart(JSON.stringify({ skippedaccounts: _this.controller.info.skippedaccounts, updateFailed: true })); } else { // Update succeeded, update npm dependencies and restart @@ -161,7 +161,7 @@ Updater.prototype.run = function(forceUpdate, respondModule, resInfo) { // Continue and pray nothing bad happens if the npminteraction helper got lost in the sauce somehow if (!npminteraction) { logger("error", "I failed trying to update the dependencies. Please check the log after other errors for more information.\nTrying to continue anyway..."); - _this.controller.restart(JSON.stringify({ skippedaccounts: _this.controller.skippedaccounts, updateFailed: false })); + _this.controller.restart(JSON.stringify({ skippedaccounts: _this.controller.info.skippedaccounts, updateFailed: false })); resolve(true); // Finished updating! return; } @@ -173,7 +173,7 @@ Updater.prototype.run = function(forceUpdate, respondModule, resInfo) { // If everything went to plan, resolve our promise and restart the bot! logger("", `${logger.colors.fgyellow}Done! Restarting...\n${logger.colors.reset}`, true); resolve(true); - _this.controller.restart(JSON.stringify({ skippedaccounts: _this.controller.skippedaccounts, updateFailed: false })); + _this.controller.restart(JSON.stringify({ skippedaccounts: _this.controller.info.skippedaccounts, updateFailed: false })); }); } } diff --git a/types/types.d.ts b/types/types.d.ts index d854071a..b5ff4711 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -29,10 +29,46 @@ declare class Bot { * Additional login related information for this bot account */ loginData: any; + /** + * Stores the timestamp and reason of the last disconnect. This is used by handleRelog() to take proper action + */ + lastDisconnect: any; /** * Calls SteamUser logOn() for this account. This will either trigger the SteamUser loggedOn or error event. */ _loginToSteam(): void; + /** + * Handles the SteamUser debug events if enabled in advancedconfig + */ + _attachSteamDebugEvent(): void; + /** + * Handles the SteamUser disconnect event and tries to relog the account + */ + _attachSteamDisconnectedEvent(): void; + /** + * Handles the SteamUser error event + */ + _attachSteamErrorEvent(): void; + /** + * Handles messages, cooldowns and executes commands. + */ + _attachSteamFriendMessageEvent(): void; + /** + * Do some stuff when account is logged in + */ + _attachSteamLoggedOnEvent(): void; + /** + * Accepts a friend request, adds the user to the lastcomment.db database and invites him to your group + */ + _attachSteamFriendRelationshipEvent(): void; + /** + * Accepts a group invite if acceptgroupinvites in the config is true + */ + _attachSteamGroupRelationshipEvent(): void; + /** + * Handles setting cookies and accepting offline friend & group invites + */ + _attachSteamWebSessionEvent(): void; /** * Checks if user is blocked, has an active cooldown for spamming or isn't a friend * @param steamID64 - The steamID64 of the message sender @@ -48,6 +84,20 @@ declare class Bot { * Handles checking for missing game licenses, requests them and then starts playing */ handleMissingGameLicenses(): void; + /** + * Changes the proxy of this bot account and relogs it. + * @param newProxyIndex - Index of the new proxy inside the DataManager.proxies array. + */ + switchProxy(newProxyIndex: number): void; + /** + * Checks host internet connection, updates the status of all proxies checked >2.5 min ago and switches the proxy of this bot account if necessary. + * @returns Resolves with a boolean indicating whether the proxy was switched when done. A relog is triggered when the proxy was switched. + */ + checkAndSwitchMyProxy(): Promise; + /** + * Attempts to get this account, after failing all logOnRetries, back online after some time. Does not apply to initial logins. + */ + handleRelog(): void; /** * Our commandHandler respondModule implementation - Sends a message to a Steam user * @param _this - The Bot object context @@ -110,6 +160,20 @@ declare class Bot { * Handles checking for missing game licenses, requests them and then starts playing */ handleMissingGameLicenses(): void; + /** + * Changes the proxy of this bot account and relogs it. + * @param newProxyIndex - Index of the new proxy inside the DataManager.proxies array. + */ + switchProxy(newProxyIndex: number): void; + /** + * Checks host internet connection, updates the status of all proxies checked >2.5 min ago and switches the proxy of this bot account if necessary. + * @returns Resolves with a boolean indicating whether the proxy was switched when done. A relog is triggered when the proxy was switched. + */ + checkAndSwitchMyProxy(): Promise; + /** + * Attempts to get this account, after failing all logOnRetries, back online after some time. Does not apply to initial logins. + */ + handleRelog(): void; /** * Our commandHandler respondModule implementation - Sends a message to a Steam user * @param _this - The Bot object context @@ -240,19 +304,19 @@ declare function comment(commandHandler: CommandHandler, resInfo: CommandHandler * Retrieves arguments from a comment request. If request is invalid (for example too many comments requested) an error message will be sent * @param commandHandler - The commandHandler object * @param args - The command arguments - * @param requesterSteamID64 - The steamID64 of the requesting user + * @param requesterID - The steamID64 of the requesting user * @param resInfo - Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). * @param respond - The shortened respondModule call * @returns Resolves promise with object containing all relevant data when done */ -declare function getCommentArgs(commandHandler: CommandHandler, args: any[], requesterSteamID64: string, resInfo: CommandHandler.resInfo, respond: (...params: any[]) => any): Promise<{ maxRequestAmount: number; commentcmdUsage: string; numberOfComments: number; profileID: string; idType: string; quotesArr: string[]; }>; +declare function getCommentArgs(commandHandler: CommandHandler, args: any[], requesterID: string, resInfo: CommandHandler.resInfo, respond: (...params: any[]) => any): Promise<{ maxRequestAmount: number; commentcmdUsage: string; numberOfComments: number; profileID: string; idType: string; quotesArr: string[]; }>; /** * Finds all needed and currently available bot accounts for a comment request. * @param commandHandler - The commandHandler object * @param numberOfComments - Number of requested comments * @param canBeLimited - If the accounts are allowed to be limited - * @param idType - Type of the request. This can either be "profile", "group" or "sharedfile". This is used to determine if limited accs need to be added first. + * @param idType - Type of the request. This can either be "profile", "group", "sharedfile" or "discussion". This is used to determine if limited accs need to be added first. * @param receiverSteamID - Optional: steamID64 of the receiving user. If set, accounts that are friend with the user will be prioritized and accsToAdd will be calculated. * @returns `availableAccounts` contains all account names from bot object, `accsToAdd` account names which are limited and not friend, `whenAvailable` is a timestamp representing how long to wait until accsNeeded accounts will be available and `whenAvailableStr` is formatted human-readable as time from now */ @@ -269,7 +333,30 @@ declare function getAvailableBotsForCommenting(commandHandler: CommandHandler, n declare function getAvailableBotsForFavorizing(commandHandler: CommandHandler, amount: number | "all", id: string, favType: string): Promise<{ amount: number; availableAccounts: string[]; whenAvailable: number; whenAvailableStr: string; }>; /** - * Retrieves arguments from a vote request. If request is invalid, an error message will be sent + * Retrieves arguments from a follow request. If request is invalid, an error message will be sent + * @param commandHandler - The commandHandler object + * @param args - The command arguments + * @param cmd - Either "upvote", "downvote", "favorite" or "unfavorite", depending on which command is calling this function + * @param resInfo - Object containing additional information your respondModule might need to process the response (for example the userID who executed the command). + * @param respond - The shortened respondModule call + * @returns If the user provided a specific amount, amount will be a number. If user provided "all" or "max", it will be returned as an unmodified string for getVoteBots.js to handle + */ +declare function getFollowArgs(commandHandler: CommandHandler, args: any[], cmd: string, resInfo: CommandHandler.resInfo, respond: (...params: any[]) => any): Promise<{ amount: number | string; id: string; }>; + +/** + * Finds all needed and currently available bot accounts for a follow request. + * @param commandHandler - The commandHandler object + * @param amount - Amount of favs requested or "all" to get the max available amount + * @param canBeLimited - If the accounts are allowed to be limited + * @param id - The user id to follow + * @param idType - Either "user" or "curator" + * @param favType - Either "follow" or "unfollow", depending on which request this is + * @returns Resolves with obj: `availableAccounts` contains all account names from bot object, `whenAvailable` is a timestamp representing how long to wait until accsNeeded accounts will be available and `whenAvailableStr` is formatted human-readable as time from now + */ +declare function getAvailableBotsForFollowing(commandHandler: CommandHandler, amount: number | "all", canBeLimited: boolean, id: string, idType: string, favType: string): Promise<{ amount: number; availableAccounts: string[]; whenAvailable: number; whenAvailableStr: string; }>; + +/** + * Retrieves arguments from a favorite/vote request. If request is invalid, an error message will be sent * @param commandHandler - The commandHandler object * @param args - The command arguments * @param cmd - Either "upvote", "downvote", "favorite" or "unfavorite", depending on which command is calling this function @@ -322,6 +409,31 @@ declare function sortFailedCommentsObject(failedObj: any): void; */ declare function failedCommentsObjToString(obj: any): string; +/** + * Checks if the following follow process iteration should be skipped + * @param commandHandler - The commandHandler object + * @param loop - Object returned by misc.js syncLoop() helper + * @param bot - Bot object of the account making this request + * @param id - ID of the profile that receives the follow + * @returns `true` if iteration should continue, `false` if iteration should be skipped using return + */ +declare function handleFollowIterationSkip(commandHandler: CommandHandler, loop: any, bot: Bot, id: string): boolean; + +/** + * Logs follow errors + * @param error - The error string returned by steam-community + * @param commandHandler - The commandHandler object + * @param bot - Bot object of the account making this request + * @param id - ID of the profile that receives the follow + */ +declare function logFollowError(error: string, commandHandler: CommandHandler, bot: Bot, id: string): void; + +/** + * Helper function to sort failed object by comment number so that it is easier to read + * @param failedObj - Current state of failed object + */ +declare function sortFailedCommentsObject(failedObj: any): void; + /** * Checks if the following vote process iteration should be skipped * @param commandHandler - The commandHandler object @@ -376,7 +488,7 @@ declare class Controller { */ misc: any; /** - * Internal: Inits the DataManager system, runs the updater and starts all bot accounts + * Internal: Initializes the bot by importing data from the disk, running the updater and finally logging in all bot accounts. */ _start(): void; /** @@ -405,7 +517,7 @@ declare class Controller { pluginSystem: PluginSystem; /** * Restarts the whole application - * @param data - Stringified restartdata object that will be kept through restarts + * @param data - Optional: Stringified restartdata object that will be kept through restarts */ restart(data: string): void; /** @@ -455,15 +567,22 @@ declare class Controller { * @returns An array or object if `mapToObject == true` containing all matching bot accounts. */ getBots(statusFilter?: EStatus | EStatus[] | string, mapToObject: boolean): any[] | any; + /** + * Retrieves bot accounts per proxy. This can be used to find the most and least used active proxies for example. + * @param [filterOffline = false] - Set to true to remove proxies which are offline. Make sure to call `checkAllProxies()` beforehand! + * @returns Bot accounts mapped to their associated proxy + */ + getBotsPerProxy(filterOffline?: boolean): { bots: Bot[]; proxy: string; proxyIndex: number; isOnline: boolean; lastOnlineCheck: number; }[]; /** * Internal: Handles process's unhandledRejection & uncaughtException error events. * Should a NPM related error be detected it attempts to reinstall all packages using our npminteraction helper function */ _handleErrors(): void; /** - * Handles converting URLs to steamIDs, determining their type if unknown and checking if it matches your expectation + * Handles converting URLs to steamIDs, determining their type if unknown and checking if it matches your expectation. + * Note: You need to provide a full URL for discussions & curators. For discussions only type checking/determination is supported. * @param str - The profileID argument provided by the user - * @param expectedIdType - The type of SteamID expected ("profile", "group" or "sharedfile") or `null` if type should be assumed. + * @param expectedIdType - The type of SteamID expected ("profile", "group", "sharedfile", "discussion" or "curator") or `null` if type should be assumed. */ handleSteamIdResolving(str: string, expectedIdType: string, callback: any): void; /** @@ -523,15 +642,22 @@ declare class Controller { * @returns An array or object if `mapToObject == true` containing all matching bot accounts. */ getBots(statusFilter?: EStatus | EStatus[] | string, mapToObject: boolean): any[] | any; + /** + * Retrieves bot accounts per proxy. This can be used to find the most and least used active proxies for example. + * @param [filterOffline = false] - Set to true to remove proxies which are offline. Make sure to call `checkAllProxies()` beforehand! + * @returns Bot accounts mapped to their associated proxy + */ + getBotsPerProxy(filterOffline?: boolean): { bots: Bot[]; proxy: string; proxyIndex: number; isOnline: boolean; lastOnlineCheck: number; }[]; /** * Internal: Handles process's unhandledRejection & uncaughtException error events. * Should a NPM related error be detected it attempts to reinstall all packages using our npminteraction helper function */ _handleErrors(): void; /** - * Handles converting URLs to steamIDs, determining their type if unknown and checking if it matches your expectation + * Handles converting URLs to steamIDs, determining their type if unknown and checking if it matches your expectation. + * Note: You need to provide a full URL for discussions & curators. For discussions only type checking/determination is supported. * @param str - The profileID argument provided by the user - * @param expectedIdType - The type of SteamID expected ("profile", "group" or "sharedfile") or `null` if type should be assumed. + * @param expectedIdType - The type of SteamID expected ("profile", "group", "sharedfile", "discussion" or "curator") or `null` if type should be assumed. */ handleSteamIdResolving(str: string, expectedIdType: string, callback: any): void; /** @@ -592,12 +718,20 @@ declare function round(value: number, decimals: number): number; declare function timeToString(timestamp: number): string; /** - * Pings an URL to check if the service and this internet connection is working + * Pings a **https** URL to check if the service and this internet connection is working * @param url - The URL of the service to check - * @param throwTimeout - If true, the function will throw a timeout error if Steam can't be reached after 20 seconds + * @param [throwTimeout = false] - If true, the function will throw a timeout error if Steam can't be reached after 20 seconds + * @param [proxy] - Provide a proxy if the connection check should be made through a proxy instead of the local connection * @returns Resolves on response code 2xx and rejects on any other response code. Both are called with parameter `response` (Object) which has a `statusMessage` (String) and `statusCode` (Number) key. `statusCode` is `null` if request failed. */ -declare function checkConnection(url: string, throwTimeout: boolean): Promise<{ statusMessage: string; statusCode: number | null; }>; +declare function checkConnection(url: string, throwTimeout?: boolean, proxy?: any): Promise<{ statusMessage: string; statusCode: number | null; }>; + +/** + * Splits a HTTP proxy URL into its parts + * @param url - The HTTP proxy URL + * @returns Object containing the proxy parts + */ +declare function splitProxyString(url: string): any; /** * Helper function which attempts to cut Strings intelligently and returns all parts. It will attempt to not cut words & links in half. @@ -635,9 +769,9 @@ declare class DataManager { constructor(controller: Controller); /** * Checks currently loaded data for validity and logs some recommendations for a few settings. - * @returns Resolves promise when all checks have finished. If promise is rejected you should terminate the application or reset the changes. Reject is called with a String specifying the failed check. + * @returns Resolves with `null` when all settings have been accepted, or with a string containing reasons if a setting has been reset. On reject you should terminate the application. It is called with a String specifying the failed check. */ - checkData(): Promise; + checkData(): Promise; /** * Writes (all) files imported by DataManager back to the disk */ @@ -675,6 +809,13 @@ declare class DataManager { * @returns Resolves promise when all files have been loaded successfully. The function will log an error and terminate the application should a fatal error occur. */ _importFromDisk(): Promise; + /** + * Verifies the data integrity of every source code file in the project by comparing its checksum. + * This function is used to verify the integrity of every module loaded AFTER the controller & DataManager. Both of those need manual checkAndGetFile() calls to import, which is handled by the Controller. + * If an already loaded file needed to be recovered then the bot will restart to load these changes. + * @returns Resolves when all files have been checked and, if necessary, restored. Does not resolve if the bot needs to be restarted. + */ + verifyIntegrity(): Promise; /** * Reference to the controller object */ @@ -693,7 +834,7 @@ declare class DataManager { */ advancedconfig: any; /** - * Stores all language strings used for responding to a user. + * Stores all supported languages and their strings used for responding to a user. * All default strings have already been replaced with corresponding matches from `customlang.json`. */ lang: any; @@ -704,7 +845,7 @@ declare class DataManager { /** * Stores all proxies provided via the `proxies.txt` file. */ - proxies: string[]; + proxies: { proxy: string; proxyIndex: number; isOnline: boolean; lastOnlineCheck: number; }[]; /** * Stores IDs from config files converted at runtime and backups for all config & data files. */ @@ -715,24 +856,34 @@ declare class DataManager { logininfo: any; /** * Database which stores the timestamp of the last request of every user. This is used to enforce `config.unfriendTime`. - * Document structure: { id: String, time: Number } + * Document structure: { id: string, time: Number } */ lastCommentDB: Nedb; /** - * Database which stores information about which bot accounts have already voted on which sharedfiles. This allows us to filter without pinging Steam for every account on every request. - * Document structure: { id: String, accountName: String, type: String, time: Number } + * Database which stores information about which bot accounts have fulfilled one-time requests (vote, fav, follow). This allows us to filter without pinging Steam for every account on every request. + * Document structure: { id: string, accountName: string, type: string, time: Number } */ ratingHistoryDB: Nedb; /** * Database which stores the refreshTokens for all bot accounts. - * Document structure: { accountName: String, token: String } + * Document structure: { accountName: string, token: string } */ tokensDB: Nedb; + /** + * Database which stores user specific settings, for example the language set + * Document structure: { id: string, lang: string } + */ + userSettingsDB: Nedb; + /** + * Loads all DataManager helper files. This is done outside of the constructor to be able to await it. + * @returns Resolved when all files have been loaded + */ + _loadDataManagerFiles(): Promise; /** * Checks currently loaded data for validity and logs some recommendations for a few settings. - * @returns Resolves promise when all checks have finished. If promise is rejected you should terminate the application or reset the changes. Reject is called with a String specifying the failed check. + * @returns Resolves with `null` when all settings have been accepted, or with a string containing reasons if a setting has been reset. On reject you should terminate the application. It is called with a String specifying the failed check. */ - checkData(): Promise; + checkData(): Promise; /** * Writes (all) files imported by DataManager back to the disk */ @@ -770,20 +921,47 @@ declare class DataManager { * @returns Resolves promise when all files have been loaded successfully. The function will log an error and terminate the application should a fatal error occur. */ _importFromDisk(): Promise; + /** + * Verifies the data integrity of every source code file in the project by comparing its checksum. + * This function is used to verify the integrity of every module loaded AFTER the controller & DataManager. Both of those need manual checkAndGetFile() calls to import, which is handled by the Controller. + * If an already loaded file needed to be recovered then the bot will restart to load these changes. + * @returns Resolves when all files have been checked and, if necessary, restored. Does not resolve if the bot needs to be restarted. + */ + verifyIntegrity(): Promise; /** * Converts owners and groups imported from config.json to steam ids and updates cachefile. (Call this after dataImport and before dataCheck) */ processData(): void; + /** + * Checks if a proxy can reach steamcommunity.com and updates its isOnline and lastOnlineCheck + * @param proxyIndex - Index of the proxy to check in the DataManager proxies array + * @returns True if the proxy can reach steamcommunity.com, false otherwise. + */ + checkProxy(proxyIndex: number): boolean; + /** + * Checks all proxies if they can reach steamcommunity.com and updates their entries + * @param [ignoreLastCheckedWithin = 0] - Ignore proxies that have already been checked in less than `ignoreLastCheckedWithin` ms + * @returns Resolves when all proxies have been checked + */ + checkAllProxies(ignoreLastCheckedWithin?: number): Promise; + /** + * Retrieves a language string from one of the available language files and replaces keywords if desired. + * If a userID is provided it will lookup which language the user has set. If nothing is set, the default language set in the config will be returned. + * @param str - Name of the language string to be retrieved + * @param [userIDOrLanguage] - Optional: ID of the user to lookup in the userSettings database. You can also pass the name of a supported language like "english" to get a specific language. + * @returns Returns a promise that resolves with the language string or `null` if it could not be found. + */ + getLang(str: string, replace: any, userIDOrLanguage?: string): Promise; /** * Gets a random quote * @param quotesArr - Optional: Custom array of quotes to choose from. If not provided the default quotes set which was imported from the disk will be used. - * @returns Resolves with `quote` (String) + * @returns Resolves with `quote` (string) */ getQuote(quotesArr: any[]): Promise; /** * Checks if a user ID is currently on cooldown and formats human readable lastRequestStr and untilStr strings. * @param id - ID of the user to look up - * @returns Resolves with object containing `lastRequest` (Unix timestamp of the last interaction received), `until` (Unix timestamp of cooldown end), `lastRequestStr` (How long ago as String), `untilStr` (Wait until as String). If id wasn't found, `null` will be returned. + * @returns Resolves with object containing `lastRequest` (Unix timestamp of the last interaction received), `until` (Unix timestamp of cooldown end), `lastRequestStr` (How long ago as string), `untilStr` (Wait until as string). If id wasn't found, `null` will be returned. */ getUserCooldown(id: string): Promise<{ lastRequest: number; until: number; lastRequestStr: string; untilStr: string; } | null>; /** @@ -797,7 +975,7 @@ declare class DataManager { */ _startExpiringTokensCheckInterval(): void; /** - * Internal: Asks user if he/she wants to refresh the tokens of all expiring accounts when no active request was found and relogs them + * Internal: Asks user if they want to refresh the tokens of all expiring accounts when no active request was found and relogs them * @param expiring - Object of botobject entries to ask user for */ _askForGetNewToken(expiring: any): void; @@ -838,16 +1016,36 @@ declare class DataManager { * Converts owners and groups imported from config.json to steam ids and updates cachefile. (Call this after dataImport and before dataCheck) */ processData(): void; + /** + * Checks if a proxy can reach steamcommunity.com and updates its isOnline and lastOnlineCheck + * @param proxyIndex - Index of the proxy to check in the DataManager proxies array + * @returns True if the proxy can reach steamcommunity.com, false otherwise. + */ + checkProxy(proxyIndex: number): boolean; + /** + * Checks all proxies if they can reach steamcommunity.com and updates their entries + * @param [ignoreLastCheckedWithin = 0] - Ignore proxies that have already been checked in less than `ignoreLastCheckedWithin` ms + * @returns Resolves when all proxies have been checked + */ + checkAllProxies(ignoreLastCheckedWithin?: number): Promise; + /** + * Retrieves a language string from one of the available language files and replaces keywords if desired. + * If a userID is provided it will lookup which language the user has set. If nothing is set, the default language set in the config will be returned. + * @param str - Name of the language string to be retrieved + * @param [userIDOrLanguage] - Optional: ID of the user to lookup in the userSettings database. You can also pass the name of a supported language like "english" to get a specific language. + * @returns Returns a promise that resolves with the language string or `null` if it could not be found. + */ + getLang(str: string, replace: any, userIDOrLanguage?: string): Promise; /** * Gets a random quote * @param quotesArr - Optional: Custom array of quotes to choose from. If not provided the default quotes set which was imported from the disk will be used. - * @returns Resolves with `quote` (String) + * @returns Resolves with `quote` (string) */ getQuote(quotesArr: any[]): Promise; /** * Checks if a user ID is currently on cooldown and formats human readable lastRequestStr and untilStr strings. * @param id - ID of the user to look up - * @returns Resolves with object containing `lastRequest` (Unix timestamp of the last interaction received), `until` (Unix timestamp of cooldown end), `lastRequestStr` (How long ago as String), `untilStr` (Wait until as String). If id wasn't found, `null` will be returned. + * @returns Resolves with object containing `lastRequest` (Unix timestamp of the last interaction received), `until` (Unix timestamp of cooldown end), `lastRequestStr` (How long ago as string), `untilStr` (Wait until as string). If id wasn't found, `null` will be returned. */ getUserCooldown(id: string): Promise<{ lastRequest: number; until: number; lastRequestStr: string; untilStr: string; } | null>; /** @@ -861,7 +1059,7 @@ declare class DataManager { */ _startExpiringTokensCheckInterval(): void; /** - * Internal: Asks user if he/she wants to refresh the tokens of all expiring accounts when no active request was found and relogs them + * Internal: Asks user if they want to refresh the tokens of all expiring accounts when no active request was found and relogs them * @param expiring - Object of botobject entries to ask user for */ _askForGetNewToken(expiring: any): void; @@ -900,6 +1098,43 @@ declare class DataManager { _pullNewFile(name: string, filepath: string, resolve: (...params: any[]) => any, noRequire: boolean): void; } +/** + * Constructor - Creates a new Discussion object + */ +declare class CSteamDiscussion { + constructor(community: SteamCommunity, data: any); + _community: SteamCommunity; + /** + * Scrapes a range of comments from this discussion + * @param startIndex - Index (0 based) of the first comment to fetch + * @param endIndex - Index (0 based) of the last comment to fetch + * @param callback - First argument is null/Error, second is array containing the requested comments + */ + getComments(startIndex: number, endIndex: number, callback: (...params: any[]) => any): void; + /** + * Posts a comment to this discussion's comment section + * @param message - Content of the comment to post + * @param callback - Takes only an Error object/null as the first argument + */ + postComment(message: string, callback: (...params: any[]) => any): void; + /** + * Delete a comment from this discussion's comment section + * @param gidcomment - ID of the comment to delete + * @param callback - Takes only an Error object/null as the first argument + */ + deleteComment(gidcomment: string, callback: (...params: any[]) => any): void; + /** + * Subscribes to this discussion's comment section + * @param callback - Takes only an Error object/null as the first argument + */ + subscribe(callback: (...params: any[]) => any): void; + /** + * Unsubscribes from this discussion's comment section + * @param callback - Takes only an Error object/null as the first argument + */ + unsubscribe(callback: (...params: any[]) => any): void; +} + /** * Constructor - Creates a new SharedFile object */ @@ -1147,6 +1382,11 @@ declare class SessionHandler { * Internal - Attempts to log into account with credentials */ _attemptCredentialsLogin(): void; + /** + * Attempts to renew the refreshToken used for the current session. Whether a new token will actually be issued is at the discretion of Steam. + * @returns Returns a promise which resolves with `true` if Steam issued a new token, `false` otherwise. Rejects if no token is stored in the database. + */ + attemptTokenRenew(): Promise; /** * Internal: Attaches listeners to all steam-session events we care about */