init commit
This commit is contained in:
parent
f1edb29c97
commit
3cdfb88ae3
43
CHANGELOG.md
Normal file
43
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Changelog
|
||||||
|
- **0.9961a - October 21, 2021**
|
||||||
|
- Shortened the permission checks for bot.queueMedia, forgot that the permission check function already does most of the work..
|
||||||
|
- ..and fixed bot.queueMedia, thought I removed something before pushing it
|
||||||
|
- **0.996a - October 21, 2021**
|
||||||
|
- Added bot.queueMedia function to allow the bot to add things to the queue
|
||||||
|
- This doesn't have any command associated with it, though. It's there if you'd like to make your own commands. There's a little JSDoc thing above the function definition for reference.
|
||||||
|
- An example for adding a simple temporary YouTube video would be: `bot.queueMedia("end", "youtube_video_url", true);`
|
||||||
|
- You can also use `"next"` instead of `"end"` to place it next in the queue assuming the bot has permission to.
|
||||||
|
- Custom embeds should work too. `bot.queueMedia("end", "customembed", true, "<iframe src='source or whatever'>", "Media Title");`
|
||||||
|
- Added `cfg.connection.sameDomainSocketOnly` option to the config, please add this to any existing configs if you need to. It will default to `true` if it does not exist.
|
||||||
|
- Only set this to `false` if you need to connect to a socket that does not live on the domain your CyTube fork is on.
|
||||||
|
- As far as I know, this is only relevant for some CyTube hosts, and not the main CyTube site.
|
||||||
|
- Logged media links (such as "now playing" logs) will have the full link instead of the shortened one
|
||||||
|
- Fixed dumb Discord bot oversight causing it to crash for those who use it (probably no one)
|
||||||
|
- Removed some room-specific emotes and filters from some commands
|
||||||
|
- Probably missed some, oops
|
||||||
|
- Added `/mute` and `/unmute` commands to the CLI, which work exactly the same as their chat command counterparts
|
||||||
|
- Added the ability for `sendChatMsg` to bypass the mute state, which the CLI command `/say` now does (this is the only command that does so by default)
|
||||||
|
- Added some clarification to some comments
|
||||||
|
- Now prevents invalid requests to update emote counts if the user is unregistered or a guest and guest data is off
|
||||||
|
- Didn't do anything other than spam DB errors
|
||||||
|
- Fixed a regular expression used for some wolfram responses
|
||||||
|
- Queue fail and queue warn logs should now properly display their messages
|
||||||
|
- Fixed issue when chat message was missing from the chatMsg frame
|
||||||
|
- This issue is only related to certain forks
|
||||||
|
- Updated license dates
|
||||||
|
- **0.9951a - January 24, 2021**
|
||||||
|
- Fixed a rare bug with chat messages sent by the server
|
||||||
|
- **0.995a - December 14, 2020**
|
||||||
|
- Increased length of room_time and afk_time for the users table
|
||||||
|
- For existing databases, run `ALTER TABLE SCHEMA.users ALTER COLUMN room_time TYPE DECIMAL(13,3);` and again for the column `afk_time`. Be sure to replace SCHEMA with whatever your schema is called, which is usually your room's name.
|
||||||
|
- Fixes room times hitting the upper limit within a few months' time.
|
||||||
|
- **0.994a - November 8, 2020**
|
||||||
|
- Add hyphen (-) to blacklistvid ID regex
|
||||||
|
- **0.993a - October 3, 2020**
|
||||||
|
- Changed the config option `misc.autoAFK` to a tri-state option. Must be updated in existing configs, or it will be ignored.
|
||||||
|
- 0: Off (default)
|
||||||
|
- 1: Always AFK
|
||||||
|
- 2: Always active
|
||||||
|
- `ipban` reason will be padded if it begins with `wrange` or `range`
|
||||||
|
- Actually use the `exitCode` passed to `bot.kill` (lol)
|
||||||
|
- DB features will be disabled if the connection to the database is refused
|
||||||
22
LICENSE
Normal file
22
LICENSE
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020-2021 deerfarce and contributors
|
||||||
|
CyTube code Copyright (c) 2013-2021 Calvin Montgomery and contributors, where noted
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
95
README.md
95
README.md
|
|
@ -1,93 +1,32 @@
|
||||||
# Tokebot
|
#Tokebot V1
|
||||||
|
Tokebot for fore.st, made with ChozoBot.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- nodejs (12+, last tested on 12.18.2)
|
||||||
|
- postgresql (tested on 12.3, optional - only if using database. will write more setup info eventually. create a schema with the name of the room you're using the bot in and grant all perms to the user who owns the database)
|
||||||
|
- root/admin access for setting up psql
|
||||||
|
|
||||||
## Getting started
|
## Setup
|
||||||
|
|
||||||
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
|
I'll get around to writing this readme properly, but:
|
||||||
|
|
||||||
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
|
Copy the config.example.js file and name it config.js. If you're providing a room parameter to the bot (see below), name it config-roomname.js instead.
|
||||||
|
|
||||||
## Add your files
|
Read through the configuration file and carefully make sure everything is set just right.
|
||||||
|
|
||||||
- [ ] [Create](https://gitlab.com/-/experiment/new_project_readme_content:e7323b580c24353b1290949e87e6a6a7?https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://gitlab.com/-/experiment/new_project_readme_content:e7323b580c24353b1290949e87e6a6a7?https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
|
Install the node modules: `npm install`
|
||||||
- [ ] [Add files using the command line](https://gitlab.com/-/experiment/new_project_readme_content:e7323b580c24353b1290949e87e6a6a7?https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
|
|
||||||
|
|
||||||
```
|
|
||||||
cd existing_repo
|
|
||||||
git remote add origin https://gitlab.com/rainbownapkin/tokebot.git
|
|
||||||
git branch -M main
|
|
||||||
git push -uf origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integrate with your tools
|
|
||||||
|
|
||||||
- [ ] [Set up project integrations](https://gitlab.com/-/experiment/new_project_readme_content:e7323b580c24353b1290949e87e6a6a7?https://gitlab.com/rainbownapkin/tokebot/-/settings/integrations)
|
|
||||||
|
|
||||||
## Collaborate with your team
|
|
||||||
|
|
||||||
- [ ] [Invite team members and collaborators](https://gitlab.com/-/experiment/new_project_readme_content:e7323b580c24353b1290949e87e6a6a7?https://docs.gitlab.com/ee/user/project/members/)
|
|
||||||
- [ ] [Create a new merge request](https://gitlab.com/-/experiment/new_project_readme_content:e7323b580c24353b1290949e87e6a6a7?https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
|
|
||||||
- [ ] [Automatically close issues from merge requests](https://gitlab.com/-/experiment/new_project_readme_content:e7323b580c24353b1290949e87e6a6a7?https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
|
|
||||||
- [ ] [Enable merge request approvals](https://gitlab.com/-/experiment/new_project_readme_content:e7323b580c24353b1290949e87e6a6a7?https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
|
|
||||||
- [ ] [Automatically merge when pipeline succeeds](https://gitlab.com/-/experiment/new_project_readme_content:e7323b580c24353b1290949e87e6a6a7?https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
|
|
||||||
|
|
||||||
## Test and Deploy
|
|
||||||
|
|
||||||
Use the built-in continuous integration in GitLab.
|
|
||||||
|
|
||||||
- [ ] [Get started with GitLab CI/CD](https://gitlab.com/-/experiment/new_project_readme_content:e7323b580c24353b1290949e87e6a6a7?https://docs.gitlab.com/ee/ci/quick_start/index.html)
|
|
||||||
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://gitlab.com/-/experiment/new_project_readme_content:e7323b580c24353b1290949e87e6a6a7?https://docs.gitlab.com/ee/user/application_security/sast/)
|
|
||||||
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://gitlab.com/-/experiment/new_project_readme_content:e7323b580c24353b1290949e87e6a6a7?https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
|
|
||||||
- [ ] [Use pull-based deployments for improved Kubernetes management](https://gitlab.com/-/experiment/new_project_readme_content:e7323b580c24353b1290949e87e6a6a7?https://docs.gitlab.com/ee/user/clusters/agent/)
|
|
||||||
- [ ] [Set up protected environments](https://gitlab.com/-/experiment/new_project_readme_content:e7323b580c24353b1290949e87e6a6a7?https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
# Editing this README
|
|
||||||
|
|
||||||
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://gitlab.com/-/experiment/new_project_readme_content:e7323b580c24353b1290949e87e6a6a7?https://www.makeareadme.com/) for this template.
|
|
||||||
|
|
||||||
## Suggestions for a good README
|
|
||||||
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
|
|
||||||
|
|
||||||
## Name
|
|
||||||
Choose a self-explaining name for your project.
|
|
||||||
|
|
||||||
## Description
|
|
||||||
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
|
|
||||||
|
|
||||||
## Badges
|
|
||||||
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
|
|
||||||
|
|
||||||
## Visuals
|
|
||||||
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
|
|
||||||
|
|
||||||
## Support
|
Run the bot: `node .`
|
||||||
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
|
|
||||||
|
|
||||||
## Roadmap
|
You can provide a room parameter: `node . -r room` if you'd like to have different configurations for different rooms.
|
||||||
If you have ideas for releases in the future, it is a good idea to list them in the README.
|
|
||||||
|
|
||||||
## Contributing
|
You may also use the scripts provided to allow the bot to restart if it is killed. Replace `node` with `./start.sh`, for example. However, the batch (Windows) script was not tested since it was last edited.
|
||||||
State if you are open to contributions and what your requirements are for accepting them.
|
|
||||||
|
|
||||||
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
|
It is recommended to have the bot at rank 3 (Admin) so it has full functionality.
|
||||||
|
|
||||||
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
|
When updating the bot, you must make sure to add any new configuration lines every time.
|
||||||
|
|
||||||
## Authors and acknowledgment
|
|
||||||
Show your appreciation to those who have contributed to the project.
|
|
||||||
|
|
||||||
## License
|
|
||||||
For open source projects, say how it is licensed.
|
|
||||||
|
|
||||||
## Project status
|
|
||||||
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
|
|
||||||
|
|
||||||
|
To create custom commands, see customcommands.example.js for writing them, and the bottom of the config.js file for more info on including them.
|
||||||
|
|
|
||||||
296
config.example.js
Normal file
296
config.example.js
Normal file
|
|
@ -0,0 +1,296 @@
|
||||||
|
var config = {
|
||||||
|
//api related stuff
|
||||||
|
api: {
|
||||||
|
//YouTube API v3 key
|
||||||
|
youtube_key: "",
|
||||||
|
//Wolfram API key
|
||||||
|
wolfram_key:""
|
||||||
|
},
|
||||||
|
//Connection and socket options...
|
||||||
|
connection: {
|
||||||
|
/* If true, will connect to a secure cytu.be server using an encrypted
|
||||||
|
* connection. If false, will use an unsecure server. You should ONLY ever
|
||||||
|
* set this to false if you cannot connect to a secure server.
|
||||||
|
*/
|
||||||
|
secureSocket: true,
|
||||||
|
//Hostname of the server to connect to. https://cytu.be/ becomes "cytu.be"
|
||||||
|
hostname: "cytu.be",
|
||||||
|
//If true, will only allow socket connections that are on the same domain as
|
||||||
|
// the hostname above. Only disable this if you need to and know what you're
|
||||||
|
// doing.
|
||||||
|
sameDomainSocketOnly: true
|
||||||
|
},
|
||||||
|
db: {
|
||||||
|
//If true, will use database stuff
|
||||||
|
use: false,
|
||||||
|
//Tables to use in the database. Comments with each table describe what they
|
||||||
|
// handle and what will be subsequently disabled if false
|
||||||
|
useTables: {
|
||||||
|
users: true, //user activity time, visits
|
||||||
|
//Disabling the "users" table will disable the child tables below
|
||||||
|
emote_data: true, //Emote counts
|
||||||
|
duel_stats: true, //Dueling stats, will also disable duels
|
||||||
|
chat: true, //Chat logging, quotes
|
||||||
|
bump_stats: true, //Bump counts for moderators
|
||||||
|
saved_polls: true, //Storage of poll info for reuse
|
||||||
|
video_play_data: true //Not implemented
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
Database connection info. Change these options as needed.
|
||||||
|
*/
|
||||||
|
connectionInfo: {
|
||||||
|
user: 'username',
|
||||||
|
host: 'localhost',
|
||||||
|
database: 'dbname',
|
||||||
|
password: 'password',
|
||||||
|
port: '5432',
|
||||||
|
client_encoding: 'utf8'
|
||||||
|
},
|
||||||
|
//If true, will allow statistic and quote collection of unregistered users
|
||||||
|
allowGuestData: false
|
||||||
|
},
|
||||||
|
//options relating to the Discord Bot feature
|
||||||
|
discord: {
|
||||||
|
//if true, will use the token below to log into a discord bot
|
||||||
|
use: false,
|
||||||
|
token: "",
|
||||||
|
//if true, will send Now Playing notifications to the given channel ID
|
||||||
|
sendNowPlayingMessages: true,
|
||||||
|
nowPlayingChannelID: "",
|
||||||
|
//if true, will send poll open/close notifications to the given channel ID
|
||||||
|
sendPollResultMessages: true,
|
||||||
|
pollResultChannelID: "",
|
||||||
|
//url to the icon to use for rich embed messages (20x20 is best)
|
||||||
|
iconUrl: "",
|
||||||
|
//hex colors for rich embed messages
|
||||||
|
pollClosedColor: "#FF2222", //color of Poll Closed messages
|
||||||
|
pollInProgressColor: "#22AAFF", //color of Poll In Progress messages
|
||||||
|
pollOpenedColor: "#22FF22" //color of Poll Opened messages
|
||||||
|
},
|
||||||
|
//Interface options...
|
||||||
|
interface: {
|
||||||
|
//If true, usernames will be colored in the terminal.
|
||||||
|
// Disabling this will cut down a few operations, but there will be
|
||||||
|
// a negligible performance boost if any at all.
|
||||||
|
colorUsernames: true,
|
||||||
|
//Same thing but for titles of media items.
|
||||||
|
colorMediaTitles: true,
|
||||||
|
//Rank colors. If colorUsernames is true, usernames will be colored
|
||||||
|
// by rank. Uses color names from the cli-color package.
|
||||||
|
rankColors: {
|
||||||
|
anonymous: "blackBright",
|
||||||
|
unregistered: "white",
|
||||||
|
server: "cyanBright",
|
||||||
|
1: "whiteBright",
|
||||||
|
1.5: "yellowBright",
|
||||||
|
2: "blueBright",
|
||||||
|
3: "greenBright",
|
||||||
|
4: "redBright",
|
||||||
|
5: "red",
|
||||||
|
255: "magentaBright"
|
||||||
|
},
|
||||||
|
/* If true, chat messages may be sent from the CLI Interface
|
||||||
|
* without using the /say command. Otherwise, if false,
|
||||||
|
* the /say command must be used to send a chat message.
|
||||||
|
* False is recommended to prevent accidental chat messages.
|
||||||
|
*/
|
||||||
|
allowQuickCLIChat: false,
|
||||||
|
//If true, debug logs will be shown and logged into debug.log.
|
||||||
|
logDebug: true,
|
||||||
|
//If true, much more info will be logged. Not necessary, but can be helpful.
|
||||||
|
// Verbose logs contain a magentaBright asterisk after the timestamp.
|
||||||
|
logVerbose: true,
|
||||||
|
//If true, non-generic logs will still be put into the regular log file.
|
||||||
|
// Can be helpful for having full log context in one file, but will result
|
||||||
|
// in a larger bot.log
|
||||||
|
logConsolidation: true,
|
||||||
|
//If true, excludes errors from the main log if consolidation is enabled.
|
||||||
|
// Highly recommended as errors might include sensitive information.
|
||||||
|
excludeErrorsFromLog: true,
|
||||||
|
//If true, logged times will use a 24-hour format. If false, will
|
||||||
|
// use 12-hour time along with am/pm. Doesn't affect anything else.
|
||||||
|
useTwentyFourHourTime: true,
|
||||||
|
//If true, title of the terminal/prompt will be populated with various
|
||||||
|
// bits of info. Turn this off if you will never see it.
|
||||||
|
fancyTitle: true,
|
||||||
|
//If true, muted users will be clearly indicated in chat message logs.
|
||||||
|
indicateMutedUsers: true
|
||||||
|
},
|
||||||
|
//Login options...
|
||||||
|
//Hidden from Bot.cfg object
|
||||||
|
login: {
|
||||||
|
//If true, bot will not log into an account.
|
||||||
|
guest: false,
|
||||||
|
//Room to connect to.
|
||||||
|
room: "",
|
||||||
|
//Room password, if required.
|
||||||
|
roomPassword: "",
|
||||||
|
//Username and password. Not needed if logging in as a guest.
|
||||||
|
username: "",
|
||||||
|
password: ""
|
||||||
|
},
|
||||||
|
//Use this to enumerate ranks for your room.
|
||||||
|
// Be absolutely sure these ranks are correct.
|
||||||
|
// If changed, make sure you refactor any occurrences in the entire bot.
|
||||||
|
// Found in Bot.RANKS, not Bot.cfg
|
||||||
|
RANKS: {
|
||||||
|
GUEST: 0,
|
||||||
|
USER: 1,
|
||||||
|
LEADER: 1.5,
|
||||||
|
MOD: 2,
|
||||||
|
ADMIN: 3,
|
||||||
|
OWNER: 4,
|
||||||
|
FOUNDER: 5,
|
||||||
|
SITEOWNER: 10,
|
||||||
|
SUPERADMIN: 255
|
||||||
|
},
|
||||||
|
//Define proper names for ranks. Used when a user doesn't have permission
|
||||||
|
//for a command, for example.
|
||||||
|
rankNames: {
|
||||||
|
unregistered: "Guest",
|
||||||
|
1: "User",
|
||||||
|
1.5: "Leader",
|
||||||
|
2: "Moderator",
|
||||||
|
3: "Room Admin",
|
||||||
|
4: "Room Owner",
|
||||||
|
5: "Room Founder",
|
||||||
|
10: "Site Owner",
|
||||||
|
255: "Superadmin"
|
||||||
|
},
|
||||||
|
chat: {
|
||||||
|
//If true, emotes will be colored. Must be true for emote data to be recorded
|
||||||
|
// in the database.
|
||||||
|
parseEmotes: true,
|
||||||
|
//Max emotes per message. This is essentially infinite in vanilla rooms, you'd
|
||||||
|
// have to edit execEmotes in your room script to limit emotes. Set below 0
|
||||||
|
// for infinite emotes (-1, for example). Set to 0 if emotes are disabled.
|
||||||
|
maxEmotes: -1,
|
||||||
|
//Allow using commands in the middle of a message with two colons, example: ::!cmdname
|
||||||
|
allowInlineCmd: true,
|
||||||
|
//The trigger character will be used to distinguish chat commands.
|
||||||
|
// Must be one character. Valid chars: !#$%^&*()_+-=`~.,?
|
||||||
|
trigger: "!",
|
||||||
|
//If true, sends chat messages using your rank color.
|
||||||
|
useFlair: true,
|
||||||
|
//Minimum rank to bypass both user and global cooldowns on commands.
|
||||||
|
// Set to -1 to enforce cooldowns for all ranks.
|
||||||
|
minRankToBypassCooldown: 3,
|
||||||
|
//If true, disables any chat commands.
|
||||||
|
disableAllCommands: true,
|
||||||
|
//Put chatfilters here if needed.
|
||||||
|
filters: {
|
||||||
|
//Code for img chat command.
|
||||||
|
img: "",
|
||||||
|
//Code for comment chat command. (excluding brackets: [code][/code] becomes just "code")
|
||||||
|
commentAuthor: "",
|
||||||
|
//Code for pokeroll filter
|
||||||
|
pokeroll: "",
|
||||||
|
//Tags for spoiler filter. Opener is the left tag, closer is the right tag.
|
||||||
|
//Example: [spoiler] is the opener, [/spoiler] is the closer
|
||||||
|
spoilerTagOpener: "",
|
||||||
|
spoilerTagCloser: ""
|
||||||
|
},
|
||||||
|
//If true, will use ssc:#rrggbb in some chat messages which requires
|
||||||
|
// Xaekai's external chat color script in the room. Search the bot's code
|
||||||
|
// for examples where this variable is used, as it doesn't automatically
|
||||||
|
// remove any instances of ssc itself.
|
||||||
|
roomHasSSC: false,
|
||||||
|
//If true, will announce winning option of polls when they close. Requires
|
||||||
|
// two or more options in the poll, and will only occur if the poll
|
||||||
|
// ends after exactly 3 minutes.
|
||||||
|
announcePollResults: true,
|
||||||
|
//If true, muted users will not be able to use commands and their emotes will not
|
||||||
|
// be checked. However, this may make it easier for users to determine if they
|
||||||
|
// are muted or not.
|
||||||
|
ignoreMutedUsers: true,
|
||||||
|
//Minimum length a chat message must be before putting it in the database
|
||||||
|
minimumQuoteLength: 16
|
||||||
|
},
|
||||||
|
//Media and playlist options...
|
||||||
|
media: {
|
||||||
|
//Maximum position to bump videos to. This should be according to a 1-based index,
|
||||||
|
// or how you'd see the playlist on the site.
|
||||||
|
bumpCap: 5,
|
||||||
|
//Amount of milliseconds between bumps of individual users.
|
||||||
|
bumpCooldown: 300000
|
||||||
|
},
|
||||||
|
misc: {
|
||||||
|
//Controls the bot's AFK state automatically.
|
||||||
|
// 0 - Disabled (default)
|
||||||
|
// 1 - Tries to stay AFK at all times
|
||||||
|
// 2 - Tries to stay active (not AFK) at all times
|
||||||
|
autoAFK: 0,
|
||||||
|
//Choose a language to use. Currently does nothing, planning on
|
||||||
|
// implementing this in the future (also a command to change it during runtime)
|
||||||
|
lang: "en",
|
||||||
|
//List of bot usernames that may be in your room. Bots will not receive mod-only
|
||||||
|
// PM broadcasts and will not be able to execute commands. Their messages cannot
|
||||||
|
// be stored and/or quoted. Keep the names lowercase. This bot's name is not needed.
|
||||||
|
// Example: ["botname1", "botname2"]
|
||||||
|
bots: [],
|
||||||
|
//List of blacklisted avatar hostnames. If a user's profile picture's hostname
|
||||||
|
// matches one of these, they'll be notified if moderation.notifyBlacklistedAvatar
|
||||||
|
// is true.
|
||||||
|
// Example: ["maliciousdomain.gov", "wackydomainname.io"]
|
||||||
|
blacklistedAvatarHosts: [],
|
||||||
|
//Threshold in seconds for the total playlist time to be considered low. Each time
|
||||||
|
// the playlist time falls below this, the bot will send a chat message warning of
|
||||||
|
// a low playlist. Must also have moderation.notifyLowPlaylistTime set to true.
|
||||||
|
lowPlaylistTime: 3600,
|
||||||
|
//Minimum amount of time in milliseconds between queued actions.
|
||||||
|
// Helps to prevent flooding the server. Should stay above maybe 200ms.
|
||||||
|
queueInterval: 200, //Generic socket event.
|
||||||
|
broadcastPMQueueInterval: 250, //For broadcasts of private messages.
|
||||||
|
largeDataQueueInterval: 2000 //For large data requests, like ban list or channel log.
|
||||||
|
},
|
||||||
|
moderation: {
|
||||||
|
//If true, will automatically disallow users if any of their aliases are disallowed.
|
||||||
|
autoDisallow: true,
|
||||||
|
//If true, will broadcast a PM to online mods if a new user joins the room.
|
||||||
|
// Requires the user table of the database to be active.
|
||||||
|
notifyNewUser: true,
|
||||||
|
//If true, will broadcast a PM to online mods if a joining user's subnet
|
||||||
|
// matches that of any banned users.
|
||||||
|
// Requires rank >=3, as the banlist requirement is hardcoded that way.
|
||||||
|
notifyBannedSubnets: true,
|
||||||
|
//If true, will automatically shadowmute users whose subnets match those of
|
||||||
|
// banned users.
|
||||||
|
autoShadowmuteOnSubnetMatch: false,
|
||||||
|
//If true, will PM users if their avatar hostname matches one in misc.blacklistedAvatarHosts
|
||||||
|
notifyBlacklistedAvatar: false,
|
||||||
|
//If true, will send a low playlist time warning message in chat whenever the
|
||||||
|
// total playlist time falls below misc.lowPlaylistTime
|
||||||
|
notifyLowPlaylistTime: true,
|
||||||
|
//If true, will send a chat message whenever the skip rate is changed
|
||||||
|
notifySkipRateChange: true
|
||||||
|
},
|
||||||
|
advanced: {
|
||||||
|
//If true, will automatically grab the Channel Log and save it after certain channel events.
|
||||||
|
// Requires Rank 3+
|
||||||
|
automaticChannelLog: false,
|
||||||
|
//If true, index.js will look for the scripts in .\lib\[channel name]\
|
||||||
|
// Probably useful for running multiple bot instances, but not very practical
|
||||||
|
scriptsInChannelFolder: false,
|
||||||
|
//If true, loads customchatcommands-roomname.js instead of
|
||||||
|
// customchatcommands.js
|
||||||
|
useChannelCustomCommands: true,
|
||||||
|
//If true, uses settings-roomname.json instead of settings.json
|
||||||
|
useChannelSettingsFile: true,
|
||||||
|
//List of command files to load. If useChannelCustomCommands is true, you
|
||||||
|
// do not need to include the channel name, but it won't break anything if
|
||||||
|
// you do. Try not to load too many command files as it might increase
|
||||||
|
// the bot's startup time.
|
||||||
|
// Command files must be named customchatcommands-NAME.js
|
||||||
|
// For example, if this is set to ["test", "external", "fun"], the bot will
|
||||||
|
// look for:
|
||||||
|
// customchatcommands-test.js
|
||||||
|
// customchatcommands-external.js
|
||||||
|
// customchatcommands-fun.js
|
||||||
|
// Be aware that this also respects the scriptsInChannelFolder option.
|
||||||
|
// Duplicate commands will be overwritten, with the last one loaded taking
|
||||||
|
// effect.
|
||||||
|
customCommandsToLoad: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
296
config.js
Normal file
296
config.js
Normal file
|
|
@ -0,0 +1,296 @@
|
||||||
|
var config = {
|
||||||
|
//api related stuff
|
||||||
|
api: {
|
||||||
|
//YouTube API v3 key
|
||||||
|
youtube_key: "",
|
||||||
|
//Wolfram API key
|
||||||
|
wolfram_key:""
|
||||||
|
},
|
||||||
|
//Connection and socket options...
|
||||||
|
connection: {
|
||||||
|
/* If true, will connect to a secure cytu.be server using an encrypted
|
||||||
|
* connection. If false, will use an unsecure server. You should ONLY ever
|
||||||
|
* set this to false if you cannot connect to a secure server.
|
||||||
|
*/
|
||||||
|
secureSocket: true,
|
||||||
|
//Hostname of the server to connect to. https://cytu.be/ becomes "cytu.be"
|
||||||
|
hostname: "ourfore.st",
|
||||||
|
//If true, will only allow socket connections that are on the same domain as
|
||||||
|
// the hostname above. Only disable this if you need to and know what you're
|
||||||
|
// doing.
|
||||||
|
sameDomainSocketOnly: true
|
||||||
|
},
|
||||||
|
db: {
|
||||||
|
//If true, will use database stuff
|
||||||
|
use: false,
|
||||||
|
//Tables to use in the database. Comments with each table describe what they
|
||||||
|
// handle and what will be subsequently disabled if false
|
||||||
|
useTables: {
|
||||||
|
users: true, //user activity time, visits
|
||||||
|
//Disabling the "users" table will disable the child tables below
|
||||||
|
emote_data: true, //Emote counts
|
||||||
|
duel_stats: true, //Dueling stats, will also disable duels
|
||||||
|
chat: true, //Chat logging, quotes
|
||||||
|
bump_stats: true, //Bump counts for moderators
|
||||||
|
saved_polls: true, //Storage of poll info for reuse
|
||||||
|
video_play_data: true //Not implemented
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
Database connection info. Change these options as needed.
|
||||||
|
*/
|
||||||
|
connectionInfo: {
|
||||||
|
user: 'username',
|
||||||
|
host: 'localhost',
|
||||||
|
database: 'dbname',
|
||||||
|
password: 'password',
|
||||||
|
port: '5432',
|
||||||
|
client_encoding: 'utf8'
|
||||||
|
},
|
||||||
|
//If true, will allow statistic and quote collection of unregistered users
|
||||||
|
allowGuestData: false
|
||||||
|
},
|
||||||
|
//options relating to the Discord Bot feature
|
||||||
|
discord: {
|
||||||
|
//if true, will use the token below to log into a discord bot
|
||||||
|
use: false,
|
||||||
|
token: "",
|
||||||
|
//if true, will send Now Playing notifications to the given channel ID
|
||||||
|
sendNowPlayingMessages: true,
|
||||||
|
nowPlayingChannelID: "",
|
||||||
|
//if true, will send poll open/close notifications to the given channel ID
|
||||||
|
sendPollResultMessages: true,
|
||||||
|
pollResultChannelID: "",
|
||||||
|
//url to the icon to use for rich embed messages (20x20 is best)
|
||||||
|
iconUrl: "",
|
||||||
|
//hex colors for rich embed messages
|
||||||
|
pollClosedColor: "#FF2222", //color of Poll Closed messages
|
||||||
|
pollInProgressColor: "#22AAFF", //color of Poll In Progress messages
|
||||||
|
pollOpenedColor: "#22FF22" //color of Poll Opened messages
|
||||||
|
},
|
||||||
|
//Interface options...
|
||||||
|
interface: {
|
||||||
|
//If true, usernames will be colored in the terminal.
|
||||||
|
// Disabling this will cut down a few operations, but there will be
|
||||||
|
// a negligible performance boost if any at all.
|
||||||
|
colorUsernames: true,
|
||||||
|
//Same thing but for titles of media items.
|
||||||
|
colorMediaTitles: true,
|
||||||
|
//Rank colors. If colorUsernames is true, usernames will be colored
|
||||||
|
// by rank. Uses color names from the cli-color package.
|
||||||
|
rankColors: {
|
||||||
|
anonymous: "blackBright",
|
||||||
|
unregistered: "white",
|
||||||
|
server: "cyanBright",
|
||||||
|
1: "whiteBright",
|
||||||
|
1.5: "yellowBright",
|
||||||
|
2: "blueBright",
|
||||||
|
3: "greenBright",
|
||||||
|
4: "redBright",
|
||||||
|
5: "red",
|
||||||
|
255: "magentaBright"
|
||||||
|
},
|
||||||
|
/* If true, chat messages may be sent from the CLI Interface
|
||||||
|
* without using the /say command. Otherwise, if false,
|
||||||
|
* the /say command must be used to send a chat message.
|
||||||
|
* False is recommended to prevent accidental chat messages.
|
||||||
|
*/
|
||||||
|
allowQuickCLIChat: false,
|
||||||
|
//If true, debug logs will be shown and logged into debug.log.
|
||||||
|
logDebug: true,
|
||||||
|
//If true, much more info will be logged. Not necessary, but can be helpful.
|
||||||
|
// Verbose logs contain a magentaBright asterisk after the timestamp.
|
||||||
|
logVerbose: true,
|
||||||
|
//If true, non-generic logs will still be put into the regular log file.
|
||||||
|
// Can be helpful for having full log context in one file, but will result
|
||||||
|
// in a larger bot.log
|
||||||
|
logConsolidation: true,
|
||||||
|
//If true, excludes errors from the main log if consolidation is enabled.
|
||||||
|
// Highly recommended as errors might include sensitive information.
|
||||||
|
excludeErrorsFromLog: true,
|
||||||
|
//If true, logged times will use a 24-hour format. If false, will
|
||||||
|
// use 12-hour time along with am/pm. Doesn't affect anything else.
|
||||||
|
useTwentyFourHourTime: true,
|
||||||
|
//If true, title of the terminal/prompt will be populated with various
|
||||||
|
// bits of info. Turn this off if you will never see it.
|
||||||
|
fancyTitle: true,
|
||||||
|
//If true, muted users will be clearly indicated in chat message logs.
|
||||||
|
indicateMutedUsers: true
|
||||||
|
},
|
||||||
|
//Login options...
|
||||||
|
//Hidden from Bot.cfg object
|
||||||
|
login: {
|
||||||
|
//If true, bot will not log into an account.
|
||||||
|
guest: false,
|
||||||
|
//Room to connect to.
|
||||||
|
room: "afterparty",
|
||||||
|
//Room password, if required.
|
||||||
|
roomPassword: "",
|
||||||
|
//Username and password. Not needed if logging in as a guest.
|
||||||
|
username: "tokebot",
|
||||||
|
password: "$M0k3&Cr04k"
|
||||||
|
},
|
||||||
|
//Use this to enumerate ranks for your room.
|
||||||
|
// Be absolutely sure these ranks are correct.
|
||||||
|
// If changed, make sure you refactor any occurrences in the entire bot.
|
||||||
|
// Found in Bot.RANKS, not Bot.cfg
|
||||||
|
RANKS: {
|
||||||
|
GUEST: 0,
|
||||||
|
USER: 1,
|
||||||
|
LEADER: 1.5,
|
||||||
|
MOD: 2,
|
||||||
|
ADMIN: 3,
|
||||||
|
OWNER: 4,
|
||||||
|
FOUNDER: 5,
|
||||||
|
SITEOWNER: 10,
|
||||||
|
SUPERADMIN: 255
|
||||||
|
},
|
||||||
|
//Define proper names for ranks. Used when a user doesn't have permission
|
||||||
|
//for a command, for example.
|
||||||
|
rankNames: {
|
||||||
|
unregistered: "Guest",
|
||||||
|
1: "User",
|
||||||
|
1.5: "Leader",
|
||||||
|
2: "Moderator",
|
||||||
|
3: "Room Admin",
|
||||||
|
4: "Room Owner",
|
||||||
|
5: "Room Founder",
|
||||||
|
10: "Site Owner",
|
||||||
|
255: "Superadmin"
|
||||||
|
},
|
||||||
|
chat: {
|
||||||
|
//If true, emotes will be colored. Must be true for emote data to be recorded
|
||||||
|
// in the database.
|
||||||
|
parseEmotes: true,
|
||||||
|
//Max emotes per message. This is essentially infinite in vanilla rooms, you'd
|
||||||
|
// have to edit execEmotes in your room script to limit emotes. Set below 0
|
||||||
|
// for infinite emotes (-1, for example). Set to 0 if emotes are disabled.
|
||||||
|
maxEmotes: -1,
|
||||||
|
//Allow using commands in the middle of a message with two colons, example: ::!cmdname
|
||||||
|
allowInlineCmd: true,
|
||||||
|
//The trigger character will be used to distinguish chat commands.
|
||||||
|
// Must be one character. Valid chars: !#$%^&*()_+-=`~.,?
|
||||||
|
trigger: "!",
|
||||||
|
//If true, sends chat messages using your rank color.
|
||||||
|
useFlair: true,
|
||||||
|
//Minimum rank to bypass both user and global cooldowns on commands.
|
||||||
|
// Set to -1 to enforce cooldowns for all ranks.
|
||||||
|
minRankToBypassCooldown: 3,
|
||||||
|
//If true, disables any chat commands.
|
||||||
|
disableAllCommands: false,
|
||||||
|
//Put chatfilters here if needed.
|
||||||
|
filters: {
|
||||||
|
//Code for img chat command.
|
||||||
|
img: "",
|
||||||
|
//Code for comment chat command. (excluding brackets: [code][/code] becomes just "code")
|
||||||
|
commentAuthor: "",
|
||||||
|
//Code for pokeroll filter
|
||||||
|
pokeroll: "",
|
||||||
|
//Tags for spoiler filter. Opener is the left tag, closer is the right tag.
|
||||||
|
//Example: [spoiler] is the opener, [/spoiler] is the closer
|
||||||
|
spoilerTagOpener: "",
|
||||||
|
spoilerTagCloser: ""
|
||||||
|
},
|
||||||
|
//If true, will use ssc:#rrggbb in some chat messages which requires
|
||||||
|
// Xaekai's external chat color script in the room. Search the bot's code
|
||||||
|
// for examples where this variable is used, as it doesn't automatically
|
||||||
|
// remove any instances of ssc itself.
|
||||||
|
roomHasSSC: false,
|
||||||
|
//If true, will announce winning option of polls when they close. Requires
|
||||||
|
// two or more options in the poll, and will only occur if the poll
|
||||||
|
// ends after exactly 3 minutes.
|
||||||
|
announcePollResults: true,
|
||||||
|
//If true, muted users will not be able to use commands and their emotes will not
|
||||||
|
// be checked. However, this may make it easier for users to determine if they
|
||||||
|
// are muted or not.
|
||||||
|
ignoreMutedUsers: true,
|
||||||
|
//Minimum length a chat message must be before putting it in the database
|
||||||
|
minimumQuoteLength: 16
|
||||||
|
},
|
||||||
|
//Media and playlist options...
|
||||||
|
media: {
|
||||||
|
//Maximum position to bump videos to. This should be according to a 1-based index,
|
||||||
|
// or how you'd see the playlist on the site.
|
||||||
|
bumpCap: 5,
|
||||||
|
//Amount of milliseconds between bumps of individual users.
|
||||||
|
bumpCooldown: 300000
|
||||||
|
},
|
||||||
|
misc: {
|
||||||
|
//Controls the bot's AFK state automatically.
|
||||||
|
// 0 - Disabled (default)
|
||||||
|
// 1 - Tries to stay AFK at all times
|
||||||
|
// 2 - Tries to stay active (not AFK) at all times
|
||||||
|
autoAFK: 0,
|
||||||
|
//Choose a language to use. Currently does nothing, planning on
|
||||||
|
// implementing this in the future (also a command to change it during runtime)
|
||||||
|
lang: "en",
|
||||||
|
//List of bot usernames that may be in your room. Bots will not receive mod-only
|
||||||
|
// PM broadcasts and will not be able to execute commands. Their messages cannot
|
||||||
|
// be stored and/or quoted. Keep the names lowercase. This bot's name is not needed.
|
||||||
|
// Example: ["botname1", "botname2"]
|
||||||
|
bots: [],
|
||||||
|
//List of blacklisted avatar hostnames. If a user's profile picture's hostname
|
||||||
|
// matches one of these, they'll be notified if moderation.notifyBlacklistedAvatar
|
||||||
|
// is true.
|
||||||
|
// Example: ["maliciousdomain.gov", "wackydomainname.io"]
|
||||||
|
blacklistedAvatarHosts: [],
|
||||||
|
//Threshold in seconds for the total playlist time to be considered low. Each time
|
||||||
|
// the playlist time falls below this, the bot will send a chat message warning of
|
||||||
|
// a low playlist. Must also have moderation.notifyLowPlaylistTime set to true.
|
||||||
|
lowPlaylistTime: 3600,
|
||||||
|
//Minimum amount of time in milliseconds between queued actions.
|
||||||
|
// Helps to prevent flooding the server. Should stay above maybe 200ms.
|
||||||
|
queueInterval: 200, //Generic socket event.
|
||||||
|
broadcastPMQueueInterval: 250, //For broadcasts of private messages.
|
||||||
|
largeDataQueueInterval: 2000 //For large data requests, like ban list or channel log.
|
||||||
|
},
|
||||||
|
moderation: {
|
||||||
|
//If true, will automatically disallow users if any of their aliases are disallowed.
|
||||||
|
autoDisallow: true,
|
||||||
|
//If true, will broadcast a PM to online mods if a new user joins the room.
|
||||||
|
// Requires the user table of the database to be active.
|
||||||
|
notifyNewUser: true,
|
||||||
|
//If true, will broadcast a PM to online mods if a joining user's subnet
|
||||||
|
// matches that of any banned users.
|
||||||
|
// Requires rank >=3, as the banlist requirement is hardcoded that way.
|
||||||
|
notifyBannedSubnets: true,
|
||||||
|
//If true, will automatically shadowmute users whose subnets match those of
|
||||||
|
// banned users.
|
||||||
|
autoShadowmuteOnSubnetMatch: false,
|
||||||
|
//If true, will PM users if their avatar hostname matches one in misc.blacklistedAvatarHosts
|
||||||
|
notifyBlacklistedAvatar: false,
|
||||||
|
//If true, will send a low playlist time warning message in chat whenever the
|
||||||
|
// total playlist time falls below misc.lowPlaylistTime
|
||||||
|
notifyLowPlaylistTime: true,
|
||||||
|
//If true, will send a chat message whenever the skip rate is changed
|
||||||
|
notifySkipRateChange: true
|
||||||
|
},
|
||||||
|
advanced: {
|
||||||
|
//If true, will automatically grab the Channel Log and save it after certain channel events.
|
||||||
|
// Requires Rank 3+
|
||||||
|
automaticChannelLog: false,
|
||||||
|
//If true, index.js will look for the scripts in .\lib\[channel name]\
|
||||||
|
// Probably useful for running multiple bot instances, but not very practical
|
||||||
|
scriptsInChannelFolder: false,
|
||||||
|
//If true, loads customchatcommands-roomname.js instead of
|
||||||
|
// customchatcommands.js
|
||||||
|
useChannelCustomCommands: false,
|
||||||
|
//If true, uses settings-roomname.json instead of settings.json
|
||||||
|
useChannelSettingsFile: true,
|
||||||
|
//List of command files to load. If useChannelCustomCommands is true, you
|
||||||
|
// do not need to include the channel name, but it won't break anything if
|
||||||
|
// you do. Try not to load too many command files as it might increase
|
||||||
|
// the bot's startup time.
|
||||||
|
// Command files must be named customchatcommands-NAME.js
|
||||||
|
// For example, if this is set to ["test", "external", "fun"], the bot will
|
||||||
|
// look for:
|
||||||
|
// customchatcommands-test.js
|
||||||
|
// customchatcommands-external.js
|
||||||
|
// customchatcommands-fun.js
|
||||||
|
// Be aware that this also respects the scriptsInChannelFolder option.
|
||||||
|
// Duplicate commands will be overwritten, with the last one loaded taking
|
||||||
|
// effect.
|
||||||
|
customCommandsToLoad: ["toke"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
3
errors/.gitignore
vendored
Normal file
3
errors/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
*
|
||||||
|
|
||||||
|
!.gitignore
|
||||||
87
index.js
Normal file
87
index.js
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const path = require("path");
|
||||||
|
const readline = require("readline");
|
||||||
|
const fs = require("fs");
|
||||||
|
const procArgs = process.argv.slice(2);
|
||||||
|
const roomExp = new RegExp(/^[\w-]{1,30}$/);
|
||||||
|
|
||||||
|
let cfgname = "config.js";
|
||||||
|
let foundArgs = {};
|
||||||
|
let argAliases = {
|
||||||
|
"--roomcfg": "-r"
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < procArgs.length; i++) {
|
||||||
|
let ARG = procArgs[i];
|
||||||
|
if (argAliases.hasOwnProperty(ARG))
|
||||||
|
ARG = argAliases[ARG];
|
||||||
|
if (!foundArgs[ARG]) {
|
||||||
|
foundArgs[ARG] = true;
|
||||||
|
switch (ARG) {
|
||||||
|
case "-r":
|
||||||
|
if (i+1 < procArgs.length && roomExp.test(procArgs[i+1])) {
|
||||||
|
i++;
|
||||||
|
cfgname = "config-" + procArgs[i] + ".js";
|
||||||
|
} else {
|
||||||
|
console.error("roomcfg: Invalid argument");
|
||||||
|
return process.exit(1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(procArgs[i] + " used more than once");
|
||||||
|
return process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout
|
||||||
|
});
|
||||||
|
|
||||||
|
let config = null;
|
||||||
|
try {
|
||||||
|
config = require(path.join(__dirname, cfgname));
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code === "MODULE_NOT_FOUND") {
|
||||||
|
let errText = cfgname + " not found!";
|
||||||
|
console.error(errText);
|
||||||
|
let date = new Date();
|
||||||
|
fs.writeFileSync(path.join(__dirname, "errors", "error_" + date.getTime() + ".txt"), date.toGMTString() + "\n\n" + e.stack + "\n\n" + errText);
|
||||||
|
setTimeout(function() {
|
||||||
|
process.exit(1);
|
||||||
|
}, 5000);
|
||||||
|
} else
|
||||||
|
onErr(e);
|
||||||
|
}
|
||||||
|
const ROOM = config.login.room;
|
||||||
|
if (!(roomExp.test(ROOM))) {
|
||||||
|
console.error("Invalid channel name! Channel names must consist of 1-30 chars and only A-Z, a-z, 0-9, _ and -");
|
||||||
|
return process.exit(1);
|
||||||
|
}
|
||||||
|
const scriptPath = config.advanced.scriptsInChannelFolder ? ROOM : "";
|
||||||
|
const logPath = path.join(__dirname, "logs", ROOM);
|
||||||
|
const errorPath = path.join(__dirname, "errors", ROOM);
|
||||||
|
if (!fs.existsSync(logPath)) {
|
||||||
|
fs.mkdirSync(logPath);
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(errorPath)) {
|
||||||
|
fs.mkdirSync(errorPath);
|
||||||
|
}
|
||||||
|
const bot = require(path.join(__dirname, "lib", scriptPath, "bot.js")).init(config, rl, __dirname);
|
||||||
|
|
||||||
|
//catches uncaught exceptions
|
||||||
|
process.on('uncaughtException', onErr);
|
||||||
|
|
||||||
|
function onErr(err) {
|
||||||
|
var date = new Date();
|
||||||
|
console.error(err.stack);
|
||||||
|
fs.writeFileSync(path.join(__dirname, "errors", "error_" + date.getTime() + ".txt"), date.toGMTString() + "\n\n" + err.stack);
|
||||||
|
if (bot) bot.kill("Uncaught Exception, see error logs", 1000, 1);
|
||||||
|
setTimeout(function() {
|
||||||
|
process.exit(1);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
4
lib/.gitignore
vendored
Normal file
4
lib/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
*/
|
||||||
|
*-hidden*
|
||||||
|
|
||||||
|
!.gitignore
|
||||||
251
lib/api.js
Normal file
251
lib/api.js
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
const fetch = require("node-fetch");
|
||||||
|
|
||||||
|
const strings = require("./strings.js");
|
||||||
|
const utils = require("./utils.js");
|
||||||
|
|
||||||
|
/*
|
||||||
|
Each API item must be a function with the follow parameters:
|
||||||
|
bot: Bot object
|
||||||
|
msg: A message from a chat command or something similar. Nullable.
|
||||||
|
apiKey: A string containing an API key if needed. Nullable.
|
||||||
|
callback: A function that does something with the status and data returned,
|
||||||
|
see below. When it comes to chat commands, the command provides the callback
|
||||||
|
here.
|
||||||
|
|
||||||
|
Once the request is done, the callback given to sendRequest is called if present,
|
||||||
|
and it should have the following parameters:
|
||||||
|
status: Status code of the request. Some of these API methods here change
|
||||||
|
the actual status code if it's not OK (yeah it's strange but some
|
||||||
|
commands use this).
|
||||||
|
data: The data. If json is true, this will be a proper object.
|
||||||
|
ok: A boolean determining if the status was OK.
|
||||||
|
|
||||||
|
Inside the callback in sendRequest, the callback for the API method is then called.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
var APIs = {
|
||||||
|
|
||||||
|
"anagram": function (bot, msg, apiKey, callback) {
|
||||||
|
var url = "https://anagramgenius.com/server.php?source_text=" + encodeURIComponent(msg) + "&vulgar=1";
|
||||||
|
var json = false;
|
||||||
|
var options = {
|
||||||
|
method: "GET",
|
||||||
|
timeout: 1000
|
||||||
|
}
|
||||||
|
sendRequest(bot, url, json, options, "anagram", function (status, data, ok) {
|
||||||
|
var _data = data.match(/anagrams to\<br\>\<span class=\"black\-18\">\'(.+?)\'\<\/span\>/);
|
||||||
|
if (!(_data && _data[1])) _data = null;
|
||||||
|
callback(status, _data, ok);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
//Make sure you change the schedule ID for each new event. This will be outdated
|
||||||
|
"gdq": function(bot, msg, apiKey, callback) {
|
||||||
|
var url = "https://horaro.org/-/api/v1/schedules/3011nzb4yh71id7a63";
|
||||||
|
var json = true;
|
||||||
|
var options = {
|
||||||
|
method: "GET",
|
||||||
|
timeout: 1000
|
||||||
|
};
|
||||||
|
sendRequest(bot, url, json, options, "gdq", function(status, data, ok) {
|
||||||
|
callback(status, data, ok);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
"saltybet": function (bot, msg, apiKey, callback) {
|
||||||
|
var url = "https://saltybet.com/state.json";
|
||||||
|
var json = true;
|
||||||
|
var options = {
|
||||||
|
method: "GET",
|
||||||
|
timeout: 1000
|
||||||
|
}
|
||||||
|
sendRequest(bot, url, json, options, "saltybet", function(status, data, ok) {
|
||||||
|
callback(status, data, ok);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
//(not really a public API but valid endpoint)
|
||||||
|
"urbandictionary": function (bot, msg, apiKey, callback) {
|
||||||
|
var url = "https://api.urbandictionary.com/v0/define?term=" + encodeURIComponent(msg);
|
||||||
|
var json = true;
|
||||||
|
var options = {
|
||||||
|
method: "GET",
|
||||||
|
timeout: 2000
|
||||||
|
}
|
||||||
|
sendRequest(bot, url, json, options, "urbandictionary", function (status, data, ok) {
|
||||||
|
if (!ok) data = null;
|
||||||
|
callback(status, data, ok);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
//YouTube v3: Comments
|
||||||
|
"youtubecomments": function (bot, videoId, apiKey, callback) {
|
||||||
|
if (apiKey.trim() === "") {
|
||||||
|
callback(-1, null, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var url = "https://www.googleapis.com:443" +
|
||||||
|
"/youtube/v3/commentThreads?" +
|
||||||
|
"part=snippet&videoId=" + videoId + "&key=" + apiKey + "&maxResults=100&order=relevance";
|
||||||
|
var json = true;
|
||||||
|
var options = {
|
||||||
|
method: "GET",
|
||||||
|
timeout: 1500
|
||||||
|
}
|
||||||
|
sendRequest(bot, url, json, options, "youtubecomments", function (status, data, ok) {
|
||||||
|
if (status !== 200) {
|
||||||
|
if (status === 403) callback(status, data, ok);
|
||||||
|
else callback(status, null, ok)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback(true, data, ok);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
//YouTube v3: PlaylistItems
|
||||||
|
//Returns a list of videos from a playlist
|
||||||
|
//Untested
|
||||||
|
/*
|
||||||
|
"youtubeplaylist": function (bot, data, apiKey, callback) {
|
||||||
|
let playlistId = data.playlistId,
|
||||||
|
maxResults = data.maxResults;
|
||||||
|
if (apiKey.trim() === "" || !playlistId || !maxResults) {
|
||||||
|
callback(-1, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var url = "https://www.googleapis.com:443" +
|
||||||
|
"/youtube/v3/playlistItems?" +
|
||||||
|
"part=contentDetails&id=" + playlistId + "&key=" + apiKey + "&maxResults=" + maxResults;
|
||||||
|
var json = true;
|
||||||
|
var options = {
|
||||||
|
method: "GET",
|
||||||
|
timeout: 1500
|
||||||
|
}
|
||||||
|
sendRequest(bot, url, json, options, "youtubeplaylist", function(status, data, ok) {
|
||||||
|
if (status !== 200) {
|
||||||
|
if (status === 403) callback(status, data, ok);
|
||||||
|
else callback(status, null, ok)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback(true, data, ok);
|
||||||
|
});
|
||||||
|
},*/
|
||||||
|
|
||||||
|
//YouTube v3: Statistics
|
||||||
|
//Returns a statistic object with views, likes, etc
|
||||||
|
"youtubestatistics": function (bot, videoId, apiKey, callback) {
|
||||||
|
if (apiKey.trim() === "") {
|
||||||
|
callback(-1, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var url = "https://www.googleapis.com:443" +
|
||||||
|
"/youtube/v3/videos?" +
|
||||||
|
"part=statistics&id=" + videoId + "&key=" + apiKey;
|
||||||
|
var json = true;
|
||||||
|
var options = {
|
||||||
|
method: "GET",
|
||||||
|
timeout: 1500
|
||||||
|
}
|
||||||
|
sendRequest(bot, url, json, options, "youtubestatistics", function(status, data, ok) {
|
||||||
|
if (status !== 200) {
|
||||||
|
if (status === 403) callback(status, data, ok);
|
||||||
|
else callback(status, null, ok)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback(true, data, ok);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
RETURNS:
|
||||||
|
status - Request status code. Can also be <0 for certain situations
|
||||||
|
-1: don't do anything. Invalid input or similar issue.
|
||||||
|
-2: Blocked input.
|
||||||
|
-3: Error or blocked output. Just log the data, don't send it
|
||||||
|
data - Response from wolfram, or error info. Empty if bad input.
|
||||||
|
*/
|
||||||
|
"wolfram": function (bot, query, apiKey, callback) {
|
||||||
|
if (apiKey.trim() === "" || !query || query.trim() === "") {
|
||||||
|
return callback(-1, null, false);
|
||||||
|
}
|
||||||
|
if (/nearme|ipv4|ipv6|latlong|latitude|longitude|rot13|base64|geoip|coordinat|whoami|whereami|ipaddress|location|myip|geograph|fromcharactercode|tocharactercode|bytearraytostring|characterrange|fromletternumber|alphabet|charactername|characterencoding|encod|decod|parse/i.test(query.replace(/\W/g, "")))
|
||||||
|
return callback(-2, "That query is not allowed", false);
|
||||||
|
|
||||||
|
var url = "https://api.wolframalpha.com/v1/result?i=" +
|
||||||
|
encodeURIComponent(query) + "&appid=" + apiKey;
|
||||||
|
var json = false;
|
||||||
|
var options = {
|
||||||
|
method: "GET",
|
||||||
|
timeout: 2000
|
||||||
|
}
|
||||||
|
|
||||||
|
sendRequest(bot, url, json, options, "wolfram", function(status, data, ok) {
|
||||||
|
if (utils.looseIPTest(data) || /.+, united states/i.test(data)) {
|
||||||
|
callback(-3, "Response blocked.", ok);
|
||||||
|
} else if (/error [\d\w]+\:/i.test(data)) {
|
||||||
|
callback(-3, data, ok);
|
||||||
|
}
|
||||||
|
else callback(status, data, ok);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a fetch request to the desired endpoint.
|
||||||
|
*
|
||||||
|
* @param {!Bot} bot Bot object
|
||||||
|
* @param {!string} url Full url of the desired endpoint
|
||||||
|
* @param {?boolean} json Whether or not the data will be JSON. Data returned will automatically be converted from JSON if needed.
|
||||||
|
* @param {?Object} options Object of options, which should contain a HTTP request method and timeout at the very least.
|
||||||
|
* @param {!type} apiName Name of the API to use. Must match one of the keys of the APIs object
|
||||||
|
* @param {?type} callback Function to call after the request
|
||||||
|
*/
|
||||||
|
function sendRequest(bot, url, json, options, apiName, callback) {
|
||||||
|
if (!options.hasOwnProperty("headers")) options["headers"] = {};
|
||||||
|
if (json) {
|
||||||
|
options.headers["Content-Type"] = "application/json";
|
||||||
|
}
|
||||||
|
var body = "";
|
||||||
|
var status = -1;
|
||||||
|
var ok = false;
|
||||||
|
var fn = ()=>{
|
||||||
|
fetch(url, options)
|
||||||
|
.then(res=>{
|
||||||
|
status = res.status;
|
||||||
|
ok = res.ok;
|
||||||
|
if (!ok) {
|
||||||
|
bot.logger.error(strings.format(bot, "API_NOT_OK", [apiName, status, JSON.stringify(res.statusText)]))
|
||||||
|
}
|
||||||
|
return json ? res.json() : res.text();
|
||||||
|
})
|
||||||
|
.then(data=>{
|
||||||
|
if (callback)
|
||||||
|
callback(status, data, ok);
|
||||||
|
})
|
||||||
|
.catch(e=>{
|
||||||
|
if (apiName === "youtubestatistics") {
|
||||||
|
bot.gettingVideoMeta = false;
|
||||||
|
} else if (apiName === "youtubecomments") {
|
||||||
|
bot.gettingComments = false;
|
||||||
|
}
|
||||||
|
if (e.type === "request-timeout") {
|
||||||
|
bot.sendChatMsg(strings.format(bot, "API_TIMEOUT", [apiName]));
|
||||||
|
} else
|
||||||
|
bot.logger.error(strings.format(bot, "API_ERROR", [apiName, e.stack]));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
bot.actionQueue.enqueue([this, fn, []]);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
APIs: APIs,
|
||||||
|
APIcall: function(bot, API, data, apiKey, callback) {
|
||||||
|
if (this.APIs.hasOwnProperty(API)) {
|
||||||
|
this.APIs[API](bot, data, apiKey, callback);
|
||||||
|
} else {
|
||||||
|
bot.logger.error(strings.format(bot, "API_NOT_FOUND", [API]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2060
lib/bot.js
Normal file
2060
lib/bot.js
Normal file
File diff suppressed because it is too large
Load diff
2785
lib/chatcommands.js
Normal file
2785
lib/chatcommands.js
Normal file
File diff suppressed because it is too large
Load diff
97
lib/classes.js
Normal file
97
lib/classes.js
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
//queue items like this:
|
||||||
|
//AutoFnQueueObject.enqueue([this (context), fn_name, [argument0, argument1, ...]])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class which allows automatic queueing of actions with a specified interval time.
|
||||||
|
* @constructor
|
||||||
|
* @param {number} interval Amount of time between actions in milliseconds.
|
||||||
|
*/
|
||||||
|
function AutoFnQueue(interval) {
|
||||||
|
if (typeof interval !== "number" || interval < 0) {
|
||||||
|
throw new TypeError("AutoFnQueue: interval must be a positive number!");
|
||||||
|
}
|
||||||
|
this.items = [];
|
||||||
|
this.interval = interval;
|
||||||
|
this.flushing = false;
|
||||||
|
this.intervalId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue an item and begin flushing the queue, if not flushing already.
|
||||||
|
*
|
||||||
|
* @param {any[]} item Array consisting of context, the function to execute, and an array of arguments to apply to the function if needed, in that order.
|
||||||
|
*/
|
||||||
|
AutoFnQueue.prototype.enqueue = function(item) {
|
||||||
|
this.items.push(item);
|
||||||
|
this.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shifts the first item from the queue and returns it.
|
||||||
|
*
|
||||||
|
* @return {any[]} Queue item
|
||||||
|
*/
|
||||||
|
AutoFnQueue.prototype.dequeue = function() {
|
||||||
|
if (!this.isEmpty()) return this.items.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the queue is empty.
|
||||||
|
*
|
||||||
|
* @return {boolean} True if empty, false otherwise.
|
||||||
|
*/
|
||||||
|
AutoFnQueue.prototype.isEmpty = function() {
|
||||||
|
return this.items.length <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the first item from the queue but does not remove it.
|
||||||
|
*
|
||||||
|
* @return {any[]} Queue item
|
||||||
|
*/
|
||||||
|
AutoFnQueue.prototype.peek = function() {
|
||||||
|
return this.items[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begins flushing the queue if it has not been started already.
|
||||||
|
*/
|
||||||
|
AutoFnQueue.prototype.flush = function() {
|
||||||
|
if (this.flushing || this.isEmpty()) return;
|
||||||
|
this.flushing = true;
|
||||||
|
var item = this.dequeue();
|
||||||
|
item[1].apply(item[0], item[2]);
|
||||||
|
this.intervalId = setInterval(()=> {
|
||||||
|
if (this.isEmpty()) {
|
||||||
|
this.interrupt();
|
||||||
|
} else {
|
||||||
|
var item = this.dequeue();
|
||||||
|
item[1].apply(item[0], item[2]);
|
||||||
|
}
|
||||||
|
}, this.interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops flushing the queue immediately and leaves the rest of the queued items waiting.
|
||||||
|
*/
|
||||||
|
AutoFnQueue.prototype.interrupt = function() {
|
||||||
|
if (this.intervalId) clearInterval(this.intervalId);
|
||||||
|
this.intervalId = null;
|
||||||
|
this.flushing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interrupts and clears the queue.
|
||||||
|
*/
|
||||||
|
AutoFnQueue.prototype.clearQueue = function() {
|
||||||
|
this.interrupt();
|
||||||
|
this.items = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
"AutoFnQueue":AutoFnQueue
|
||||||
|
}
|
||||||
175
lib/clicommands.js
Normal file
175
lib/clicommands.js
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const C = require("cli-color");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
const strings = require("./strings.js");
|
||||||
|
const api = require("./api.js");
|
||||||
|
const utils = require("./utils.js");
|
||||||
|
|
||||||
|
/*
|
||||||
|
Here are commands that will execute if used in the terminal, for example:
|
||||||
|
/say msg
|
||||||
|
These default commands are mostly used for debugging purposes.
|
||||||
|
|
||||||
|
"cmdname": function(bot, cmd [the cmd name used], message [the rest of the input after the command name]) {
|
||||||
|
action when used;
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
var clicommands = {
|
||||||
|
"exit": function(bot) {
|
||||||
|
bot.kill("exit by user", 1000, 3);
|
||||||
|
},
|
||||||
|
"restart": function(bot) {
|
||||||
|
bot.kill("restart by user", 1000, 0);
|
||||||
|
},
|
||||||
|
"say": function(bot, cmd, message) {
|
||||||
|
bot.sendChatMsg(message, false, true, true);
|
||||||
|
},
|
||||||
|
"userinfo": function(bot, cmd, message) {
|
||||||
|
if (message.trim() === "") return;
|
||||||
|
let i = 0;
|
||||||
|
for (;i<bot.CHANNEL.users.length;i++) {
|
||||||
|
let user = bot.CHANNEL.users[i];
|
||||||
|
if (message.toLowerCase() === user.name.toLowerCase()) {
|
||||||
|
bot.logger.info(JSON.stringify(user));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"users": function(bot, cmd, message) {
|
||||||
|
//logic from CyTube: https://github.com/calzoneman/sync/blob/f081bc782adba074052884995b90bc77dcef3338/www/js/util.js#L417
|
||||||
|
function sortUserlist() {
|
||||||
|
let userlist = bot.CHANNEL.users;
|
||||||
|
userlist.sort(function(A,B) {
|
||||||
|
let nameA = A.name.toLowerCase(),
|
||||||
|
nameB = B.name.toLowerCase();
|
||||||
|
let afkA = A.meta.afk,
|
||||||
|
afkB = B.meta.afk;
|
||||||
|
if (afkA && !afkB) return 1;
|
||||||
|
if (!afkA && afkB) return -1;
|
||||||
|
|
||||||
|
let rankA = A.rank,
|
||||||
|
rankB = B.rank;
|
||||||
|
if (rankA < rankB) return 1;
|
||||||
|
if (rankA > rankB) return -1;
|
||||||
|
|
||||||
|
if (nameA > nameB) return 1;
|
||||||
|
if (nameA < nameB) return -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
sortUserlist();
|
||||||
|
let i = 0,
|
||||||
|
users = bot.CHANNEL.users,
|
||||||
|
out = [];
|
||||||
|
for (;i<users.length;i++) {
|
||||||
|
out.push(utils.colorUsername(bot, users[i]));
|
||||||
|
}
|
||||||
|
bot.logger.info("Registered users online: " + out.join(", "));
|
||||||
|
},
|
||||||
|
"videolist": function(bot, cmd, message) {
|
||||||
|
for (var i = 0; i < bot.CHANNEL.playlist.length; i++) {
|
||||||
|
bot.logger.info(JSON.stringify(bot.CHANNEL.playlist[i]));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"currentmedia": function(bot,cmd,message) {
|
||||||
|
var cm = bot.CHANNEL.currentMedia;
|
||||||
|
bot.logger.info(cm ? JSON.stringify(bot.CHANNEL.currentMedia) : "currentMedia appears to be empty.");
|
||||||
|
},
|
||||||
|
"readchanlog": function(bot, cmd, message) {
|
||||||
|
bot.readChanLog();
|
||||||
|
},
|
||||||
|
"setname": function(bot, cmd, message) {
|
||||||
|
if (bot.username === "" && !bot.guest) {
|
||||||
|
if (!utils.isValidUserName(message)) {
|
||||||
|
bot.logger.error("Invalid username. Must be 1-20 chars long and consist of -, _, or alphanumeric characters only.");
|
||||||
|
} else {
|
||||||
|
var fn = (()=>{
|
||||||
|
bot.socket.emit("login", {
|
||||||
|
name: message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
bot.actionQueue.enqueue([this, fn, []]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"showbumpstats": function(bot, cmd, message) {
|
||||||
|
let bs = bot.bumpStats;
|
||||||
|
bot.logger.info(JSON.stringify(bs));
|
||||||
|
},
|
||||||
|
"memory": function(bot, cmd, message) {
|
||||||
|
bot.logger.info(strings.format(bot, "MEMORY_USAGE", [(process.memoryUsage().heapUsed / 1024), "KB"]));
|
||||||
|
},
|
||||||
|
"subnet": function(bot, cmd, message) {
|
||||||
|
if (message.trim() === "") return;
|
||||||
|
let user = bot.getUser(message);
|
||||||
|
if (user && user.meta.ip) {
|
||||||
|
let matches = bot.matchSubnet(user.meta.ip, true);
|
||||||
|
bot.logger.info(matches.length > 0 ? matches.join(", ") : "No matches found.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"setluck": function(bot,cmd,message) {
|
||||||
|
if (message.trim() === "") return;
|
||||||
|
let spl = message.split(" ");
|
||||||
|
if (spl.length < 2) return;
|
||||||
|
let user = bot.getUser(spl[0]);
|
||||||
|
let luck = parseInt(spl[1]);
|
||||||
|
if (user && !isNaN(luck)) {
|
||||||
|
bot.settings.lucky[user.name] = luck;
|
||||||
|
bot.logger.info("Set " + user.name + "'s luck to " + luck);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"testalllogs": function(bot,cmd,message) {
|
||||||
|
let logs = bot.logger;
|
||||||
|
for (var i in logs) {
|
||||||
|
logs[i]("Test message.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disablecommands": function(bot, cmd, message) {
|
||||||
|
bot.cfg.chat.disableAllCommands = true;
|
||||||
|
bot.logger.info("Disabled commands.");
|
||||||
|
},
|
||||||
|
"enablecommands": function(bot, cmd, message) {
|
||||||
|
bot.cfg.chat.disableAllCommands = false;
|
||||||
|
bot.logger.info("Enabled commands.");
|
||||||
|
},
|
||||||
|
"mute": function(bot, cmd, message) { //TODO: make global mute/unmute functions that the chatcommands will also use
|
||||||
|
if (!bot.getOpt("muted", false)) {
|
||||||
|
bot.logger.mod(strings.format(bot, "BOT_MUTED", ["CLI user"]));
|
||||||
|
bot.setOpt("muted", true);
|
||||||
|
} else {
|
||||||
|
bot.logger.warn("You're already muted.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unmute": function(bot, cmd, message) {
|
||||||
|
if (bot.getOpt("muted", false)) {
|
||||||
|
bot.logger.mod(strings.format(bot, "BOT_UNMUTED", ["CLI user"]));
|
||||||
|
bot.setOpt("muted", false);
|
||||||
|
} else {
|
||||||
|
bot.logger.warn("You're not muted.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var aliases = {
|
||||||
|
kill: "exit"
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
"exec":function(bot, input) {
|
||||||
|
if (bot.killed) return;
|
||||||
|
var split = input.split(" "),
|
||||||
|
cmd = split.splice(0,1)[0].substr(1);
|
||||||
|
if (aliases.hasOwnProperty(cmd) && !clicommands.hasOwnProperty(cmd)) {
|
||||||
|
cmd = aliases[cmd];
|
||||||
|
}
|
||||||
|
cmd = cmd.toLowerCase();
|
||||||
|
if (clicommands.hasOwnProperty(cmd)) {
|
||||||
|
clicommands[cmd](bot, cmd, split.join(" "));
|
||||||
|
} else {
|
||||||
|
bot.logger.warn(strings.format(bot, "UNKNOWN_CLI_COMMAND", [C.yellow("/" + cmd)]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
lib/customchatcommands-m4c.js
Normal file
79
lib/customchatcommands-m4c.js
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/*
|
||||||
|
Use this file to define custom commands, especially room-centric ones.
|
||||||
|
Try to avoid editing chatcommands.js so future updates won't erase your edits.
|
||||||
|
|
||||||
|
Rename this file to customchatcommands.js to use it, OR if the advanced
|
||||||
|
configuration setting "useChannelCustomCommands" is true, rename this to
|
||||||
|
customchatcommands-roomname.js instead.
|
||||||
|
|
||||||
|
You can also rename this to customchatcommands-(anything).js and edit your
|
||||||
|
configuration file accordingly. Within your config, refer to:
|
||||||
|
advanced.customCommandsToLoad
|
||||||
|
There is more information in the configuration file on how to set this up.
|
||||||
|
|
||||||
|
See chatcommands.js for more information on creating commands.
|
||||||
|
|
||||||
|
!!
|
||||||
|
This file may contain usage of emotes or other features
|
||||||
|
that you probably don't have in your room. These are just here as examples.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const C = require("cli-color");
|
||||||
|
|
||||||
|
const utils = require("./utils.js");
|
||||||
|
const strings = require("./strings.js");
|
||||||
|
const api = require("./api.js");
|
||||||
|
const Command = require("./chatcommands.js").Command;
|
||||||
|
|
||||||
|
function getCommands(bot) {
|
||||||
|
var commands = {
|
||||||
|
"gdqschedule": new Command({
|
||||||
|
cmdName: "gdqschedule",
|
||||||
|
minRank: bot.RANKS.USER,
|
||||||
|
rankMatch: ">=",
|
||||||
|
userCooldown: 600000,
|
||||||
|
cmdCooldown: 300000,
|
||||||
|
isActive: false,
|
||||||
|
requiredChannelPerms: ["pollctl"],
|
||||||
|
allowRankChange: true,
|
||||||
|
canBeUsedInPM: true
|
||||||
|
}, function (cmd, user, message, opts) {
|
||||||
|
api.APIcall(bot, "gdq", null, null, function(status, data, ok) {
|
||||||
|
if (ok) {
|
||||||
|
let schedule = [], i = 0, now = Date.now();
|
||||||
|
let items = data.data.items;
|
||||||
|
for (;i < items.length && schedule.length < 6; i++) {
|
||||||
|
let time = (items[i].scheduled_t+items[i].length_t)*1000;
|
||||||
|
if (time > now) {
|
||||||
|
schedule.push(items[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (schedule.length > 0) {
|
||||||
|
let item = schedule[0];
|
||||||
|
let title = "now: " + item.data[0] + ", " + item.data[3] + " (runner: "+item.data[1]+", est: "+item.data[2]+")";
|
||||||
|
let options = [], i = 1;
|
||||||
|
for (;i < schedule.length; i++) {
|
||||||
|
let item = schedule[i];
|
||||||
|
options.push(new Date(item.scheduled_t*1000).toGMTString().split(" ")[4] + " UTC: " + item.data[0] + ", " + item.data[3] + " (runner: "+item.data[1]+", est: "+item.data[2]+")");
|
||||||
|
}
|
||||||
|
bot.openPoll({
|
||||||
|
title: title,
|
||||||
|
opts: options,
|
||||||
|
obscured: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var aliases = {}
|
||||||
|
|
||||||
|
return {commands: commands, aliases: aliases}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getCommands:getCommands
|
||||||
|
}
|
||||||
145
lib/customchatcommands-toke.js
Normal file
145
lib/customchatcommands-toke.js
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/*
|
||||||
|
Use this file to define custom commands, especially room-centric ones.
|
||||||
|
Try to avoid editing chatcommands.js so future updates won't erase your edits.
|
||||||
|
|
||||||
|
Rename this file to customchatcommands.js to use it, OR if the advanced
|
||||||
|
configuration setting "useChannelCustomCommands" is true, rename this to
|
||||||
|
customchatcommands-roomname.js instead.
|
||||||
|
|
||||||
|
You can also rename this to customchatcommands-(anything).js and edit your
|
||||||
|
configuration file accordingly. Within your config, refer to:
|
||||||
|
advanced.customCommandsToLoad
|
||||||
|
There is more information in the configuration file on how to set this up.
|
||||||
|
|
||||||
|
See chatcommands.js for more information on creating commands.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const C = require("cli-color");
|
||||||
|
|
||||||
|
const utils = require("./utils.js");
|
||||||
|
const strings = require("./strings.js");
|
||||||
|
const api = require("./api.js");
|
||||||
|
const Command = require("./chatcommands.js").Command;
|
||||||
|
let toking = 0;//0 ready to start toke, 1 toking, 2 cooldown active
|
||||||
|
let tokers = [];
|
||||||
|
let cdown = 3;
|
||||||
|
let cdel = 120;
|
||||||
|
let ctime = cdel;
|
||||||
|
|
||||||
|
function getCommands(bot) {
|
||||||
|
var commands = {
|
||||||
|
|
||||||
|
"420blazeit": new Command({
|
||||||
|
cmdName: "420blazeit",
|
||||||
|
minRank: bot.RANKS.USER,
|
||||||
|
rankMatch: ">=",
|
||||||
|
userCooldown: 0,
|
||||||
|
cmdCooldown: 0,
|
||||||
|
isActive: true,
|
||||||
|
requiredChannelPerms: ["chat"],
|
||||||
|
allowRankChange: true,
|
||||||
|
canBeUsedInPM: false
|
||||||
|
}, function (cmd, user, message, opts) {
|
||||||
|
toke(user.name, bot);
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var aliases = {
|
||||||
|
toak: "420blazeit",
|
||||||
|
666: "420blazeit",
|
||||||
|
420: "420blazeit",
|
||||||
|
toke: "420blazeit",
|
||||||
|
tokem: "420blazeit",
|
||||||
|
toek: "420blazeit",
|
||||||
|
hailsatan: "420blazeit",
|
||||||
|
cheers: "420blazeit",
|
||||||
|
toast: "420blazeit",
|
||||||
|
toastem: "420blazeit",
|
||||||
|
burn: "420blazeit",
|
||||||
|
burnem: "420blazeit",
|
||||||
|
lightem: "420blazeit",
|
||||||
|
dab: "420blazeit",
|
||||||
|
dabem: "420blazeit",
|
||||||
|
smoke: "420blazeit",
|
||||||
|
smokem: "420blazeit",
|
||||||
|
blaze: "420blazeit",
|
||||||
|
blazeit: "420blazeit",
|
||||||
|
blazem: "420blazeit",
|
||||||
|
}
|
||||||
|
|
||||||
|
return {commands: commands, aliases: aliases}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getCommands:getCommands
|
||||||
|
}
|
||||||
|
|
||||||
|
function toke(name, bot){
|
||||||
|
switch (toking){
|
||||||
|
case 0://ready to start toke
|
||||||
|
bot.sendChatMsg("A group toke has been started by " + name + "! We'll be taking a toke in 60 seconds - join in by posting !toke");
|
||||||
|
cdown = 3;
|
||||||
|
toking = 1;
|
||||||
|
tokers.push(name);
|
||||||
|
setTimeout(countdown, 57000, bot);
|
||||||
|
break;
|
||||||
|
case 1://taking toke
|
||||||
|
if(tokers.includes(name)){
|
||||||
|
bot.sendPM(name, ("You're already taking part in this toke!"));
|
||||||
|
}else{
|
||||||
|
bot.sendChatMsg(name + " joined the toke! Post !toke to take part!");
|
||||||
|
tokers.push(name);
|
||||||
|
cdown = 3;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 2://cooldown
|
||||||
|
bot.sendPM(name, "Please wait " + ctime + " before starting a new group toke.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function countdown(bot){
|
||||||
|
toking = 1;//set toking mode
|
||||||
|
|
||||||
|
bot.sendChatMsg(cdown + "...");//send countdown msg
|
||||||
|
--cdown;//count down
|
||||||
|
|
||||||
|
if(cdown <= 0){//if cdown hits 0
|
||||||
|
setTimeout(endToke, 1000, bot);
|
||||||
|
}else{
|
||||||
|
setTimeout(countdown, 1000, bot);//call endtoke
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function endToke(bot){
|
||||||
|
if(cdown != 0){
|
||||||
|
setTimeout(countdown, 1000, bot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(tokers.length > 1){
|
||||||
|
bot.sendChatMsg("Take a toke " + tokers.toString() + "! " + tokers.length + " tokers!");
|
||||||
|
}else{
|
||||||
|
bot.sendChatMsg("Take a toke " + tokers.toString() + ". https://ourfore.st/img/femotes/onetoker.jpg");
|
||||||
|
}
|
||||||
|
tokers = [];
|
||||||
|
toking = 2;//reset toking mode
|
||||||
|
setTimeout(cooldown, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cooldown(){
|
||||||
|
if(ctime > 0){
|
||||||
|
toking = 2;
|
||||||
|
--ctime;
|
||||||
|
setTimeout(cooldown, 1000);
|
||||||
|
}else{
|
||||||
|
toking = 0;
|
||||||
|
ctime = cdel;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
181
lib/customchatcommands-v4c.js
Normal file
181
lib/customchatcommands-v4c.js
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/*
|
||||||
|
Use this file to define custom commands, especially room-centric ones.
|
||||||
|
Try to avoid editing chatcommands.js so future updates won't erase your edits.
|
||||||
|
|
||||||
|
Rename this file to customchatcommands.js to use it, OR if the advanced
|
||||||
|
configuration setting "useChannelCustomCommands" is true, rename this to
|
||||||
|
customchatcommands-roomname.js instead.
|
||||||
|
|
||||||
|
You can also rename this to customchatcommands-(anything).js and edit your
|
||||||
|
configuration file accordingly. Within your config, refer to:
|
||||||
|
advanced.customCommandsToLoad
|
||||||
|
There is more information in the configuration file on how to set this up.
|
||||||
|
|
||||||
|
See chatcommands.js for more information on creating commands.
|
||||||
|
|
||||||
|
!!
|
||||||
|
This file may contain usage of emotes or other features
|
||||||
|
that you probably don't have in your room. These are just here as examples.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const C = require("cli-color");
|
||||||
|
|
||||||
|
const utils = require("./utils.js");
|
||||||
|
const strings = require("./strings.js");
|
||||||
|
const api = require("./api.js");
|
||||||
|
const Command = require("./chatcommands.js").Command;
|
||||||
|
|
||||||
|
function getCommands(bot) {
|
||||||
|
var commands = {
|
||||||
|
"fortune": new Command({
|
||||||
|
cmdName: "fortune",
|
||||||
|
minRank: bot.RANKS.USER,
|
||||||
|
rankMatch: ">=",
|
||||||
|
userCooldown: 20000,
|
||||||
|
cmdCooldown: 3000,
|
||||||
|
isActive: true,
|
||||||
|
requiredChannelPerms: ["chat"],
|
||||||
|
allowRankChange: true,
|
||||||
|
canBeUsedInPM: false
|
||||||
|
}, function (cmd, user, message, opts) {
|
||||||
|
var fortunes = [
|
||||||
|
["ssc:#F51C6A ","Reply hazy, try again",0],
|
||||||
|
["ssc:#FD4D32 ","Excellent Luck",14],
|
||||||
|
["ssc:#E7890C ","Good Luck",4],
|
||||||
|
["ssc:#BAC200 ","Average Luck",0],
|
||||||
|
["ssc:#7FEC11 ","Bad Luck",0],
|
||||||
|
["ssc:#43FD3B ","Good news will come to you by mail",1],
|
||||||
|
["ssc:#16F174 ","( ´_ゝ`)フーン",0],
|
||||||
|
["ssc:#00CBB0 ","キタ━━━━━━(゚∀゚)━━━━━━ !!!!",2],
|
||||||
|
["ssc:#0893E1 ","You will meet a dark handsome stranger",1],
|
||||||
|
["ssc:#2A56FB ","Better not tell you now",0],
|
||||||
|
["ssc:#6023F8 ","Outlook good",4],
|
||||||
|
["ssc:#9D05DA ","Very Bad Luck",0],
|
||||||
|
["ssc:#D302A7 ","Godly Luck",29],
|
||||||
|
["ssc:#ff4f4f ","JUST",0]
|
||||||
|
];
|
||||||
|
let fortune = fortunes[Math.floor(Math.random() * fortunes.length)];
|
||||||
|
let settingChanged = false;
|
||||||
|
let userLucky = bot.settings.lucky[user.name];
|
||||||
|
if (fortune[2] > 0) {
|
||||||
|
bot.settings.lucky[user.name] = fortune[2];
|
||||||
|
settingChanged = true;
|
||||||
|
} else if (userLucky) {
|
||||||
|
delete bot.settings.lucky[user.name];
|
||||||
|
settingChanged = true;
|
||||||
|
}
|
||||||
|
if (settingChanged) bot.writeSettings();
|
||||||
|
if (!bot.cfg.chat.roomHasSSC) fortune[0] = "";
|
||||||
|
return bot.sendChatMsg(fortune[0] + "**" + user.name + "'s fortune: " + fortune[1] + "**");
|
||||||
|
}),
|
||||||
|
"saltpoll": new Command({
|
||||||
|
cmdName: "saltpoll",
|
||||||
|
minRank: bot.RANKS.MOD,
|
||||||
|
rankMatch: ">=",
|
||||||
|
userCooldown: 0,
|
||||||
|
cmdCooldown: 10000,
|
||||||
|
isActive: true,
|
||||||
|
requiredChannelPerms: ["pollctl"],
|
||||||
|
allowRankChange: true,
|
||||||
|
canBeUsedInPM: true
|
||||||
|
}, function (cmd, user, message, opts) {
|
||||||
|
api.APIcall(bot, "saltybet", null, null, function(status, data, ok) {
|
||||||
|
if (!ok) return bot.sendPM(user.name, "There was an error getting SaltyBet data.");
|
||||||
|
let poll = {
|
||||||
|
title:"PLACE YOUR BETS!",
|
||||||
|
opts: [],
|
||||||
|
obscured: false
|
||||||
|
},
|
||||||
|
emoteA = "",
|
||||||
|
emoteB = "";
|
||||||
|
let spl = message.split(" ");
|
||||||
|
if (spl[0]) emoteA = spl[0];
|
||||||
|
if (spl[1]) emoteB = spl[1];
|
||||||
|
let p1 = emoteA + " " + data.p1name,
|
||||||
|
p2 = emoteB + " " + data.p2name;
|
||||||
|
if (data.status === "open" || data.status === 2) {
|
||||||
|
poll.opts = [p1, p2];
|
||||||
|
} else if (data.status === "locked") {
|
||||||
|
p1 += " $" + data.p1total;
|
||||||
|
p2 += " $" + data.p2total;
|
||||||
|
poll.title = "BETTING CLOSED!";
|
||||||
|
poll.opts = [p1, p2];
|
||||||
|
}
|
||||||
|
if (poll.opts.length > 0) {
|
||||||
|
bot.openPoll(poll);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
"vidya": new Command({
|
||||||
|
cmdName: "vidya",
|
||||||
|
minRank: bot.RANKS.MOD,
|
||||||
|
rankMatch: ">=",
|
||||||
|
userCooldown: 0,
|
||||||
|
cmdCooldown: 2000,
|
||||||
|
isActive: true,
|
||||||
|
requiredChannelPerms: ["seeplaylist", "playlistmove"],
|
||||||
|
allowRankChange: true,
|
||||||
|
canBeUsedInPM: false
|
||||||
|
}, function (cmd, user, message, opts) {
|
||||||
|
let vidObj = null;
|
||||||
|
let spl = message.split(" ");
|
||||||
|
let playlist = bot.CHANNEL.playlist;
|
||||||
|
if (message.trim() !== "") {
|
||||||
|
vidObj = findLastMedia(spl[0]);
|
||||||
|
} else {
|
||||||
|
vidObj = findLastMedia(user.name);
|
||||||
|
}
|
||||||
|
if (vidObj) {
|
||||||
|
let TARGETUSER = vidObj.media.queueby;
|
||||||
|
if (bot.disallowed(TARGETUSER)) {
|
||||||
|
bot.sendPM(user.name, TARGETUSER + " is disallowed.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let active = bot.getMediaIndex(bot.CHANNEL.currentUID);
|
||||||
|
if (!~active) {
|
||||||
|
bot.moveMedia(vidObj.media.uid, "prepend");
|
||||||
|
} else {
|
||||||
|
bot.moveMedia(vidObj.media.uid, playlist[active].uid);
|
||||||
|
}
|
||||||
|
bot.logger.mod(strings.format(bot, "BUMP_LOG", [
|
||||||
|
"VIDYA BUMP",
|
||||||
|
utils.formatLink(vidObj.media.media.id, vidObj.media.media.type, true),
|
||||||
|
TARGETUSER,
|
||||||
|
user.name,
|
||||||
|
(vidObj.index+1),
|
||||||
|
(~active ? active+1 : 1)
|
||||||
|
]));
|
||||||
|
if (bot.db && bot.cfg.db.useTables.users && bot.cfg.db.useTables.bump_stats) {
|
||||||
|
let column = "vidya_others";
|
||||||
|
if (TARGETUSER === user.name) column = "vidya_self";
|
||||||
|
bot.db.run("bumpCount", [user.name, column]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let aliases = {};
|
||||||
|
|
||||||
|
function findLastMedia(name) {
|
||||||
|
name = name.toLowerCase();
|
||||||
|
let playlist = bot.CHANNEL.playlist;
|
||||||
|
let i = playlist.length-1;
|
||||||
|
for (;i >= 0;i--) {
|
||||||
|
if (playlist[i].queueby.toLowerCase() === name) {
|
||||||
|
return {media:playlist[i], index:i};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {commands: commands, aliases: aliases}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getCommands:getCommands
|
||||||
|
}
|
||||||
52
lib/customchatcommands.example.js
Normal file
52
lib/customchatcommands.example.js
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/*
|
||||||
|
Use this file to define custom commands, especially room-centric ones.
|
||||||
|
Try to avoid editing chatcommands.js so future updates won't erase your edits.
|
||||||
|
|
||||||
|
Rename this file to customchatcommands.js to use it, OR if the advanced
|
||||||
|
configuration setting "useChannelCustomCommands" is true, rename this to
|
||||||
|
customchatcommands-roomname.js instead.
|
||||||
|
|
||||||
|
You can also rename this to customchatcommands-(anything).js and edit your
|
||||||
|
configuration file accordingly. Within your config, refer to:
|
||||||
|
advanced.customCommandsToLoad
|
||||||
|
There is more information in the configuration file on how to set this up.
|
||||||
|
|
||||||
|
See chatcommands.js for more information on creating commands.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const C = require("cli-color");
|
||||||
|
|
||||||
|
const utils = require("./utils.js");
|
||||||
|
const strings = require("./strings.js");
|
||||||
|
const api = require("./api.js");
|
||||||
|
const Command = require("./chatcommands.js").Command;
|
||||||
|
|
||||||
|
function getCommands(bot) {
|
||||||
|
var commands = {
|
||||||
|
"testcommand": new Command({
|
||||||
|
cmdName: "testcommand",
|
||||||
|
minRank: bot.RANKS.USER,
|
||||||
|
rankMatch: ">=",
|
||||||
|
userCooldown: 2000,
|
||||||
|
cmdCooldown: 2000,
|
||||||
|
isActive: true,
|
||||||
|
requiredChannelPerms: ["chat"],
|
||||||
|
allowRankChange: true,
|
||||||
|
canBeUsedInPM: false
|
||||||
|
}, function (cmd, user, message, opts) {
|
||||||
|
bot.sendChatMsg("Test command working!");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var aliases = {
|
||||||
|
testcmd: "testcommand"
|
||||||
|
}
|
||||||
|
|
||||||
|
return {commands: commands, aliases: aliases}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getCommands:getCommands
|
||||||
|
}
|
||||||
462
lib/db.js
Normal file
462
lib/db.js
Normal file
|
|
@ -0,0 +1,462 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const pg = require("pg");
|
||||||
|
var pool = {};
|
||||||
|
|
||||||
|
module.exports["active"] = false;
|
||||||
|
module.exports["pool"] = pool;
|
||||||
|
module.exports["endPool"] = function(){};
|
||||||
|
module.exports["run"] = function(){return Promise.resolve(false)};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Excuse the mess in here
|
||||||
|
someone please rewrite all this
|
||||||
|
*/
|
||||||
|
|
||||||
|
var initialized = false;
|
||||||
|
|
||||||
|
function init(bot) {
|
||||||
|
if (initialized) return;
|
||||||
|
initialized = true;
|
||||||
|
var active = bot.cfg.db.use,
|
||||||
|
schema = bot.CHANNEL.room;
|
||||||
|
module.exports["active"] = active;
|
||||||
|
if (active && schema.trim() !== "") {
|
||||||
|
try {
|
||||||
|
pool = new pg.Pool(bot.cfg.db.connectionInfo);
|
||||||
|
} catch (e) {
|
||||||
|
bot.logger.error(e.stack);
|
||||||
|
bot.logger.error(strings.format(bot, "DB_BAD_INFO"));
|
||||||
|
module.exports["active"] = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
module.exports["pool"] = pool;
|
||||||
|
module.exports["endPool"] = pool.end.bind(pool);
|
||||||
|
|
||||||
|
pool.query(`CREATE SCHEMA IF NOT EXISTS ${schema}`, (err, res) => {
|
||||||
|
genericHandler(err, res, function() {
|
||||||
|
|
||||||
|
//Define new tables here
|
||||||
|
|
||||||
|
if (!bot.cfg.db.useTables.users) return false;
|
||||||
|
pool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS ${schema}.users
|
||||||
|
(
|
||||||
|
"uname" varchar(20) PRIMARY KEY,
|
||||||
|
"first_seen" timestamp DEFAULT NOW(),
|
||||||
|
"last_seen" timestamp NOT NULL DEFAULT NOW(),
|
||||||
|
"room_time" decimal(13,3) NOT NULL DEFAULT 0.000,
|
||||||
|
"afk_time" decimal(13,3) NOT NULL DEFAULT 0.000,
|
||||||
|
"joins" integer NOT NULL DEFAULT 1
|
||||||
|
);`, (err, res)=>{genericHandler(err,res)});
|
||||||
|
|
||||||
|
if (bot.cfg.db.useTables.emote_data)
|
||||||
|
pool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS ${schema}.emote_data
|
||||||
|
(
|
||||||
|
"uname" varchar(20) REFERENCES ${schema}.users(uname),
|
||||||
|
"emote" varchar(320) NOT NULL,
|
||||||
|
"count" integer NOT NULL DEFAULT 1,
|
||||||
|
UNIQUE (uname, emote)
|
||||||
|
);`, (err, res)=>{genericHandler(err,res)});
|
||||||
|
|
||||||
|
if (bot.cfg.db.useTables.duel_stats)
|
||||||
|
pool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS ${schema}.duel_stats
|
||||||
|
(
|
||||||
|
"uname" varchar(20) REFERENCES ${schema}.users(uname),
|
||||||
|
"wins" integer NOT NULL,
|
||||||
|
"losses" integer NOT NULL,
|
||||||
|
UNIQUE (uname)
|
||||||
|
);`, (err, res)=>{genericHandler(err,res)});
|
||||||
|
|
||||||
|
if (bot.cfg.db.useTables.chat)
|
||||||
|
pool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS ${schema}.chat
|
||||||
|
(
|
||||||
|
"uname" varchar(20) REFERENCES ${schema}.users(uname),
|
||||||
|
"time" timestamp NOT NULL,
|
||||||
|
"msg" varchar(320) NOT NULL,
|
||||||
|
UNIQUE (uname, msg)
|
||||||
|
);`, (err, res)=>{genericHandler(err,res)});
|
||||||
|
|
||||||
|
if (bot.cfg.db.useTables.bump_stats)
|
||||||
|
pool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS ${schema}.bump_stats
|
||||||
|
(
|
||||||
|
"uname" varchar(20) REFERENCES ${schema}.users(uname),
|
||||||
|
"others" integer NOT NULL DEFAULT 0,
|
||||||
|
"self" integer NOT NULL DEFAULT 0,
|
||||||
|
"vidya_self" integer NOT NULL DEFAULT 0,
|
||||||
|
"vidya_others" integer NOT NULL DEFAULT 0,
|
||||||
|
UNIQUE (uname)
|
||||||
|
);`, (err, res)=>{genericHandler(err,res)});
|
||||||
|
|
||||||
|
if (bot.cfg.db.useTables.saved_polls)
|
||||||
|
pool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS ${schema}.saved_polls
|
||||||
|
(
|
||||||
|
"savedby" varchar(20) REFERENCES ${schema}.users(uname),
|
||||||
|
"poll_name" varchar(30) NOT NULL PRIMARY KEY,
|
||||||
|
"title" varchar(255) NOT NULL,
|
||||||
|
"obscured" boolean NOT NULL,
|
||||||
|
"options" text NOT NULL,
|
||||||
|
UNIQUE (title, obscured, options)
|
||||||
|
);`, (err, res)=>{genericHandler(err,res)});
|
||||||
|
|
||||||
|
/*if (bot.cfg.db.useTables.video_play_data)
|
||||||
|
pool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS ${schema}.video_play_data
|
||||||
|
(
|
||||||
|
"uname" varchar(20) REFERENCES ${schema}.users(uname),
|
||||||
|
"mediaID" text NOT NULL,
|
||||||
|
"date_played" timestamp DEFAULT NOW() NOT NULL,
|
||||||
|
"skip_percent_needed" boolean,
|
||||||
|
"duration_percent" text,
|
||||||
|
UNIQUE (mediaID, date_played)
|
||||||
|
);`, (err, res)=>{genericHandler(err,res)});*/
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
Define queries here.
|
||||||
|
Typically, you should call these like:
|
||||||
|
bot.db.run("queryName", [values], cb(res){})
|
||||||
|
Values is an array, and is used for parameterized values in most cases
|
||||||
|
cb is the callback carrying the database's response
|
||||||
|
*/
|
||||||
|
|
||||||
|
var queries = {
|
||||||
|
addNewChat: function(values, cb) {
|
||||||
|
//Check the required tables for each query
|
||||||
|
if (bot.cfg.db.useTables.users && bot.cfg.db.useTables.chat) {
|
||||||
|
//Return the Promise from runQuery
|
||||||
|
return runQuery(`INSERT INTO ${schema}.chat (uname, time, msg)
|
||||||
|
VALUES ($1, TO_TIMESTAMP($2), $3)
|
||||||
|
ON CONFLICT (uname, msg)
|
||||||
|
DO NOTHING;`, values, cb);
|
||||||
|
}
|
||||||
|
//Return false to reject the promise if one of the tables are not active
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
addNewUser: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users) {
|
||||||
|
return runQuery(`INSERT INTO ${schema}.users (uname)
|
||||||
|
VALUES ($1)
|
||||||
|
ON CONFLICT (uname)
|
||||||
|
DO NOTHING
|
||||||
|
RETURNING joins;`, values, cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
bumpCount: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users && bot.cfg.db.useTables.bump_stats) {
|
||||||
|
return runQuery(`INSERT INTO ${schema}.bump_stats (uname, ${values[1]})
|
||||||
|
VALUES($1, 1)
|
||||||
|
ON CONFLICT (uname)
|
||||||
|
DO
|
||||||
|
UPDATE SET
|
||||||
|
${values[1]} = ${schema}.bump_stats.${values[1]} + 1`, [values[0]], cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
cleanUnusedEmotes: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users && bot.cfg.db.useTables.emote_data) {
|
||||||
|
return runQuery(`DELETE FROM ${schema}.emote_data
|
||||||
|
WHERE
|
||||||
|
emote = ANY($1)`, values, cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
deleteUserChat: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users && bot.cfg.db.useTables.chat) {
|
||||||
|
return runQuery(`DELETE FROM ${schema}.chat
|
||||||
|
WHERE
|
||||||
|
LOWER(uname)=LOWER($1)`, values, cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
deleteUserEmotes: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users && bot.cfg.db.useTables.emote_data) {
|
||||||
|
return runQuery(`DELETE FROM ${schema}.emote_data
|
||||||
|
WHERE
|
||||||
|
LOWER(uname)=LOWER($1)`, values, cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
deletePoll: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users && bot.cfg.db.useTables.saved_polls) {
|
||||||
|
return runQuery(`DELETE FROM ${schema}.saved_polls
|
||||||
|
WHERE
|
||||||
|
LOWER(savedby)=LOWER($1) AND poll_name=LOWER($2)`, values, cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
insertDuelRecord: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users && bot.cfg.db.useTables.duel_stats) {
|
||||||
|
return runQuery(`INSERT INTO ${schema}.duel_stats (uname, wins, losses)
|
||||||
|
VALUES ($1, 1, 0), ($2, 0, 1)
|
||||||
|
ON CONFLICT (uname)
|
||||||
|
DO
|
||||||
|
UPDATE SET
|
||||||
|
wins = excluded.wins + ${schema}.duel_stats.wins,
|
||||||
|
losses = excluded.losses + ${schema}.duel_stats.losses`, values, cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
insertPoll: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users && bot.cfg.db.useTables.saved_polls) {
|
||||||
|
return runQuery(`INSERT INTO ${schema}.saved_polls (savedby, poll_name, title, obscured, options)
|
||||||
|
VALUES ($1, LOWER($2), $3, $4, $5)
|
||||||
|
ON CONFLICT
|
||||||
|
DO NOTHING`, values, cb);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getPoll: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users && bot.cfg.db.useTables.saved_polls) {
|
||||||
|
return runQuery(`SELECT * FROM ${schema}.saved_polls
|
||||||
|
WHERE
|
||||||
|
poll_name=LOWER($1)`, values, cb);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getDuelRecord: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users && bot.cfg.db.useTables.duel_stats) {
|
||||||
|
return runQuery(`SELECT uname, wins, losses FROM ${schema}.duel_stats
|
||||||
|
WHERE
|
||||||
|
LOWER(uname)=LOWER($1);`, values, cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
getRandomChat: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users && bot.cfg.db.useTables.chat) {
|
||||||
|
return runQuery(`SELECT * FROM ${schema}.chat
|
||||||
|
WHERE
|
||||||
|
LOWER(uname)=LOWER($1)
|
||||||
|
OFFSET
|
||||||
|
floor(random()*(
|
||||||
|
SELECT count(*)
|
||||||
|
FROM ${schema}.chat
|
||||||
|
WHERE
|
||||||
|
LOWER(uname)=LOWER($1)))
|
||||||
|
LIMIT 1;`, values, cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
getUserRoomTime: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users) {
|
||||||
|
return runQuery(`SELECT first_seen, room_time, afk_time FROM ${schema}.users
|
||||||
|
WHERE
|
||||||
|
LOWER(uname)=LOWER($1);`, values, cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
getUserEmoteCount: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users && bot.cfg.db.useTables.emote_data) {
|
||||||
|
return runQuery(`SELECT uname, count FROM ${schema}.emote_data
|
||||||
|
WHERE
|
||||||
|
LOWER(uname)=LOWER($1) AND emote=$2`, values, cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
getUserTotalEmoteCount: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users && bot.cfg.db.useTables.emote_data) {
|
||||||
|
return runQuery(`SELECT uname, sum(count) FROM ${schema}.emote_data
|
||||||
|
WHERE
|
||||||
|
LOWER(uname)=LOWER($1)
|
||||||
|
GROUP BY uname`, values, cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
getEmoteTotalCount: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users && bot.cfg.db.useTables.emote_data) {
|
||||||
|
return runQuery(`SELECT sum(count) FROM ${schema}.emote_data
|
||||||
|
WHERE
|
||||||
|
emote=$1`, values, cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
getTopFiveEmotes: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users && bot.cfg.db.useTables.emote_data) {
|
||||||
|
var query = {
|
||||||
|
name: "topfiveemotes",
|
||||||
|
text:`SELECT emote, SUM(count) FROM ${schema}.emote_data
|
||||||
|
GROUP BY emote
|
||||||
|
ORDER BY sum
|
||||||
|
DESC
|
||||||
|
LIMIT 5`
|
||||||
|
};
|
||||||
|
return runQuery(query, values, cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
getTopFiveEmoteUsers: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users && bot.cfg.db.useTables.emote_data) {
|
||||||
|
return runQuery(`SELECT uname, sum(count) FROM ${schema}.emote_data
|
||||||
|
GROUP BY uname
|
||||||
|
ORDER BY sum
|
||||||
|
DESC
|
||||||
|
LIMIT 5;`, values, cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
getStoredEmotes: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users && bot.cfg.db.useTables.emote_data) {
|
||||||
|
return runQuery(`SELECT DISTINCT emote FROM ${schema}.emote_data`, values, cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
getLastSeen: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users) {
|
||||||
|
var query = {
|
||||||
|
text:`SELECT uname, last_seen FROM ${schema}.users
|
||||||
|
WHERE LOWER(uname)=LOWER($1)`
|
||||||
|
};
|
||||||
|
return runQuery(query, values, cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
updateEmoteCounts: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users && bot.cfg.db.useTables.emote_data) {
|
||||||
|
var query = {
|
||||||
|
text: `INSERT INTO ${schema}.emote_data (uname, emote, count)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (uname, emote)
|
||||||
|
DO
|
||||||
|
UPDATE SET
|
||||||
|
count = $3 + ${schema}.emote_data.count`
|
||||||
|
}
|
||||||
|
for (var i = 0; i < values.length; i++) {
|
||||||
|
if (values[i].length === 3 && values[i][2] > 0) {
|
||||||
|
var _cb = i >= values.length - 1 ? cb : null;
|
||||||
|
runQuery(query, values[i], _cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
updateUserRoomTime: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users) {
|
||||||
|
runQuery(`UPDATE ${schema}.users
|
||||||
|
SET
|
||||||
|
room_time = $1 + ${schema}.users.room_time,
|
||||||
|
afk_time = $2 + ${schema}.users.afk_time,
|
||||||
|
last_seen = NOW()
|
||||||
|
WHERE
|
||||||
|
uname = $3`, values, cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
updateUserAfkTime: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users) {
|
||||||
|
values[0] /= 1000;
|
||||||
|
runQuery(`UPDATE ${schema}.users
|
||||||
|
SET
|
||||||
|
afk_time = $1 + ${schema}.users.afk_time
|
||||||
|
WHERE
|
||||||
|
uname = $2`, values, cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
updateUserRoomTimeAll: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users) {
|
||||||
|
//[username, roomtime, afktime]
|
||||||
|
var query = {
|
||||||
|
text: `UPDATE ${schema}.users
|
||||||
|
SET
|
||||||
|
room_time = $2 + ${schema}.users.room_time,
|
||||||
|
afk_time = $3 + ${schema}.users.afk_time,
|
||||||
|
last_seen = NOW()
|
||||||
|
WHERE
|
||||||
|
uname = $1`
|
||||||
|
}
|
||||||
|
for (var i = 0; i < values.length; i++) {
|
||||||
|
var _cb = i >= values.length - 1 ? cb : null;
|
||||||
|
runQuery(query, values[i], _cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
updateUserLastSeen: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users) {
|
||||||
|
return runQuery(`UPDATE ${schema}.users
|
||||||
|
SET
|
||||||
|
last_seen = NOW()
|
||||||
|
WHERE
|
||||||
|
uname = ANY ($1);`, [values], cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
updateUserBlacklistState: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users) {
|
||||||
|
return runQuery(`UPDATE ${schema}.users
|
||||||
|
SET
|
||||||
|
blacklisted = $2
|
||||||
|
WHERE
|
||||||
|
LOWER(uname) = LOWER($1);`, values, cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
userJoin: function(values, cb) {
|
||||||
|
if (bot.cfg.db.useTables.users) {
|
||||||
|
return runQuery(`INSERT INTO ${schema}.users (uname)
|
||||||
|
VALUES ($1)
|
||||||
|
ON CONFLICT (uname)
|
||||||
|
DO UPDATE SET joins = ${schema}.users.joins + 1, last_seen = NOW()
|
||||||
|
RETURNING joins;`, values, cb);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.run = function(query, values, cb) {
|
||||||
|
if (!bot.cfg.db.use) return Promise.resolve(false);
|
||||||
|
return new Promise((resolve)=>{
|
||||||
|
if (queries.hasOwnProperty(query)) {
|
||||||
|
resolve(queries[query](values, cb));
|
||||||
|
} else resolve(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function genericHandler(err, res, cb) {
|
||||||
|
if (err) {
|
||||||
|
bot.logger.error(err.stack);
|
||||||
|
if (err.code === "ECONNREFUSED") {
|
||||||
|
disableDB(bot, "Connection to PostgreSQL refused, disabling database");
|
||||||
|
}
|
||||||
|
} else if (cb) cb();
|
||||||
|
}
|
||||||
|
|
||||||
|
function runQuery(query, values, cb) {
|
||||||
|
var released = false;
|
||||||
|
return pool.connect().then(client=>{
|
||||||
|
return client.query(query, values).then(res=>{
|
||||||
|
client.release();
|
||||||
|
released = true;
|
||||||
|
if (cb) cb(res);
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
.catch(e=>{
|
||||||
|
if (!released) client.release();
|
||||||
|
bot.logger.error(e.stack);
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(e=>{
|
||||||
|
bot.logger.error(e.stack);
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disableDB(bot, errText) {
|
||||||
|
await module.exports.endPool();
|
||||||
|
module.exports["endPool"] = function(){};
|
||||||
|
module.exports["active"] = false;
|
||||||
|
module.exports["run"] = function(){return Promise.resolve(false)};
|
||||||
|
bot.cfg.db.use = false;
|
||||||
|
bot.logger.error(errText);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports["init"] = init;
|
||||||
47
lib/discordbot.js
Normal file
47
lib/discordbot.js
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
const C = require("cli-color");
|
||||||
|
const Discord = require("discord.js");
|
||||||
|
|
||||||
|
const utils = require("./utils.js");
|
||||||
|
const strings = require("./strings.js");
|
||||||
|
|
||||||
|
function DiscordBot(bot, token) {
|
||||||
|
this.client = new Discord.Client();
|
||||||
|
|
||||||
|
this.client.on("ready", ()=>{
|
||||||
|
bot.logger.log(C.greenBright(strings.format(bot, "DISCORD_READY")));
|
||||||
|
});
|
||||||
|
|
||||||
|
/*this.client.on("shardDisconnect", (ev, shardID)=>{
|
||||||
|
bot.logger.log(C.redBright("Discord Bot disconnected! [" + shardID + "]"));
|
||||||
|
});*/
|
||||||
|
|
||||||
|
/*this.client.on("shardReconnecting", (shardID)=>{
|
||||||
|
bot.logger.log(C.yellowBright("Discord Bot reconnecting... [" + shardID + "]"));
|
||||||
|
});*/
|
||||||
|
|
||||||
|
this.client.on("error", (err)=>{
|
||||||
|
bot.logger.error(C.red("Discord error: " + err));
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.login(token);
|
||||||
|
|
||||||
|
this.lastNowPlayingWasGreen = false;
|
||||||
|
|
||||||
|
this.createEmbed = function() { return new Discord.MessageEmbed()
|
||||||
|
.setFooter("https://"+bot.cfg.connection.hostname+"/r/" + bot.CHANNEL.room + " • " + utils.getUTCTimestamp() + " UTC", bot.cfg.discord.iconUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DiscordBot.prototype.getChannel = function(id) {
|
||||||
|
return this.client.channels.cache.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
init: function(bot, token) {
|
||||||
|
if (token.trim() === "") {
|
||||||
|
bot.logger.error(strings.format(bot, "DISCORD_ERR_INIT", ["no token given"]));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new DiscordBot(bot, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
1148
lib/eventhandlers.js
Normal file
1148
lib/eventhandlers.js
Normal file
File diff suppressed because it is too large
Load diff
287
lib/strings.js
Normal file
287
lib/strings.js
Normal file
|
|
@ -0,0 +1,287 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const C = require("cli-color");
|
||||||
|
|
||||||
|
//Define strings here, with the string ID as the key and the actual string as the value.
|
||||||
|
//Positional params are notated by %s# and begin at 0.
|
||||||
|
//Use the exported "format" function to retrieve these strings.
|
||||||
|
|
||||||
|
//e.g. "UNKNOWN_COMMAND" : "Unknown command %s0."
|
||||||
|
var strings = {
|
||||||
|
ACTIONQUEUE_INIT_INTERVAL:"Action queue interval set to %s0ms.",
|
||||||
|
ANAGRAM_BAD_LENGTH:"Anagram: Input must be between %s0-%s1 characters",
|
||||||
|
ANAGRAM_RESULT:"[%s0] => %s1",
|
||||||
|
API_ERROR:"APIcall error with %s0: %s1",
|
||||||
|
API_NOT_FOUND:"Tried to call an undefined API %s0",
|
||||||
|
API_NOT_OK:"API %s0 returned status code %s1: %s2",
|
||||||
|
API_PLAIN_RESPONSE:"[%s0] %s1",
|
||||||
|
API_TIMEOUT:"API request to \"%s0\" timed out.",
|
||||||
|
API_WR_RESPONSE: "[wolfram] %s0: %s1",
|
||||||
|
API_YT_COMMENTSDISABLED: "Comments are disabled on this video.",
|
||||||
|
API_YT_ERROR:"Error with YouTube API (%s0, status %s1): %s2",
|
||||||
|
API_YT_NOCOMMENTS: "No comments found on this video.",
|
||||||
|
AVATAR_BLACKLIST:"Your profile picture is hosted by a blacklisted domain. Please use a different host for it.",
|
||||||
|
BLACKLIST_FAIL:"%s0 is already blacklisted, or the input was invalid.",
|
||||||
|
BLACKLIST_MSG:"%s0 is a blacklisted video.",
|
||||||
|
BLACKLIST_REMOVE_FAIL:"%s0 was not found in the blacklist.",
|
||||||
|
BLACKLIST_REMOVE_SUCCESS:"%s0 successfully removed from the blacklist.",
|
||||||
|
BLACKLIST_SUCCESS:"%s0 successfully blacklisted.",
|
||||||
|
BLACKLIST_USER:"You are currently blacklisted from adding videos.",
|
||||||
|
BUMP_LOG:"%s0: %s1 added by %s2, bumped by %s3 (%s4 => %s5)",
|
||||||
|
BOT_MUTED:"%s0 muted the bot.",
|
||||||
|
BOT_MUTED_INIT:"Bot is starting muted!",
|
||||||
|
BOT_UNMUTED:"%s0 unmuted the bot.",
|
||||||
|
CALLBACK_INVALID:"%s0: callback is not a function!",
|
||||||
|
CHAT_BLANK_MESSAGE:"Tried to send a blank message",
|
||||||
|
CHAT_EC_USED: "<EC> %s0 has been used %s1 %s2.",
|
||||||
|
CHAT_EC_NOTUSED: "<EC> %s0 has not been used before.",
|
||||||
|
CHAT_EIGHTBALL: "[8ball: %s0] %s1",
|
||||||
|
CHAT_LS_INROOM: "%s0 is in the room right now.",
|
||||||
|
CHAT_LS_LASTSEEN: "%s0 was last seen at %s1.",
|
||||||
|
CHAT_LS_NOTSEEN: "%s0 has not been seen in the room yet.",
|
||||||
|
CHAT_QUOTE: "[%s0] <%s1> %s2",
|
||||||
|
CHAT_QUOTE_ME: "[%s0] <%s1 %s2>",
|
||||||
|
CHAT_ROOMTIME: "%s0: First seen at %s1. Total room time: %s2; active time: %s3 (%s4\%)",
|
||||||
|
CHAT_ROOMTIME_ONLYSEEN: "%s0: First seen at %s1. No room time recorded, most likely due to a reset.",
|
||||||
|
CHAT_UEC_USED: "<UserEC> %s0 has used %s1 %s2 %s3.",
|
||||||
|
CHAT_UEC_USEDTOTAL: "<UserEC> %s0 has used %s1 %s2.",
|
||||||
|
CHAT_UEC_NONEUSED: "<UserEC> %s0 has not used any emotes.",
|
||||||
|
CHAT_UEC_NOTUSED: "<UserEC> %s0 has not used %s1 before.",
|
||||||
|
CHAT_UPTIME: "Uptime: %s0",
|
||||||
|
CLI_INPUT: "[CLI] %s0",
|
||||||
|
CLI_NOT_ACCEPTING_INPUT:"Bot is not yet accepting CLI input, please wait",
|
||||||
|
CLI_ACCEPTING_INPUT:"Now accepting CLI input",
|
||||||
|
CHANNEL_NOT_REGISTERED:"This channel is not registered to a CyTube account. Some of CyTube's features within this channel will not be available. You can claim channels on CyTube via the Channels page of your account. If this was unexpected, make sure the correct channel is set within the bot's config.",
|
||||||
|
CHANNEL_OPTS_CHANGED:"Channel options changed: %s0",
|
||||||
|
CHANNEL_PERMS_UPDATED:"Channel permissions updated.",
|
||||||
|
CHANLOG_ERROR:"Error reading channel log.",
|
||||||
|
CHANLOG_READING:"Reading channel log...",
|
||||||
|
CHANLOG_WRITTEN:"Channel log written to chan.log.",
|
||||||
|
CHATFILTER_UPDATE:"Chat filter \"%s0\" has been added/updated.",
|
||||||
|
CHATFILTER_DELETE:"Chat filter \"%s0\" has been deleted.",
|
||||||
|
COLORNAME_NONAME:"utils.colorUsername called without a username",
|
||||||
|
COMMAND_ATTEMPT:"%s0 attempted chat command: %s1",
|
||||||
|
COMMAND_CHANPERM_FAIL:"Could not execute chat command \"%s0\" due to either an invalid permission name or insufficient permissions for \"%s1\". If this should have worked, double check the permission name.",
|
||||||
|
COMMAND_CREATING:"Creating chat commands...",
|
||||||
|
COMMAND_DISABLED:"Chat command \"%s0\" disabled.",
|
||||||
|
COMMAND_DISABLED_FAIL:"Tried to disable chat command \"%s0\" but it is already disabled.",
|
||||||
|
COMMAND_ENABLED:"Chat command \"%s0\" enabled.",
|
||||||
|
COMMAND_ENABLED_BROKEN:"Tried to enable chat command \"%s0\" but it is broken. Make sure it is correctly written.",
|
||||||
|
COMMAND_ENABLED_FAIL:"Tried to enable chat command \"%s0\" but it is already enabled.",
|
||||||
|
COMMAND_INACTIVE:"Chat command \"%s0\" is starting inactive.",
|
||||||
|
COMMAND_INVALID:"Chat command \"%s0\" has invalid properties and will not work.",
|
||||||
|
COMMAND_INVALID_UNEQUAL_ID:"Chat command \"%s0\"'s key name does not match its cmdName (make it lowercase!) and it will be considered broken!",
|
||||||
|
COMMAND_LISTENING:"Chat commands created, now listening for commands",
|
||||||
|
COMMAND_USED_BEFOREHANDLING:"Chat command \"%s0\" was used before handling commands",
|
||||||
|
COMMAND_USED_BROKEN:"Chat command \"%s0\" was used, but it is broken. Make sure the command has valid properties.",
|
||||||
|
COMMAND_USED_INACTIVE:"Chat command \"%s0\" was used, but it is inactive.",
|
||||||
|
COMMAND_USED_NOPM:"Chat command \"%s0\" was used in private, but that command cannot be used in PM.",
|
||||||
|
CONNECT_ERROR:"Unable to connect: %s0",
|
||||||
|
CONNECT_SUCCESS:C.greenBright("Successfully connected to %s0!"),
|
||||||
|
CONNECTING:C.yellowBright("Connecting..."),
|
||||||
|
CURRENTLY_PLAYING:C.cyan("Currently playing via ") + "%s0" + C.cyan(":") + " %s1 %s2 %s3 %s4",
|
||||||
|
CUSTOMCOMMANDS_ALIASES_NOT_OBJ:"Could not load custom command aliases: expected the aliases in an object.",
|
||||||
|
CUSTOMCOMMANDS_ALIASES_OVERWRITE:"Overwriting existing command alias with custom alias: %s0",
|
||||||
|
CUSTOMCOMMANDS_CMDS_NOT_OBJ:"Could not load custom commands: expected the commands in an object.",
|
||||||
|
CUSTOMCOMMANDS_LOAD_ERROR:"Error loading custom commands: %s0",
|
||||||
|
CUSTOMCOMMANDS_NOT_FOUND:"Could not load custom commands: customchatcommands.js not found. Rename customchatcommands-example.js in the lib folder to use the template. Ignore if not using custom commands.",
|
||||||
|
CUSTOMCOMMANDS_OVERWRITE:"Overwriting existing command with custom definition: %s0",
|
||||||
|
CY_ANNOUNCEMENT:"%s0 :: %s1 \u2014%s2",
|
||||||
|
DB_BAD_INFO:"Error creating database pool. Check your credential configuration.",
|
||||||
|
DB_EMOTES_CLEANED:"%s0: Emote records have been cleaned of unused emotes.",
|
||||||
|
DB_EMOTES_CLEANED_NONE:"%s0: No unused emotes were found in the database.",
|
||||||
|
DB_EMOTES_ERASED:"Emotes erased. You may also use \"%s0quotes off\" to exempt yourself from quotes and emote records. Keep in mind that this bot may still log chat among other room events.",
|
||||||
|
DB_QUOTES_ERASED:"Quotes erased. You may also use \"%s0quotes off\" to exempt yourself from quotes and emote records. Keep in mind that this bot may still log chat among other room events.",
|
||||||
|
DB_QUOTES_ERASED_OTHER:"%s0's quotes erased.",
|
||||||
|
DISCIPLINE_LOG:"%s0 used %s1 on %s2. Reason: %s3",
|
||||||
|
DISCONNECTED:C.redBright("Disconnected from server."),
|
||||||
|
DISCORD_ERR_INIT:"Error initiating Discord bot: %s0.",
|
||||||
|
DISCORD_EMBED_CLOSE_POLL_AUTHOR:":: poll closed ::",
|
||||||
|
DISCORD_EMBED_NEW_POLL_AUTHOR:":: poll opened ::",
|
||||||
|
DISCORD_EMBED_POLL_INPROGRESS_AUTHOR:":: poll in progress ::",
|
||||||
|
DISCORD_EMBED_POLL_TIMESTAMP:"Started by %s0 at %s1 UTC",
|
||||||
|
DISCORD_READY:"Discord Bot ready!",
|
||||||
|
DUEL_BEGIN:"%s0: %s1 challenged you to a duel! Type \`%s2%s3\`, or \`%s2%s4\` like a pussy...",
|
||||||
|
DUEL_DECLINE:"%s0 declined %s1's duel! What a bitch! %s2",
|
||||||
|
DUEL_EXPIRED:"%s0's duel request has expired due to %s1 being a little bitch.",
|
||||||
|
DUEL_PM_INDUEL:"That user is already in a duel.",
|
||||||
|
DUEL_PM_CALLERWAITING:"You're waiting for %s0 to respond to your duel request!",
|
||||||
|
DUEL_PM_TARGETWAITING:"%s0 is waiting for you to respond to their duel request!",
|
||||||
|
DUEL_RECORD:"%s0's duel record: %s1W-%s2L, win rate: %s3",
|
||||||
|
DUEL_RESULT_LOSS:"%s1 WINS against %s0! [%s3 vs %s2] %s4",
|
||||||
|
DUEL_RESULT_WIN:"%s0 WINS against %s1! [%s2 vs %s3] %s4",
|
||||||
|
DUEL_USER_LEFT:"%s0 left the room; pending duel with %s1 has ended.",
|
||||||
|
EMOTE_REMOVE:"Emote \"%s0\" removed.",
|
||||||
|
EMOTE_RENAME:"Emote \"%s0\" renamed to \"%s1\".",
|
||||||
|
EMOTE_REJECTED:"%s0: Rejecting invalid emote: %s1",
|
||||||
|
EMOTE_UPDATE:"Emote \"%s0\" added/updated.",
|
||||||
|
EXIT:"Bot stopped, exiting in %s0. Reason: %s1",
|
||||||
|
FILE_READ_ERROR:"Error reading from %s0: %s1",
|
||||||
|
FILE_WRITE_ERROR:"Error writing to %s0: %s1",
|
||||||
|
INIT:"-- Initializing %s0 v%s1 --",
|
||||||
|
INVALID_USERNAME:"That username is invalid.",
|
||||||
|
JOINING_ROOM:"Joining %s0",
|
||||||
|
KICKED:"You have been kicked.%s0",
|
||||||
|
KILL_GENERIC_DISCONNECT:"disconnected",
|
||||||
|
KILL_WRONG_PWD:"wrong password",
|
||||||
|
LEADER_GIVEN:"Leader given to %s0",
|
||||||
|
LEADER_NOPERM:"The bot does not have permission to make itself a leader. Make it one before using this command.",
|
||||||
|
LEADER_REMOVED:"Leader removed from %s0",
|
||||||
|
LOGIN_SUCCESS:"Successfully logged in as: %s0",
|
||||||
|
MEDIA_ADD_BOTTOM:"%s0 added %s1 to the bottom of the playlist",
|
||||||
|
MEDIA_ADD_POS:"%s0 added %s1 to #%s2",
|
||||||
|
MEDIA_ADD_TOP:"%s0 added %s1 to the top of the playlist",
|
||||||
|
MEDIA_MOVE_AFTER:"%s0 moved after %s1 (#%s2 -> #%s3)",
|
||||||
|
MEDIA_MOVE_TOP:"%s0 moved to the top of the playlist (#%s1 -> #%s2)",
|
||||||
|
MEMORY_USAGE:"Memory usage: %s0%s1",
|
||||||
|
MOTD_CHANGED:"The channel's MOTD has been changed.",
|
||||||
|
NEW_POLL:"%s0 opened a poll at %s1: %s2 %s3",
|
||||||
|
NEW_USER_CHAT:"Your account is too new to chat in this channel. Please wait a while and try again.",
|
||||||
|
NEW_USER_CHAT_LINK:"Your account is too new to post links in this channel. Please wait a while and try again.",
|
||||||
|
NEW_USER_JOIN:"New user: %s0",
|
||||||
|
NEW_USERS:"New users: %s0",
|
||||||
|
NO_ABORT:"Type /exit instead of using CTRL-C to exit the bot.",
|
||||||
|
NO_FLOOD:C.redBright("%s0: %s1"),
|
||||||
|
NO_USERNAME:"NO_USERNAME",
|
||||||
|
NO_VIDEO:"No video is playing.",
|
||||||
|
NOW_PLAYING:C.yellowBright("Now playing via ") + "%s0" + C.yellowBright(":") + " %s1 %s2 %s3",
|
||||||
|
PARTITION_CHANGE:"Reconnecting due to a partition change...",
|
||||||
|
PERMISSION_INSUFFICIENT:"Insufficient permission (rank %s2) for %s0; need rank %s1.",
|
||||||
|
PERMISSION_NOT_FOUND:"Tried to check permission %s0 but it wasn't found!",
|
||||||
|
PLAYLIST_EMPTY:"The playlist is empty.",
|
||||||
|
PLAYLIST_IS_LOCKED:"The playlist is currently " + C.redBright("locked") + ".",
|
||||||
|
PLAYLIST_INVALID_POSITION:"Invalid playlist position.",
|
||||||
|
PLAYLIST_LOCKED:"Playlist locked.",
|
||||||
|
PLAYLIST_LOW:"Playlist time is running low. Add videos!",
|
||||||
|
PLAYLIST_RECEIVED:"Playlist data received.",
|
||||||
|
PLAYLIST_IS_UNLOCKED:"The playlist is currently " + C.greenBright("unlocked") + ".",
|
||||||
|
PLAYLIST_UNLOCKED:"Playlist unlocked.",
|
||||||
|
PLAYLIST_VIDEONOTFOUND:"Video not found.",
|
||||||
|
PM_RECV:"Received PM from %s0: %s1",
|
||||||
|
PM_SENT:"Sent PM to %s0: %s1",
|
||||||
|
POLL_CLOSED:"%s0's poll closed: %s1 %s2",
|
||||||
|
POLL_CLOSED_RESULT:"\"%s0\": %s1 had the most votes with %s2 vote(s) (%s3)",
|
||||||
|
POLL_CLOSED_TIE:"\"%s0\": Some options tied with %s1 vote(s) each (%s2)",
|
||||||
|
POLL_CLOSED_CMD:"%s0 ended the poll via chat command.",
|
||||||
|
PWD_ACCEPTED:"Room password accepted!",
|
||||||
|
PWD_REQUIRED:"Room %s0 requires a password, attempting to join...",
|
||||||
|
QUEUE_FAIL:"Queue failure: %s0",
|
||||||
|
QUEUE_WARN:"Queue warning: %s0",
|
||||||
|
QUOTE_NO_ARG_PM:"Currently %s0 from quotes and chat/emote storage (does not include channel logs). Type \"%s1%s2 on\" or \"%s1%s2 off\" to control this, or \"%s1clearquotes\" or \"%s1clearemotecount\" to erase your stored messages or emote records respectively (except those in logs).",
|
||||||
|
QUOTE_OFF_PM:"You're now exempt from %s0quote and any further messages and emotes will not be stored (except normal channel logging). Type \"%s0quotes on\" to enable this again, or \"%s0clearquotes\" or \"%s0clearemotecount\" to erase any of your stored messages or emote records respectively (again, does not erase logs).",
|
||||||
|
QUOTE_ON_PM:"Your chat messages and emotes may now be stored and then retrieved when users use %s0quote and emote count commands. Type \"%s0quotes off\" to exempt yourself from this.",
|
||||||
|
RANK_SET:"Rank set to %s0.",
|
||||||
|
RANK_SET_USER:"Rank for %s0 set to %s1",
|
||||||
|
SAVEPOLL_ERR_NOACTIVE:"There must be a poll active with at least a title or some options.",
|
||||||
|
SAVEPOLL_ERR_STARTSWITHTIME:"Poll name cannot begin with \"time:\", because loadpoll uses this for the timer.",
|
||||||
|
SAVEPOLL_ERR_NAMELENGTH:"Poll name must be %s0-%s1 characters.",
|
||||||
|
SAVEPOLL_ERR_OPTLENGTH:"Poll must have %s0 or less options.",
|
||||||
|
SAVEPOLL_ERR_NOTUNIQUE:"Could not save the poll. There may be a poll with the same name, or the same title and options.",
|
||||||
|
SAVEPOLL_SUCCESS:"Poll saved as: %s0",
|
||||||
|
SEEK_TOOFAR:"Can't seek past the length of the video.",
|
||||||
|
SELFPURGE_NOARG:"Did you mean \"%s0selfremove\"? If not, try \"%s0selfpurge\" again with nothing after it.",
|
||||||
|
SELFPURGE_SEMISUCCESS:"Your videos have been purged. However, the current video was not deleted.",
|
||||||
|
SELFPURGE_SUCCESS:"Your videos have been purged.",
|
||||||
|
SELFREMOVE_ERR_ACTIVE:"You may not remove your video if it is playing.",
|
||||||
|
SELFREMOVE_ERR_NOTYOURS:"Did not remove %s0: You may only remove videos that you have added.",
|
||||||
|
SELFREMOVE_SUCCESS:"Removed %s0",
|
||||||
|
SELFREMOVE_USAGE:"%s0%s1 <video position number|first|last> - removes the video at the given position (or first or last video found if specified) if added by you",
|
||||||
|
SERVER_BAD_HOSTNAME:"Server found, but it was not on %s0. Stopping.",
|
||||||
|
SERVER_INSECURE:"config.connection.secureServer is false! Bot will use an INSECURE and UNENCRYPTED server!",
|
||||||
|
SERVER_REQUEST_ERROR:"Failure requesting socket configuration: %s0",
|
||||||
|
SERVER_ROOM_NOT_FOUND:"getSocketConfig: unable to find a server for room %s0",
|
||||||
|
SETTINGS_PROPERTY_MISSING:"Loaded settings file does not have property %s0, updating",
|
||||||
|
SETTINGS_READ:"Reading persistent settings",
|
||||||
|
SETTINGS_WRITE:"Writing persistent settings",
|
||||||
|
SHUFFLE_ERR:"The shuffle command can only be used without any other text in the message. Did you mean \"%s0shuffleuser\"?",
|
||||||
|
SKIPRATE_CHANGE:"Skip ratio changed from %s0\% to %s1\%",
|
||||||
|
SOCKET_CONN_ERR:"Unable to connect to the socket server.",
|
||||||
|
SPAM_FILTERED:"Spam filtered.",
|
||||||
|
STOPALLTIMERS_FAIL:"stopAllTimers cannot be called if the bot has not been killed, unless forcefully done so!",
|
||||||
|
SUBNET_MATCH:"%s0's subnet matches banned IPs: %s1",
|
||||||
|
TARGETUSER_EXEMPT: "That user does not allow chat record retrieval.",
|
||||||
|
TIMEBAN_NONE:"A timeban was not found for %s0.",
|
||||||
|
TIMEBAN_SOON:"%s0 is timebanned but will be automatically unbanned within a minute or so.",
|
||||||
|
TIMEBAN_TIME:"%s0 has %s1 left on their ban.",
|
||||||
|
TIMECODE_BADFORMAT:"Invalid time. Must be in [H:]M:S format.",
|
||||||
|
TRIGGER_INVALID:"Invalid trigger found. Reverted trigger to \"%s0\"",
|
||||||
|
UNABLE_TO_LOGIN:"Unable to login. Check your credentials. Error: %s0",
|
||||||
|
UNBAN_FAIL_TIMEBANNED:"%s0 is timebanned. Use %s1untimeban instead, or %s1gettimeban to see the ban length.",
|
||||||
|
UNBANNED:"%s0 unbanned (id: %s1, ip: %s2)",
|
||||||
|
UNKNOWN_CHAT_COMMAND:"Unknown chat command: %s0",
|
||||||
|
UNKNOWN_CLI_COMMAND:"Unknown console command \"%s0\".",
|
||||||
|
USER_ALLOWED:"Allowed %s0",
|
||||||
|
USER_ALLOWED_FAIL:"Tried to allow %s0 but they are not disallowed.",
|
||||||
|
USER_DISALLOWED:"Disallowed %s0",
|
||||||
|
USER_DISALLOWED_FAIL:"Tried to disallow %s0 but they are already disallowed.",
|
||||||
|
USER_JOINED_ROOM:C.green("+ ") + "%s0" + C.green(" joined the room."),
|
||||||
|
USER_JOINED_ROOM_ALIASES:C.green("+ ") + "%s0" + C.green(" joined the room. ") + C.blackBright("(aliases: %s1)"),
|
||||||
|
USER_LEFT_ROOM:C.red("- ") + "%s0" + C.red(" left the room."),
|
||||||
|
USER_PURGED:"Purged %s0's videos.",
|
||||||
|
WRONG_PWD:"Wrong password for room %s0! If not done already, please set the room password within the config.",
|
||||||
|
|
||||||
|
COMMAND_RANK_CHANGED:"Rank for chat command %s0 changed from %s1=>%s2 by %s3.",
|
||||||
|
COMMAND_RANKMATCH_CHANGED:"Rank match for chat command %s0 changed from %s1 to %s2 by %s3.",
|
||||||
|
COMMAND_REQUIRED_RANK:"Required rank for %s0: %s2%s1",
|
||||||
|
COMMAND_RUNTIME_ERROR:"That command encountered a runtime error. Tell the bot maintainer.",
|
||||||
|
COOLDOWN_C_ACTIVE:"Command cooldown for %s0 is still active. %s1 seconds remaining.",
|
||||||
|
COOLDOWN_U_ACTIVE:"User cooldown for %s0 is still active. %s1 seconds remaining.",
|
||||||
|
|
||||||
|
DBG_CMD_CHECKOVERRIDE:"Checking for chat command property overrides",
|
||||||
|
DBG_CMD_FOUNDCDOVERRIDE:"Found user cooldown override for chat command %s0 (%s1 => %s2)",
|
||||||
|
DBG_CMD_FOUNDGCDOVERRIDE:"Found global cooldown override for chat command %s0 (%s1 => %s2)",
|
||||||
|
DBG_CMD_FOUNDRANKOVERRIDE:"Found rank override for chat command %s0 (%s1 => %s2)",
|
||||||
|
DBG_CMD_FOUNDRANKOVERRIDE_NOTALLOWED:"Found rank override for chat command %s0 but it does not allow rank changes. Skipping.",
|
||||||
|
DBG_CMD_FOUNDRANKMATCHOVERRIDE:"Found rankmatch override for chat command %s0 (%s1 to %s2)",
|
||||||
|
DBG_CMD_FOUNDRANKMATCHOVERRIDE_NOTALLOWED:"Found rankmatch override for chat command %s0 but it does not allow rankmatch changes. Skipping.",
|
||||||
|
DBG_CMD_FOUNDSTATEOVERRIDE:"Found active state override for chat command %s0 (%s1 => %s2)",
|
||||||
|
DBG_CMD_SETCDPROP:"Setting %s0 property in cooldown obj",
|
||||||
|
DBG_FOUND_SOCKET:"Found socket config info, connecting to %s0",
|
||||||
|
DBG_REMOVEUID:"Removed uid:%s0",
|
||||||
|
DBG_SETCURRENT:"setCurrent called with uid:%s0",
|
||||||
|
DBG_SETTING_HANDLERS:"Setting socket event handlers",
|
||||||
|
|
||||||
|
RECV_BANLIST:"Ban list received.",
|
||||||
|
RECV_RANKS:"Rank list received.",
|
||||||
|
RECV_USERLIST:"User list received."
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
//format: Takes a stringID (the string key) and an array of strings as parameters.
|
||||||
|
//params may be left null if the requested string does not take parameters.
|
||||||
|
"format":function(bot, stringID, params) {
|
||||||
|
if (bot && bot.pendingLanguageChange) {
|
||||||
|
try {
|
||||||
|
if (bot.pendingLanguageChange.length < 2 || bot.pendingLanguageChange.length > 3) {
|
||||||
|
if (bot.pendingLanguageChange === "reset") {
|
||||||
|
let file = require("./strings.js");
|
||||||
|
strings = file._strings;
|
||||||
|
} else
|
||||||
|
throw new Error("Language code must be 2-3 chars!");
|
||||||
|
} else {
|
||||||
|
let file = require("./strings-" + bot.pendingLanguageChange + ".js");
|
||||||
|
strings = file._strings;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code === "MODULE_NOT_FOUND"){}
|
||||||
|
else
|
||||||
|
bot.logger.error(e.stack);
|
||||||
|
}
|
||||||
|
bot.pendingLanguageChange = null;
|
||||||
|
}
|
||||||
|
if (strings.hasOwnProperty(stringID)) {
|
||||||
|
if (params === null || params === undefined) return strings[stringID];
|
||||||
|
return strings[stringID].replace(/\%s(\d+)/g, function(match, capture) {
|
||||||
|
var num = parseInt(capture);
|
||||||
|
if (!isNaN(num) && num < params.length)
|
||||||
|
return params[num];
|
||||||
|
else
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
bot.logger.error("strings.format: Requested stringID " + stringID + " but it is not defined!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"_strings":strings
|
||||||
|
}
|
||||||
792
lib/utils.js
Normal file
792
lib/utils.js
Normal file
|
|
@ -0,0 +1,792 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const C = require("cli-color");
|
||||||
|
|
||||||
|
const strings = require("./strings.js");
|
||||||
|
var spaceReg = new RegExp("\\s+", "gm");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object of public functions for performing various miscellaneous stuff
|
||||||
|
* @namespace utils
|
||||||
|
*/
|
||||||
|
var utils = module.exports = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colors a media title based on its source. Requires colorMediaTitles to be enabled.
|
||||||
|
* @memberof utils
|
||||||
|
* @param {!Bot} bot Bot object
|
||||||
|
* @param {!string} type Media type (host abbreviation)
|
||||||
|
* @param {!string} title Title to color
|
||||||
|
* @return {string} Colored title
|
||||||
|
*/
|
||||||
|
colorMediaTitle: function(bot, type, title) {
|
||||||
|
if (bot.cfg.interface.colorMediaTitles) {
|
||||||
|
switch (type) {
|
||||||
|
case "yt":
|
||||||
|
return C.redBright(title);
|
||||||
|
case "li":
|
||||||
|
case "vi":
|
||||||
|
return C.blueBright(title);
|
||||||
|
case "dm":
|
||||||
|
return C.yellowBright(title);
|
||||||
|
case "sc":
|
||||||
|
return C.yellow(title);
|
||||||
|
case "tw":
|
||||||
|
case "tc":
|
||||||
|
return C.magentaBright(title);
|
||||||
|
case "im":
|
||||||
|
return C.greenBright(title);
|
||||||
|
case "us":
|
||||||
|
case "hb":
|
||||||
|
return C.blue(title);
|
||||||
|
case "gd":
|
||||||
|
case "sb":
|
||||||
|
return C.cyanBright(title);
|
||||||
|
default:
|
||||||
|
return C.whiteBright(title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return title;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colors a username based on the user's rank. Requires colorUsernames.
|
||||||
|
* @memberof utils
|
||||||
|
* @param {!Bot} bot Bot object
|
||||||
|
* @param {!string|Object} username Username or user object to color
|
||||||
|
* @return {string|Object} Colored username, or object if given and invalid
|
||||||
|
*/
|
||||||
|
colorUsername: function(bot, user) {
|
||||||
|
if (!user) {
|
||||||
|
bot.logger.error(strings.format(bot, "COLORNAME_NONAME"));
|
||||||
|
return strings.format(bot, "NO_USERNAME");
|
||||||
|
}
|
||||||
|
if (bot.cfg.interface.colorUsernames) {
|
||||||
|
let _user = null;
|
||||||
|
if (typeof user === "string") {
|
||||||
|
if (user.indexOf("[") >= 0)
|
||||||
|
return C[bot.cfg.interface.rankColors.server](user);
|
||||||
|
else if (user === "(anon)")
|
||||||
|
return C[bot.cfg.interface.rankColors.anonymous](user);
|
||||||
|
_user = bot.getUser(user);
|
||||||
|
if (!_user) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
} else if (utils.isObject(user) && user.hasOwnProperty("name") && user.hasOwnProperty("rank")) {
|
||||||
|
_user = user;
|
||||||
|
}
|
||||||
|
if (!_user) return strings.format(bot, "NO_USERNAME");
|
||||||
|
if (_user.rank < 1 || (_user.meta && _user.meta.guest))
|
||||||
|
return C[bot.cfg.interface.rankColors.unregistered](_user.name);
|
||||||
|
else if (_user.rank >= 1 && bot.cfg.interface.rankColors.hasOwnProperty(_user.rank)) {
|
||||||
|
if (_user.rank >= bot.RANKS.SITEOWNER)
|
||||||
|
return C.magentaBright(_user.name);
|
||||||
|
else
|
||||||
|
return C[bot.cfg.interface.rankColors[_user.rank]](_user.name);
|
||||||
|
}
|
||||||
|
return C.whiteBright(_user.name);
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares two arrays and checks if the contents are identical. Order does not matter.
|
||||||
|
* @memberof utils
|
||||||
|
* @param {!any[]} arrA Array A
|
||||||
|
* @param {!any[]} arrB Array B
|
||||||
|
* @return {boolean} True if arrays are the same, false otherwise.
|
||||||
|
*/
|
||||||
|
compareArrays: function(arrA, arrB) {
|
||||||
|
if (Array.isArray(arrA) && Array.isArray(arrB)) {
|
||||||
|
if (arrA.length === arrB.length) {
|
||||||
|
for (var i = 0; i < arrA.length; i++) {
|
||||||
|
var eq = false;
|
||||||
|
if (Array.isArray(arrA[i])) {
|
||||||
|
for (var j = 0; j < arrB.length && !eq; j++) {
|
||||||
|
if (Array.isArray(arrB[j])) {
|
||||||
|
if (utils.compareArrays(arrA[i], arrB[j])) eq = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!eq) return false;
|
||||||
|
} else if (utils.isObject(arrA[i])) {
|
||||||
|
for (var j = 0; j < arrB.length && !eq; j++) {
|
||||||
|
if (utils.isObject(arrB[j])) {
|
||||||
|
if (utils.compareObjects(arrA[i], arrB[j])) eq = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!eq) return false;
|
||||||
|
} else if (!~arrB.indexOf(arrA[i])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares two objects and checks if contents are identical.
|
||||||
|
* @memberof utils
|
||||||
|
* @param {!Object} objA Object A
|
||||||
|
* @param {!Object} objB Object B
|
||||||
|
* @return {boolean} True if objects are the same, false otherwise.
|
||||||
|
*/
|
||||||
|
compareObjects: function(objA, objB) {
|
||||||
|
if (utils.isObject(objA) && utils.isObject(objB)) {
|
||||||
|
if (Object.keys(objA).length !== Object.keys(objB).length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (var i in objA) {
|
||||||
|
if (objB.hasOwnProperty(i)) {
|
||||||
|
//if values are arrays
|
||||||
|
if (Array.isArray(objA[i]) && Array.isArray(objB[i])) {
|
||||||
|
if (!utils.compareArrays(objA[i], objB[i])) return false;
|
||||||
|
}
|
||||||
|
//if values are objects
|
||||||
|
else if (utils.isObject(objA[i]) && utils.isObject(objB[i])) {
|
||||||
|
if (!utils.compareObjects(objA[i], objB[i])) return false;
|
||||||
|
}
|
||||||
|
//otherwise if unequal
|
||||||
|
else if (objA[i] !== objB[i])
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return utils.compareArrays(objA, objB);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colors emotes in a message for the CLI. If enabled and uname is given, inserts the usage into the database.
|
||||||
|
* From CyTube's source.
|
||||||
|
* {@link https://github.com/calzoneman/sync/blob/f081bc782adba074052884995b90bc77dcef3338/www/js/util.js#L2730}
|
||||||
|
* @memberof utils
|
||||||
|
* @param {!Bot} bot Bot object
|
||||||
|
* @param {!string} msg Entire message
|
||||||
|
* @param {?string=} uname Sender's username
|
||||||
|
* @return {string} Message with colored emotes
|
||||||
|
*/
|
||||||
|
execEmotes: function (bot, msg, uname) {
|
||||||
|
if (!bot.cfg.chat.parseEmotes) {
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
if (uname) uname = C.strip(uname);
|
||||||
|
var count = 0,
|
||||||
|
noLimit = bot.cfg.chat.maxEmotes < 0,
|
||||||
|
counts = {};
|
||||||
|
function foundEmote(name) {
|
||||||
|
count++;
|
||||||
|
if (!noLimit && count > bot.cfg.chat.maxEmotes) {
|
||||||
|
return C.blackBright(name);
|
||||||
|
} else {
|
||||||
|
countEmote(name);
|
||||||
|
}
|
||||||
|
return C.cyan(name);
|
||||||
|
}
|
||||||
|
function countEmote(emote) {
|
||||||
|
if (!counts.hasOwnProperty(emote)) counts[emote] = 1;
|
||||||
|
else counts[emote] += 1;
|
||||||
|
}
|
||||||
|
bot.CHANNEL.badEmotes.forEach(function (e) {
|
||||||
|
msg = msg.replace(e.regex, function (){
|
||||||
|
return foundEmote(e.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
msg = msg.replace(/[^\s]+/gi, function (m) {
|
||||||
|
var _m = m.toLowerCase();
|
||||||
|
if (bot.CHANNEL.emoteMap.hasOwnProperty(_m)) {
|
||||||
|
var e = bot.CHANNEL.emoteMap[_m];
|
||||||
|
return foundEmote(e.name);
|
||||||
|
} else {
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (count > 0 && uname && bot.db && bot.cfg.db.useTables.users && bot.cfg.db.useTables.emote_data && !bot.getSavedUserData(uname).quoteExempt && Object.keys(counts).length > 0) {
|
||||||
|
var values = [];
|
||||||
|
for (var i in counts) {
|
||||||
|
values.push([uname, i, counts[i]]);
|
||||||
|
}
|
||||||
|
bot.db.run("updateEmoteCounts", values, function() {
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a full link from a media type and ID, or a shortened link.
|
||||||
|
* Most of this comes from CyTube's source.
|
||||||
|
* {@link https://github.com/calzoneman/sync/blob/db48104b80f5713e33e91badb82626fe1ea278b7/src/utilities.js#L180}
|
||||||
|
* @memberof utils
|
||||||
|
* @param {number} id Media ID
|
||||||
|
* @param {string} type Media type
|
||||||
|
* @param {?boolean=} short If true, will shorten the media source to type:id
|
||||||
|
* @return {string} Full or shortened link
|
||||||
|
*/
|
||||||
|
formatLink: function (id, type, short) {
|
||||||
|
if (!type || !id) return "";
|
||||||
|
if (short) return type + ":" + id;
|
||||||
|
switch (type) {
|
||||||
|
case "yt":
|
||||||
|
return "https://youtu.be/" + id;
|
||||||
|
case "vi":
|
||||||
|
return "https://vimeo.com/" + id;
|
||||||
|
case "dm":
|
||||||
|
return "https://dailymotion.com/video/" + id;
|
||||||
|
case "sc":
|
||||||
|
return id;
|
||||||
|
case "li":
|
||||||
|
return "https://livestream.com/" + id;
|
||||||
|
case "tw":
|
||||||
|
return "https://twitch.tv/" + id;
|
||||||
|
case "rt":
|
||||||
|
return id;
|
||||||
|
case "im":
|
||||||
|
return "https://imgur.com/a/" + id;
|
||||||
|
case "us":
|
||||||
|
return "https://ustream.tv/channel/" + id;
|
||||||
|
case "gd":
|
||||||
|
return "https://docs.google.com/file/d/" + id;
|
||||||
|
case "fi":
|
||||||
|
return id;
|
||||||
|
case "hb":
|
||||||
|
return "https://www.smashcast.tv/" + id;
|
||||||
|
case "hl":
|
||||||
|
return id;
|
||||||
|
case "sb":
|
||||||
|
return "https://streamable.com/" + id;
|
||||||
|
case "tc":
|
||||||
|
return "https://clips.twitch.tv/" + id;
|
||||||
|
case "cm":
|
||||||
|
return id;
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the hostname from a URL.
|
||||||
|
* @memberof utils
|
||||||
|
* @param {!string} link Full link
|
||||||
|
* @return {string} Hostname
|
||||||
|
*/
|
||||||
|
getHostname: function(link) {
|
||||||
|
var matches = link.match(/^(?:https?\:\/\/)(?:.+?\.)*?([^\.\/]*?\.[^\.\/]*?)(?:[\:\/]|$)/i);
|
||||||
|
if (!matches) return "";
|
||||||
|
return matches[1];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current time (or provided time) as a timestamp.
|
||||||
|
* @memberof utils
|
||||||
|
* @param {?boolean} twentyfour If true, uses 24h time instead of AM/PM
|
||||||
|
* @param {?number=} time Epoch time in milliseconds
|
||||||
|
* @return {string} Timestamp
|
||||||
|
*/
|
||||||
|
getTimestamp: function(twentyfour, time) {
|
||||||
|
var date = time ? new Date(time) : new Date(),
|
||||||
|
now = {
|
||||||
|
M: date.getMonth()+1,
|
||||||
|
D: date.getDate(),
|
||||||
|
Y: date.getFullYear().toString().substr(-2),
|
||||||
|
h: date.getHours(),
|
||||||
|
m: date.getMinutes(),
|
||||||
|
s: date.getSeconds(),
|
||||||
|
P: "a"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (twentyfour) {
|
||||||
|
now.P = "";
|
||||||
|
} else {
|
||||||
|
if (now.h >= 12) {
|
||||||
|
now.P = "p";
|
||||||
|
if (now.h > 12)
|
||||||
|
now.h -= 12;
|
||||||
|
} else if (now.h === 0) {
|
||||||
|
now.h = 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now.m < 10) now.m = "0" + now.m;
|
||||||
|
if (now.s < 10) now.s = "0" + now.s;
|
||||||
|
|
||||||
|
return "[" + now.M + "/" + now.D + "/" + now.Y + " " + now.h + ":" + now.m + ":" + now.s + now.P + "]";
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a date string using the current UTC time, or a provided time.
|
||||||
|
* @memberof utils
|
||||||
|
* @see {@link getUTCTimeStringFromDate}
|
||||||
|
* @see {@link getUTCTimestamp}
|
||||||
|
* @param {?number=} date Epoch time in milliseconds
|
||||||
|
* @return {string} Date string
|
||||||
|
*/
|
||||||
|
getUTCDateStringFromDate: function(date) {
|
||||||
|
date = date ? new Date(date) : new Date();
|
||||||
|
return date.toUTCString().split(" ").splice(1,3).join(" ");
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a date and time string using the current UTC time or a provided time.
|
||||||
|
* @memberof utils
|
||||||
|
* @see {@link getUTCDateStringFromDate}
|
||||||
|
* @see {@link getUTCTimestamp}
|
||||||
|
* @param {?number=} date Epoch time in milliseconds
|
||||||
|
* @return {string} Date+time string
|
||||||
|
*/
|
||||||
|
getUTCTimeStringFromDate: function(date) {
|
||||||
|
date = date ? new Date(date) : new Date();
|
||||||
|
return date.toUTCString().split(" ").splice(1,4).join(" ") + " UTC";
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a simple time string using the current UTC time or a provided time.
|
||||||
|
* @memberof utils
|
||||||
|
* @see {@link getUTCDateStringFromDate}
|
||||||
|
* @see {@link getUTCTimeStringFromDate}
|
||||||
|
* @param {?number=} time Epoch time in milliseconds
|
||||||
|
* @return {string} Time string
|
||||||
|
*/
|
||||||
|
getUTCTimestamp: function(time) {
|
||||||
|
var now = time ? new Date(time) : new Date();
|
||||||
|
return now.getUTCHours() + ":" +
|
||||||
|
(now.getUTCMinutes() > 9 ? now.getUTCMinutes() : "0" + now.getUTCMinutes()) + ":" +
|
||||||
|
(now.getUTCSeconds() > 9 ? now.getUTCSeconds() : "0" + now.getUTCSeconds())
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a string contains the beginning of a valid inline command.
|
||||||
|
* @memberof utils
|
||||||
|
* @param {!string} trig Bot trigger
|
||||||
|
* @param {!string} str Message
|
||||||
|
* @return {string} Empty if no match, or everything after the match
|
||||||
|
*/
|
||||||
|
inlineCmdCheck:function(trig, str) {
|
||||||
|
let i = str.indexOf("\:\:" + trig);
|
||||||
|
if (~i) {
|
||||||
|
return str.substr(i + 2 + trig.length);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if something is an Object and not an array.
|
||||||
|
* @memberof utils
|
||||||
|
* @param {*} item Input to check
|
||||||
|
* @return {boolean} True if object, false otherwise
|
||||||
|
*/
|
||||||
|
isObject: function(item) {
|
||||||
|
return (item instanceof Object && !Array.isArray(item));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a whole string is a number (int or decimal), but doesn't parse it.
|
||||||
|
* @memberof utils
|
||||||
|
* @param {!string} num Number string
|
||||||
|
* @return {boolean} True if pattern matched, otherwise false
|
||||||
|
*/
|
||||||
|
isNumber:function(num) {
|
||||||
|
return /^\d+(?:\.(?:\d+)?)?$/.test(num);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a username is valid according to CyTube. Code from CyTube.
|
||||||
|
* {@link https://github.com/calzoneman/sync/blob/f081bc782adba074052884995b90bc77dcef3338/src/utilities.js#L10}
|
||||||
|
* @memberof utils
|
||||||
|
* @param {!string} name Username
|
||||||
|
* @return {boolean} True if username is valid, false otherwise
|
||||||
|
*/
|
||||||
|
isValidUserName: function(name) {
|
||||||
|
return name.match(/^[\w-]{1,20}$/);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a string is in an IPv4 format, but very loosely. Octets can be >255
|
||||||
|
* @memberof utils
|
||||||
|
* @param {!string} str IP string
|
||||||
|
* @return {boolean} True if IPv4, false otherwise
|
||||||
|
*/
|
||||||
|
looseIPTest:function(str) {
|
||||||
|
return /(\d{1,3}\.){3}\d{1,3}/.test(str);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a boolean from different datatypes.
|
||||||
|
* @memberof utils
|
||||||
|
* @param {boolean|number|string} bool Input to be parsed
|
||||||
|
* @return {boolean|null} True/false if valid input. Null otherwise
|
||||||
|
*/
|
||||||
|
parseBool: function(bool) {
|
||||||
|
if (typeof bool === "boolean") return bool;
|
||||||
|
if (typeof bool !== "string") {
|
||||||
|
if (typeof bool === "number") return !!bool;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
//check these individually and explicitly because null lets us know if the input is bad
|
||||||
|
if (bool.toLowerCase() === "true") return true;
|
||||||
|
if (bool.toLowerCase() === "false") return false;
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given image link matches one of the specified link patterns. Used for validation.
|
||||||
|
* @memberof utils
|
||||||
|
* @param {string} str Input link
|
||||||
|
* @return {boolean|string} False if not HTTPS, a string, or a valid pattern; otherwise returns the URL without the protocol
|
||||||
|
*/
|
||||||
|
parseImageLink: function(str) {
|
||||||
|
if (typeof str !== "string") return false;
|
||||||
|
str = str.trim();
|
||||||
|
|
||||||
|
if (str.toLowerCase().indexOf("https://") !== 0) return false;
|
||||||
|
|
||||||
|
let exp = [
|
||||||
|
/*discord*/ /(?:cdn\.discordapp\.com|media\.discordapp\.net)\/attachments\/\d+\/\d+\/[^\s\0\\\/\:\*\?\"\<\>\|]+\.(?:jpe?g|gif|png|webp)/gi,
|
||||||
|
/*4chan*/ /i(?:s)?(?:\d)?\.(?:4cdn|4chan)\.org\/\w{1,6}\/\d{8,15}\.(?:jpe?g|gif|png|webp)/gi,
|
||||||
|
/*gyazo*/ /i\.gyazo\.com\/\w+\.(?:jpe?g|gif|png|webp)/gi,
|
||||||
|
/*tumblr*/ /(?:\d+\.)?(?:static|media)\.tumblr\.com\/(?:\w+\/)*tumblr_\w+(?:\_\w+)?\.(?:jpe?g|gif|png|webp)/gi,
|
||||||
|
/*puush*/ /puu\.sh\/\w+\/\w+\.(?:jpe?g|gif|png|webp)/gi,
|
||||||
|
/*leddit*/ /i\.redd\.it\/\w+\.(?:jpe?g|gif|png|webp)/gi,
|
||||||
|
/*gfycat*/ /giant\.gfycat\.com\/\w+\.gif/gi
|
||||||
|
];
|
||||||
|
|
||||||
|
let match = null,
|
||||||
|
i = 0;
|
||||||
|
|
||||||
|
for (;i < exp.length;i++) {
|
||||||
|
match = str.match(exp[i]);
|
||||||
|
if (match) return "https://" + match[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a data object to be used with queueing videos.
|
||||||
|
* Directly from CyTube source.
|
||||||
|
* {@link https://github.com/calzoneman/sync/blob/bd63013524d06f25258aab054d150325a4b91e10/www/js/util.js#L1287}
|
||||||
|
* @memberof utils
|
||||||
|
* @param {string} url Media URL
|
||||||
|
* @return {Object|null} Data object, or null if parsing failed
|
||||||
|
*/
|
||||||
|
parseMediaLink: function(url) {
|
||||||
|
if(typeof url != "string") {
|
||||||
|
return {
|
||||||
|
id: null,
|
||||||
|
type: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
url = url.trim();
|
||||||
|
url = url.replace("feature=player_embedded&", "");
|
||||||
|
|
||||||
|
//this also comes from CyTube
|
||||||
|
function extractQueryParam(query, param) {
|
||||||
|
var params = {};
|
||||||
|
query.split("&").forEach(function (kv) {
|
||||||
|
kv = kv.split("=");
|
||||||
|
params[kv[0]] = kv[1];
|
||||||
|
});
|
||||||
|
|
||||||
|
return params[param];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(url.indexOf("rtmp://") == 0) {
|
||||||
|
return {
|
||||||
|
id: url,
|
||||||
|
type: "rt"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var m;
|
||||||
|
if((m = url.match(/youtube\.com\/watch\?([^#]+)/))) {
|
||||||
|
return {
|
||||||
|
id: extractQueryParam(m[1], "v"),
|
||||||
|
type: "yt"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// YouTube shorts
|
||||||
|
if((m = url.match(/youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/))) {
|
||||||
|
return {
|
||||||
|
id: m[1],
|
||||||
|
type: "yt"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if((m = url.match(/youtu\.be\/([^\?&#]+)/))) {
|
||||||
|
return {
|
||||||
|
id: m[1],
|
||||||
|
type: "yt"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if((m = url.match(/youtube\.com\/playlist\?([^#]+)/))) {
|
||||||
|
return {
|
||||||
|
id: extractQueryParam(m[1], "list"),
|
||||||
|
type: "yp"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((m = url.match(/clips\.twitch\.tv\/([A-Za-z]+)/))) {
|
||||||
|
return {
|
||||||
|
id: m[1],
|
||||||
|
type: "tc"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// #790
|
||||||
|
if ((m = url.match(/twitch\.tv\/(?:.*?)\/clip\/([A-Za-z]+)/))) {
|
||||||
|
return {
|
||||||
|
id: m[1],
|
||||||
|
type: "tc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if((m = url.match(/twitch\.tv\/(?:.*?)\/([cv])\/(\d+)/))) {
|
||||||
|
return {
|
||||||
|
id: m[1] + m[2],
|
||||||
|
type: "tv"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2017-02-23
|
||||||
|
* Twitch changed their URL pattern for recorded videos, apparently.
|
||||||
|
* https://github.com/calzoneman/sync/issues/646
|
||||||
|
*/
|
||||||
|
if((m = url.match(/twitch\.tv\/videos\/(\d+)/))) {
|
||||||
|
return {
|
||||||
|
id: "v" + m[1],
|
||||||
|
type: "tv"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if((m = url.match(/twitch\.tv\/([\w-]+)/))) {
|
||||||
|
return {
|
||||||
|
id: m[1],
|
||||||
|
type: "tw"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if((m = url.match(/livestream\.com\/([^\?&#]+)/))) {
|
||||||
|
return {
|
||||||
|
id: m[1],
|
||||||
|
type: "li"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if((m = url.match(/ustream\.tv\/([^\?&#]+)/))) {
|
||||||
|
return {
|
||||||
|
id: m[1],
|
||||||
|
type: "us"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((m = url.match(/(?:hitbox|smashcast)\.tv\/([^\?&#]+)/))) {
|
||||||
|
return {
|
||||||
|
id: m[1],
|
||||||
|
type: "hb"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if((m = url.match(/vimeo\.com\/([^\?&#]+)/))) {
|
||||||
|
return {
|
||||||
|
id: m[1],
|
||||||
|
type: "vi"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if((m = url.match(/dailymotion\.com\/video\/([^\?&#_]+)/))) {
|
||||||
|
return {
|
||||||
|
id: m[1],
|
||||||
|
type: "dm"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if((m = url.match(/soundcloud\.com\/([^\?&#]+)/))) {
|
||||||
|
return {
|
||||||
|
id: url,
|
||||||
|
type: "sc"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((m = url.match(/(?:docs|drive)\.google\.com\/file\/d\/([a-zA-Z0-9_-]+)/)) ||
|
||||||
|
(m = url.match(/drive\.google\.com\/open\?id=([a-zA-Z0-9_-]+)/))) {
|
||||||
|
return {
|
||||||
|
id: m[1],
|
||||||
|
type: "gd"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((m = url.match(/(.*\.m3u8)/))) {
|
||||||
|
return {
|
||||||
|
id: url,
|
||||||
|
type: "hl"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if((m = url.match(/streamable\.com\/([\w-]+)/))) {
|
||||||
|
return {
|
||||||
|
id: m[1],
|
||||||
|
type: "sb"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shorthand URIs */
|
||||||
|
// So we still trim DailyMotion URLs
|
||||||
|
if((m = url.match(/^dm:([^\?&#_]+)/))) {
|
||||||
|
return {
|
||||||
|
id: m[1],
|
||||||
|
type: "dm"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Raw files need to keep the query string
|
||||||
|
if ((m = url.match(/^fi:(.*)/))) {
|
||||||
|
return {
|
||||||
|
id: m[1],
|
||||||
|
type: "fi"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if ((m = url.match(/^cm:(.*)/))) {
|
||||||
|
return {
|
||||||
|
id: m[1],
|
||||||
|
type: "cm"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Generic for the rest.
|
||||||
|
if ((m = url.match(/^([a-z]{2}):([^\?&#]+)/))) {
|
||||||
|
return {
|
||||||
|
id: m[2],
|
||||||
|
type: m[1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Raw file */
|
||||||
|
var tmp = url.split("?")[0];
|
||||||
|
if (tmp.match(/^https?:\/\//)) {
|
||||||
|
if (tmp.match(/\.json$/)) {
|
||||||
|
// Custom media manifest format
|
||||||
|
return {
|
||||||
|
id: url,
|
||||||
|
type: "cm"
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Assume raw file (server will check)
|
||||||
|
return {
|
||||||
|
id: url,
|
||||||
|
type: "fi"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//null if parsing didn't work
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces all whitespace in a string with a single space, including newlines and zero-width spaces
|
||||||
|
* @memberof utils
|
||||||
|
* @param {!string} str Input string
|
||||||
|
* @return {string} String without excess whitespace
|
||||||
|
*/
|
||||||
|
removeExcessWhitespace: function(str) {
|
||||||
|
return str.replace(spaceReg, " ");
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loosely removes anything encased with < and >
|
||||||
|
* @memberof utils
|
||||||
|
* @param {!string} str Input string
|
||||||
|
* @return {string} String without tags
|
||||||
|
*/
|
||||||
|
removeHtmlTags: function(str) {
|
||||||
|
return str.replace(/(\<.+?\>)+/gi, "");
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a number into a timecode.
|
||||||
|
* @memberof utils
|
||||||
|
* @param {?number} num The input to convert
|
||||||
|
* @param {boolean=} letters If true, will output with dhms instead of :
|
||||||
|
* @return {string} Timecode string
|
||||||
|
*/
|
||||||
|
secsToTime: function(num, letters) {
|
||||||
|
if (undefined==num) num = 0;
|
||||||
|
else num = Math.floor(num);
|
||||||
|
var days = Math.floor(num/(3600*24));
|
||||||
|
var hours = Math.floor(num/3600) % 24;
|
||||||
|
var minutes = Math.floor(num / 60) % 60;
|
||||||
|
var seconds = num % 60;
|
||||||
|
|
||||||
|
if (hours < 10 && days > 0)
|
||||||
|
hours = "0" + hours;
|
||||||
|
|
||||||
|
if (minutes < 10 && (hours > 0 || days > 0))
|
||||||
|
minutes = "0" + minutes;
|
||||||
|
|
||||||
|
if (seconds < 10)
|
||||||
|
seconds = "0" + seconds;
|
||||||
|
|
||||||
|
var time = "";
|
||||||
|
if (letters) {
|
||||||
|
if (days != 0)
|
||||||
|
time += days + "d";
|
||||||
|
if (hours != 0)
|
||||||
|
time += hours + "h";
|
||||||
|
if (minutes != 0)
|
||||||
|
time += minutes + "m";
|
||||||
|
if (seconds != 0)
|
||||||
|
time += seconds + "s";
|
||||||
|
if (time === "") return "0s";
|
||||||
|
} else {
|
||||||
|
if (days != 0)
|
||||||
|
time += days + ":" + hours + ":";
|
||||||
|
else if (hours != 0)
|
||||||
|
time += hours + ":";
|
||||||
|
|
||||||
|
time += minutes + (letters ? "m" : ":") + seconds + (letters ? "s" : "");
|
||||||
|
}
|
||||||
|
return time;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts [H:]M:S to seconds. Used with user input, so -1 handles invalid input.
|
||||||
|
* @memberof utils
|
||||||
|
* @param {!string} timecode A timecode expected to consist of [H:]M:S
|
||||||
|
* @return {number} Returns -1 if invalid input, or amount of seconds represented by the timecode.
|
||||||
|
*/
|
||||||
|
timecodeToSecs: function(timecode) {
|
||||||
|
let matches = [...timecode.matchAll(/^(\d+\:)?(\d{1,2}\:)(\d{2})$/g)][0];
|
||||||
|
if (!matches) return -1;
|
||||||
|
let secs = 0;
|
||||||
|
if (matches[1]) secs += (parseInt(matches[1]) * 60 * 60);
|
||||||
|
if (matches[2]) secs += (parseInt(matches[2]) * 60);
|
||||||
|
if (matches[3]) secs += parseInt(matches[3]);
|
||||||
|
return secs;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cuts the first and last chars from a string.
|
||||||
|
* @memberof utils
|
||||||
|
* @param {!string} str Input string
|
||||||
|
* @return {string} Returns str without the first and last characters
|
||||||
|
*/
|
||||||
|
trimStringEnds: function(str) {
|
||||||
|
return str.substr(1, str.length - 2);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Removes an item from an unsorted array by putting the last item on the given index and reducing the array's length by 1. ONLY USE ON UNSORTED ARRAYS
|
||||||
|
* @memberof utils
|
||||||
|
* @param {any[]} arr Input array
|
||||||
|
* @param {number} i Index of item to remove
|
||||||
|
* @return {boolean} False if index out of bounds, otherwise true.
|
||||||
|
*/
|
||||||
|
unsortedRemove: function(arr, i) {
|
||||||
|
if (i < 0 || i >= arr.length) return false;
|
||||||
|
if (i < arr.length-1)
|
||||||
|
arr[i] = arr[arr.length-1];
|
||||||
|
arr.length--;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
logs/.gitignore
vendored
Normal file
3
logs/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
*
|
||||||
|
|
||||||
|
!.gitignore
|
||||||
19
package.json
Normal file
19
package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "chozobot",
|
||||||
|
"description": "CyTube client emulator/bot with moderation features and CLI input",
|
||||||
|
"main": "index.js",
|
||||||
|
"author": "deerfarce",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/deerfarce/ChozoBot.git"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"cli-color": "^2.0.0",
|
||||||
|
"discord.js": "^12.2.0",
|
||||||
|
"html-entities": "^1.3.1",
|
||||||
|
"node-fetch": "^2.6.0",
|
||||||
|
"pg": "^8.3.0",
|
||||||
|
"socket.io-client": "^2.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
settings-afterparty.json
Normal file
1
settings-afterparty.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"muted":false,"disallow":[],"timeBans":{"test":[]},"minRankOverrides":{},"rankMatchOverrides":{},"userCooldownOverrides":{},"cmdCooldownOverrides":{},"cmdStateOverrides":{"about":false,"duel":false,"acceptduel":false,"declineduel":false,"duelrecord":false,"echo":false,"img":false,"roll":false,"selfremove":false,"top5emotes":false,"top5emoteusers":false,"urbandictionary":false,"vidstats":false,"useremotecount":false,"wolfram":false},"userData":{},"mediaBlacklist":[],"userBlacklist":["remove"],"lucky":{},"flatSkiprate":{"managing":false,"target":-1,"original_rate":-1}}
|
||||||
1
settings.example.json
Normal file
1
settings.example.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"muted":false,"disallow":[],"timeBans":{},"minRankOverrides":{},"cmdCooldownOverrides":{},"userCooldownOverrides":{},"rankMatchOverrides":{},"userData":{},"mediaBlacklist":[],"userBlacklist":[],"lucky":{},"flatSkiprate":{"managing":false,"target":-1,"original_rate":-1}}
|
||||||
28
start.bat
Normal file
28
start.bat
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
@echo off
|
||||||
|
REM This file was made a while ago and has not been tested since.
|
||||||
|
set starts=0
|
||||||
|
:bot
|
||||||
|
set /a starts=starts+1
|
||||||
|
cls
|
||||||
|
title ChozoBot - KeepAlive
|
||||||
|
echo Times started: %starts%
|
||||||
|
start /wait node . %*
|
||||||
|
if %errorlevel% EQU 1 (
|
||||||
|
color 0c
|
||||||
|
echo An unhandled error was thrown, most likely a syntax error. Run the bot without this script to see what the error is.
|
||||||
|
echo.
|
||||||
|
goto close
|
||||||
|
)
|
||||||
|
if %errorlevel% EQU 3 (
|
||||||
|
color 0c
|
||||||
|
echo The bot was killed without restarting.
|
||||||
|
echo.
|
||||||
|
goto close
|
||||||
|
)
|
||||||
|
|
||||||
|
timeout /t 2 /nobreak
|
||||||
|
goto bot
|
||||||
|
|
||||||
|
:close
|
||||||
|
pause
|
||||||
|
color
|
||||||
25
start.sh
Normal file
25
start.sh
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#!/bin/bash
|
||||||
|
starts=0
|
||||||
|
while true; do
|
||||||
|
starts=$((starts+1))
|
||||||
|
clear
|
||||||
|
echo Times started: $starts
|
||||||
|
node . "$@"
|
||||||
|
exitcode=$?
|
||||||
|
if [ $exitcode -eq 1 ]; then
|
||||||
|
echo "An unhandled error was thrown. If there was no error stack logged, run the bot without this script to see what the error is. Exit code: 1"
|
||||||
|
read -p "Press a key to exit."
|
||||||
|
exit 1
|
||||||
|
elif [ $exitcode -eq 3 ]; then
|
||||||
|
echo "Bot killed without restarting."
|
||||||
|
exit 3
|
||||||
|
elif [ $exitcode -ne 0 ]; then
|
||||||
|
echo "Unhandled exit code was returned: ${exitcode}"
|
||||||
|
read -p "Press a key to exit."
|
||||||
|
exit $exitcode
|
||||||
|
else
|
||||||
|
echo "Process closed with exit code 0."
|
||||||
|
fi
|
||||||
|
echo "Restarting in 2 seconds."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
Loading…
Reference in a new issue