├── .env.example ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── labeler.yml ├── pull_request_template.md └── workflows │ ├── code_formatting.yml │ └── label.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── bot ├── __main__.py ├── assets │ ├── .gitignore │ └── font.ttf ├── base.py ├── category.py ├── commands │ ├── binary.py │ ├── calc.py │ ├── changePresence │ │ ├── activity.py │ │ └── status.py │ ├── coderoles │ │ └── menu.py │ ├── desktoproles │ │ └── menu.py │ ├── distroroles │ │ ├── info.py │ │ └── menu.py │ ├── fun │ │ ├── gen_spam.py │ │ ├── gigachad.py │ │ └── pp.py │ ├── games │ │ ├── minesweeper.py │ │ └── tic_tac_toe.py │ ├── leet_code_problem.py │ ├── levels │ │ ├── __assets │ │ │ ├── card.png │ │ │ ├── dnd.png │ │ │ ├── font.ttf │ │ │ ├── idle.png │ │ │ ├── offline.png │ │ │ ├── online.png │ │ │ └── streaming.png │ │ ├── __box_generator.py │ │ ├── __level_generator.py │ │ ├── __uis.py │ │ ├── leaderboard.py │ │ ├── rank.py │ │ ├── resetbg.py │ │ ├── resetfontcolor.py │ │ ├── setbg.py │ │ ├── setfontcolor.py │ │ └── setrank.py │ ├── minecraft │ │ ├── players.py │ │ └── status.py │ ├── mod │ │ ├── removetag.py │ │ ├── report.py │ │ ├── request_close.py │ │ └── send.py │ ├── polls │ │ ├── create_multiple_option.py │ │ ├── create_truefalse_option.py │ │ └── result.py │ ├── reminders │ │ ├── add.py │ │ └── list.py │ ├── request │ │ ├── request.py │ │ ├── request_book.py │ │ └── request_status.py │ ├── tag │ │ ├── get.py │ │ ├── list.py │ │ └── set.py │ └── utility │ │ ├── about.py │ │ ├── avatar.py │ │ ├── embeds.py │ │ ├── help.py │ │ ├── member_count.py │ │ ├── ping.py │ │ └── version.py ├── config.py ├── events │ ├── on_member_join.py │ ├── on_message.py │ ├── on_raw_reaction_add.py │ └── on_voice_state.py ├── logger.py ├── manager.py ├── sql.py └── tasks │ ├── activity.py │ ├── check_members.py │ ├── reminders.py │ ├── run_fetch.py │ └── youtube.py ├── flake.lock ├── flake.nix ├── poetry.lock ├── pyproject.toml └── requirements.txt /.env.example: -------------------------------------------------------------------------------- 1 | TESTING_IGNORE_DB= 2 | TESTING_IGNORED_FILES= 3 | DISCOX_TOKEN= 4 | DISCOX_PREFIX= 5 | DISCOX_GENERAL_ID= 6 | DISCOX_REPORT_ID= 7 | DISCOX_MOD_ROLE_ID= 8 | DISCOX_TEMP_CHANNEL= 9 | DISCOX_CHANNEL_ID= 10 | DISCOX_ROLE_CHANNEL= 11 | DISCOX_YOUTUBE_ANNOUNCEMENT_ID= 12 | DISCOX_MYSQL_HOST= 13 | DISCOX_MYSQL_PORT= 14 | DISCOX_MYSQL_USER= 15 | DISCOX_MYSQL_PASSWORD= 16 | DISCOX_MYSQL_DATABASE= 17 | DISCOX_STARBOARD_CHANNEL= 18 | MINECRAFT_URL= 19 | MINECRAFT_PORT= 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Version [e.g. 22] 29 | 30 | **Smartphone (please complete the following information):** 31 | - Device: [e.g. iPhone6] 32 | - OS: [e.g. iOS8.1] 33 | - Version [e.g. 22] 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Our Discord server 4 | url: https://discord.gg/jJ8s5jQ6A5 5 | about: You can go to our discord server for small questions. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | 22 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | # Add 'Not Reviewed' label to any root file changes 2 | Not Reviewed: 3 | - '*' 4 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **What kind of change does this PR introduce?** 2 | (Bug fix, feature, docs update, ...) 3 | 4 | **Describe the changes proposed in the pull request** 5 | A quick description of the changes made to the repository 6 | 7 | **What is the current behavior?** 8 | (You can also link to an open issue here) 9 | 10 | **What is the new behavior** 11 | (if this is a feature change)? 12 | 13 | **Does this PR introduce a breaking change?** 14 | (What changes might users need to make in their application due to this PR?) 15 | 16 | **Does this PR introduce changes to the database?** 17 | (What has changed? Did you make sure to setup the create statements in `__main__.py`?) 18 | 19 | **Other information:** 20 | 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/code_formatting.yml: -------------------------------------------------------------------------------- 1 | name: Code formatting 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | permissions: write-all 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Install code beautifier 18 | run: make install-beautifier 19 | 20 | - name: Magik 21 | run: make beautify 22 | 23 | - name: Commit those back 24 | uses: EndBug/add-and-commit@v9.1.1 25 | with: 26 | message: "Beautified" 27 | author_name: "Code beautifier" 28 | -------------------------------------------------------------------------------- /.github/workflows/label.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Labeler" 2 | on: 3 | - pull_request_target 4 | 5 | jobs: 6 | triage: 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/labeler@v4 13 | with: 14 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | # Devenv 163 | .devenv* 164 | devenv.local.nix 165 | 166 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | POETRY_PYTHON_PATH = $(shell poetry env info --path) 2 | POETRY_PYTHON_PATH := $(subst ,,$(POETRY_PYTHON_PATH)) # remove spaces 3 | ifeq ($(OS),Windows_NT) 4 | # Windows 5 | PYTHON = $(addsuffix \Scripts\python.exe,$(POETRY_PYTHON_PATH)) 6 | else 7 | # Linux 8 | PYTHON = $(addsuffix /bin/python,$(POETRY_PYTHON_PATH)) 9 | endif 10 | 11 | ifeq (add,$(firstword $(MAKECMDGOALS))) 12 | RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) 13 | $(eval $(RUN_ARGS):;@:) 14 | endif 15 | 16 | ifeq (remove,$(firstword $(MAKECMDGOALS))) 17 | RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) 18 | $(eval $(RUN_ARGS):;@:) 19 | endif 20 | 21 | .PHONY: init run add remove beautify install-beautifier 22 | 23 | init: 24 | poetry install 25 | 26 | run: 27 | $(PYTHON) -m bot 28 | 29 | install-beautifier: 30 | @pip install black isort 31 | @echo "Successfully installed beautifier!" 32 | 33 | beautify: 34 | @black . 35 | @echo "Successfully beautified code!" 36 | @isort . 37 | @echo "Successfully sorted imports!" 38 | 39 | add: 40 | @echo "Adding new module..." 41 | @poetry add $(RUN_ARGS) 42 | @echo "Updating requirements.txt..." 43 | @poetry export -f requirements.txt --output requirements.txt --without-hashes 44 | @echo "Successfully added new module!" 45 | 46 | remove: 47 | @echo "Removing module..." 48 | @poetry remove $(RUN_ARGS) 49 | @echo "Updating requirements.txt..." 50 | @poetry export -f requirements.txt --output requirements.txt --without-hashes 51 | @echo "Successfully removed module!" 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua) 2 | 3 | # Discox 4 | 5 | Virbox Discord Bot community project ^\_^ 6 | 7 | Written in ~~blazingly fast~~ **effective** Python. 8 | 9 | Documentation and installation guide can be found at our [wiki](https://github.com/v1rbox/discox/wiki). 10 | 11 | ## Contributors & Authors 12 | 13 | [![contributors](https://contrib.rocks/image?repo=v1rbox/discox)](https://github.com/v1rbox/discox/graphs/contributors) 14 | -------------------------------------------------------------------------------- /bot/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import threading 4 | import traceback 5 | from typing import List, Tuple 6 | 7 | import discord 8 | 9 | from .config import Config, Embed 10 | from .logger import Logger 11 | from .manager import (CommandsManager, EventsManager, PoolingManager, 12 | TasksManager) 13 | from .sql import SQLParser 14 | 15 | logger = Logger() 16 | config = Config() 17 | 18 | CREATE_STATEMENTS = [ 19 | """ 20 | CREATE TABLE IF NOT EXISTS discox.polls ( 21 | channel_id BIGINT NOT NULL, 22 | message_id BIGINT NOT NULL PRIMARY KEY, 23 | type ENUM('single', 'multiple') 24 | ); 25 | """, 26 | """ 27 | CREATE TABLE IF NOT EXISTS discox.levels ( 28 | user_id VARCHAR(100) PRIMARY KEY, 29 | level INTEGER, 30 | exp INTEGER, 31 | font_color VARCHAR(25), 32 | bg VARCHAR(2048) DEFAULT NULL 33 | ); 34 | """, 35 | """ 36 | CREATE TABLE IF NOT EXISTS discox.latest_video ( 37 | video_id VARCHAR(50) PRIMARY KEY 38 | ); 39 | """, 40 | """ 41 | CREATE TABLE IF NOT EXISTS discox.request ( 42 | Number_id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, 43 | Member_id VARCHAR(100) NOT NULL, 44 | Title VARCHAR(255) NOT NULL, 45 | Description VARCHAR(2048) NOT NULL, 46 | Upvote INTEGER NOT NULL, 47 | Downvote INTEGER NOT NULL, 48 | Pending_close INTEGER NOT NULL 49 | ); 50 | """, 51 | """ 52 | CREATE TABLE IF NOT EXISTS discox.reminders ( 53 | id INTEGER AUTO_INCREMENT PRIMARY KEY, 54 | User VARCHAR(100), 55 | Timestamp INTEGER, 56 | Reminder VARCHAR(2048), 57 | Channel VARCHAR(100), 58 | Message VARCHAR(100) 59 | ); 60 | """, 61 | """ 62 | CREATE TABLE IF NOT EXISTS discox.membercount ( 63 | membercount INT PRIMARY KEY 64 | ); 65 | """, 66 | """ 67 | CREATE TABLE IF NOT EXISTS discox.starboard ( 68 | message_id VARCHAR(100) PRIMARY KEY, 69 | board_message_id VARCHAR(100) 70 | ); 71 | """, 72 | """ 73 | CREATE TABLE IF NOT EXISTS discox.tags ( 74 | Name VARCHAR(100) UNIQUE PRIMARY KEY, 75 | Content VARCHAR(2048) 76 | ); 77 | """, 78 | ] 79 | 80 | 81 | def parse_user_input(user_input: str) -> Tuple[str, List[str]]: 82 | """Parse user input 83 | 84 | [Args]: 85 | user_input (str): user input 86 | 87 | [Returns]: 88 | command_name (str): the command name 89 | args (List[str]): command args 90 | """ 91 | 92 | command_name, *args = user_input.split() 93 | args = [ 94 | tuple(group for group in tpl if group)[0] 95 | for tpl in re.findall( 96 | r'"([^"]+)"|\'([^\']+)\'|\`\`\`([^\']+)\`\`\`|(\S+)', " ".join(args) 97 | ) 98 | ] 99 | 100 | return command_name, args 101 | 102 | 103 | # TODO for 'args', do a struct 'Argument' with a 'required' bool field 104 | async def parse_usage_text( 105 | usage: str, args: List[str], message: discord.Message 106 | ) -> bool: 107 | """Get the usage of a command into a more workable format 108 | 109 | [Args]: 110 | usage (str): command usage (help) 111 | args (List[str]): command arguments 112 | message (discord.Message): discord message 113 | 114 | [Returns]: 115 | (bool): True if all went good, False otherwise 116 | """ 117 | 118 | required: List[str] = re.findall(f'<([^"]+)>', usage) 119 | optional: List[str] = re.findall(f'\[([^"]+)\]', usage) 120 | 121 | usage_args: List[Tuple[str, str]] = re.findall( 122 | f"\[([^\[\]]+)\]|\<([^\<\>]+)\>", usage 123 | ) 124 | args_raw: List[str] = [f"<{i[1]}>" if i[1] else f"[{i[0]}]" for i in usage_args] 125 | 126 | # Check for missing required arguments 127 | if len(args) < len(required): 128 | missing = required[len(args)] 129 | indx = usage.index(missing) 130 | errmsg = f"{config.prefix}{usage}\n{' '*(indx+len(config.prefix)-1)}{'^'*(len(missing)+2)}" 131 | 132 | embed = Embed( 133 | title="Error in command syntax", 134 | description=f"Missing required argument '{missing}'\n```{errmsg}```", 135 | ) 136 | embed.set_color("red") 137 | await message.channel.send(embed=embed) 138 | return False 139 | 140 | # Check if the given amount of arguments exceeds the expected amount 141 | if len(args) > len(required) + len(optional) and args_raw[-1][1] != "*": 142 | embed = Embed( 143 | title="Error in command syntax", 144 | description=f"Expected `{len(required) + len(optional)}` argument{'s' if len(required) + len(optional) > 1 else ''} but got `{len(args)}`.\nCommand usage: ```{config.prefix}{usage}```", 145 | ) 146 | embed.set_color("red") 147 | await message.channel.send(embed=embed) 148 | return False 149 | 150 | return True 151 | 152 | 153 | async def match_type(type: str, arg: str, message: discord.Message) -> any: 154 | match type: 155 | case "int": 156 | if arg.removeprefix("-").isdigit(): 157 | return int(arg) 158 | else: 159 | await logger.send_error(f"'{arg}' is not an integer.", message) 160 | raise ValueError() 161 | 162 | case "float": 163 | if re.match("^-?\d+(?:\.\d+)$", arg) is not None: 164 | return float(arg) 165 | else: 166 | await logger.send_error(f"'{arg}' is not a float.", message) 167 | raise ValueError() 168 | 169 | case "bool": 170 | if arg.lower() == "true": 171 | return True 172 | elif arg.lower() == "false": 173 | return False 174 | else: 175 | await logger.send_error(f"'{arg}' is not a boolean.", message) 176 | raise ValueError() 177 | 178 | case "member": 179 | # temp fix 180 | if arg == "": 181 | arg = str(message.author.id) 182 | try: 183 | user = message.guild.get_member_named(arg) 184 | assert user is not None 185 | except AssertionError: 186 | try: 187 | user = await message.guild.fetch_member(arg) 188 | except (discord.NotFound, discord.HTTPException, discord.Forbidden): 189 | try: 190 | user = await message.guild.fetch_member(message.mentions[0].id) 191 | except IndexError: 192 | await logger.send_error( 193 | f"The user `{arg}` was not found.\nNote: This command is case sensitive. E.g use `Virbox#2050` instead of `virbox#2050`.", 194 | message, 195 | ) 196 | raise ValueError() 197 | except ( 198 | discord.NotFound, 199 | discord.HTTPException, 200 | discord.Forbidden, 201 | ): 202 | logger.send_error( 203 | f"The user `{arg}` was not found.\nNote: This command is case sensitive. E.g use `Virbox#2050` instead of `virbox#2050`.", 204 | message, 205 | ) 206 | raise ValueError() 207 | return user 208 | case _: 209 | return arg 210 | 211 | 212 | async def parse_types( 213 | usage: str, arguments: List[str], message: discord.Message 214 | ) -> List: 215 | required_args: List[str] = re.findall("\<(.*?)\>", usage) 216 | optional_args: List[str] = re.findall("\[(.*?)\]", usage) 217 | usage_args = [*required_args, *optional_args] 218 | 219 | args_raw: List[str] = re.findall("\[.+\]|<.+>", usage) 220 | usage_types = [] 221 | for i in usage_args: 222 | if i[1] is None: 223 | usage_types.append([i[0], "str"]) 224 | else: 225 | usage_types.append([i[0], i[1]]) 226 | result = [] 227 | 228 | for i in range(len(arguments)): 229 | type = ( 230 | "str" if len(usage_args[i].split(":")) == 1 else usage_args[i].split(":")[1] 231 | ) 232 | try: 233 | typed = await match_type(type, arguments[i], message) 234 | result.append(typed) 235 | except ValueError: 236 | return False 237 | return result 238 | 239 | 240 | def main() -> None: 241 | """Main setup function.""" 242 | 243 | db = ( 244 | SQLParser("bot/assets/main.db", CREATE_STATEMENTS) 245 | if not config.testing["ignore_db"] 246 | else SQLParser 247 | ) 248 | bot = discord.Client(intents=discord.Intents.all()) 249 | 250 | bot.manager = CommandsManager(bot, db) 251 | bot.event_manager = EventsManager(bot, db) 252 | tasks_manager = TasksManager(bot, db) 253 | 254 | # Start autoreloads 255 | threading.Thread(target=PoolingManager, args=(bot,)).start() 256 | 257 | async def register_all(): 258 | # Stop the bot attempting to load the commands multiple times 259 | if len(bot.manager.commands) != 0: 260 | # bot.manager.reset() 261 | # bot.event_manager.reset() 262 | # tasks_manager.reset() 263 | return 264 | 265 | # Load the commands 266 | entries = [ 267 | i 268 | for i in os.listdir(os.path.join("bot", "commands")) 269 | if not i.startswith("__") and not i in config.testing["ignored_files"] 270 | ] 271 | for entry in entries: 272 | cmd = entry.split(".")[0] 273 | if os.path.isfile(os.path.join("bot", "commands", entry)): 274 | bot.manager.register( 275 | __import__( 276 | f"bot.commands.{cmd}", globals(), locals(), ["cmd"], 0 277 | ).cmd, 278 | file=entry, 279 | ) 280 | else: 281 | # Current entry is a category 282 | for cmd in [ 283 | i.split(".")[0] 284 | for i in os.listdir(os.path.join("bot", "commands", entry)) 285 | if not i.startswith("__") 286 | and not i in config.testing["ignored_files"] 287 | ]: 288 | bot.manager.register( 289 | __import__( 290 | f"bot.commands.{entry}.{cmd}", 291 | globals(), 292 | locals(), 293 | ["cmd"], 294 | 0, 295 | ).cmd, 296 | entry, 297 | file=os.path.join(entry, cmd + ".py"), 298 | ) 299 | 300 | # Setup events 301 | entries = [ 302 | i.split(".")[0] 303 | for i in os.listdir(os.path.join("bot", "events")) 304 | if not i.startswith("__") and not i in config.testing["ignored_files"] 305 | ] 306 | for idx, entry in zip(range(1, len(entries) + 1, 1), entries): 307 | event = __import__( 308 | f"bot.events.{entry}", globals(), locals(), ["event"], 0 309 | ).event 310 | bot.event_manager.register(event) 311 | 312 | # Setup tasks 313 | entries = [ 314 | i.split(".")[0] 315 | for i in os.listdir(os.path.join("bot", "tasks")) 316 | if not i.startswith("__") and not i in config.testing["ignored_files"] 317 | ] 318 | for idx, entry in zip(range(1, len(entries) + 1, 1), entries): 319 | imp = __import__(f"bot.tasks.{entry}", globals(), locals(), ["*"], 0) 320 | task = [getattr(imp, i) for i in dir(imp) if i.endswith("Loop")][0] 321 | 322 | tasks_manager.register(task) 323 | 324 | bot.register_all = register_all 325 | 326 | @bot.event 327 | async def on_ready(): 328 | """When the bot is connected.""" 329 | if not config.testing["ignore_db"]: 330 | await db.initialise() 331 | 332 | if bot.user is None: 333 | raise RuntimeError("Bot user is None") 334 | 335 | logger.log("Bot is ready!") 336 | logger.log(f"Logged in as {bot.user}") 337 | 338 | logger.newline() 339 | logger.log("Username:", f"{bot.user.name}") 340 | logger.log("ID:", f"{bot.user.id}") 341 | logger.log("Guilds:", f"{len(bot.guilds)}") 342 | logger.log("Prefix:", config.prefix) 343 | logger.newline() 344 | 345 | activity = discord.Activity( 346 | type=discord.ActivityType.watching, name=f"Virbox videos" 347 | ) 348 | 349 | await bot.register_all() 350 | 351 | await bot.change_presence(activity=activity) 352 | bot.current_activity = activity 353 | bot.current_status = discord.Status.online 354 | 355 | @bot.event 356 | async def on_message(message: discord.Message): 357 | """Handle incoming messages.""" 358 | if ( 359 | message.author == bot.user 360 | or not message.content.startswith(config.prefix) 361 | or not bot.is_ready() 362 | or message.channel.id == config.general_channel 363 | ): 364 | if not "on_message.py" in Config.testing["ignored_files"]: 365 | await bot.event_manager.event_map()["on_message"].execute(message) 366 | return 367 | 368 | command, arguments = parse_user_input(message.content[len(config.prefix) :]) 369 | 370 | # Check for category 371 | prefixes: List[str] = [i.prefix for i in bot.manager.categories] 372 | if command in prefixes: 373 | try: 374 | cmdobj = { 375 | i.prefix: i for i in bot.manager.categories if i.prefix is not None 376 | }[command].commands_map()[arguments[0]] 377 | except KeyError: 378 | await logger.send_error( 379 | f"Command '{command} {arguments[0]}' not found", message 380 | ) 381 | return 382 | except IndexError: 383 | await logger.send_error( 384 | f"No subcommand found, use '{config.prefix}help {command}' for more information", 385 | message, 386 | ) 387 | return 388 | 389 | command = arguments[0] 390 | arguments = arguments[1:] 391 | else: 392 | try: 393 | cmdobj = bot.manager[command] 394 | except KeyError: 395 | try: 396 | cmdobj = [ 397 | [c for c in i.commands if c.name == command] 398 | for i in bot.manager.categories 399 | if i.prefix is None 400 | ] 401 | cmdobj = [i for i in cmdobj if len(i) != 0][0][0] 402 | except IndexError: 403 | await logger.send_error(f"Command '{command}' not found", message) 404 | return 405 | 406 | logger.log( 407 | f"'{message.author}' issued command '{command}'", 408 | f"with arguments: {arguments}", 409 | ) 410 | 411 | if cmdobj.category is not None: 412 | if not cmdobj.category.check_permissions(message): 413 | logger.error("Insufficient permissions.") 414 | await logger.send_error("Insufficient permissions.", message) 415 | return 416 | if ( 417 | len(cmdobj.category.channels) 418 | and not message.channel.id in cmdobj.category.channels 419 | ): 420 | logger.error("Channel not allowed.") 421 | await logger.send_error("Channel not allowed.", message) 422 | return 423 | 424 | # Join args 425 | usage_args: List[Tuple[str, str]] = re.findall( 426 | f"\[([^\[\]]+)\]|\<([^\<\>]+)\>", cmdobj.usage 427 | ) 428 | args_raw: List[str] = [f"<{i[1]}>" if i[1] else f"[{i[0]}]" for i in usage_args] 429 | 430 | if len(args_raw) >= 1 and args_raw[-1][1] == "*": 431 | args, tmp = (arguments[: len(args_raw) - 1], arguments[len(args_raw) - 1 :]) 432 | arguments = args + [" ".join(tmp)] 433 | 434 | # Check if a valid number of arguments have been passed 435 | if await parse_usage_text(cmdobj.usage, arguments, message): 436 | arguments_typed = await parse_types(cmdobj.usage, arguments, message) 437 | if arguments_typed == False: 438 | return 439 | try: 440 | await cmdobj.execute(arguments_typed, message) 441 | except Exception as e: 442 | await logger.send_error(str(e), message) 443 | print(traceback.format_exc()) 444 | 445 | bot.run(config.token) 446 | 447 | 448 | if __name__ == "__main__": 449 | main() 450 | -------------------------------------------------------------------------------- /bot/assets/.gitignore: -------------------------------------------------------------------------------- 1 | # database 2 | main.db 3 | *.db -------------------------------------------------------------------------------- /bot/assets/font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1rbox/discox/5797fe4a7cbe2264d0417af9a21c6980bfe62634/bot/assets/font.ttf -------------------------------------------------------------------------------- /bot/category.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Callable, Dict, List, Optional 3 | 4 | import discord 5 | 6 | from .base import Command 7 | from .config import Config 8 | 9 | config = Config() 10 | 11 | 12 | class Category(ABC): 13 | """Template for category classes.""" 14 | 15 | name: Optional[str] = None 16 | prefix: Optional[str] = None 17 | commands: List[Command] = [] 18 | channels: List[int] = [] 19 | 20 | def __init__(self) -> None: 21 | """Initialize the category.""" 22 | self.commands_map: Callable[..., Dict[str, Command]] = lambda: { 23 | command.name: command for command in self.commands if command.name 24 | } 25 | 26 | if not self.name: 27 | raise ValueError("Category name is required") 28 | 29 | @abstractmethod 30 | def check_permissions(self, message: discord.Message) -> bool: 31 | raise NotImplementedError("Check permissions method is required") 32 | 33 | 34 | class UtilityCategory(Category): 35 | """A command category instance.""" 36 | 37 | name = "utility" 38 | prefix = None 39 | commands: List[Command] = [] 40 | 41 | def check_permissions(self, message: discord.Message) -> bool: 42 | return True 43 | 44 | 45 | class DistroCategory(Category): 46 | """A command category instance.""" 47 | 48 | name = "distroroles" 49 | prefix = "distro" 50 | commands: List[Command] = [] 51 | # channels = [config.role_channel] 52 | 53 | def check_permissions(self, message: discord.Message) -> bool: 54 | return True 55 | 56 | 57 | class CodeCategory(Category): 58 | """A command category instance.""" 59 | 60 | name = "coderoles" 61 | prefix = "code" 62 | commands: List[Command] = [] 63 | channels = [config.role_channel] 64 | 65 | def check_permissions(self, message: discord.Message) -> bool: 66 | return True 67 | 68 | 69 | class DesktopCategory(Category): 70 | """A command category instance.""" 71 | 72 | name = "desktoproles" 73 | prefix = "desktop" 74 | commands: List[Command] = [] 75 | channels = [config.role_channel] 76 | 77 | def check_permissions(self, message: discord.Message) -> bool: 78 | return True 79 | 80 | 81 | class PresenceCategory(Category): 82 | """A command category instance.""" 83 | 84 | name = "changePresence" 85 | prefix = "change" 86 | commands: List[Command] = [] 87 | 88 | def check_permissions(self, message: discord.Message) -> bool: 89 | # Check for a specific role in the member 90 | return any([i.id in config.mod_role_id for i in message.author.roles]) 91 | 92 | 93 | class ModCategory(Category): 94 | """A command category instance.""" 95 | 96 | name = "mod" 97 | prefix = None 98 | commands: List[Command] = [] 99 | 100 | def check_permissions(self, message: discord.Message) -> bool: 101 | # Checking for everyone who are a mod in the server. Usually the server has more mod roles than just one 102 | return any([i.id in config.mod_role_id for i in message.author.roles]) 103 | 104 | 105 | class RemindCategory(Category): 106 | """A command category instance.""" 107 | 108 | name = "reminders" 109 | prefix = "reminder" 110 | commands: List[Command] = [] 111 | 112 | def check_permissions(self, message: discord.Message) -> bool: 113 | return True 114 | 115 | 116 | class RequestCategory(Category): 117 | """A command category instance.""" 118 | 119 | name = "request" 120 | prefix = "req" 121 | config = config 122 | 123 | def check_permissions(self, message: discord.Message) -> bool: 124 | return True 125 | 126 | 127 | class FunCategory(Category): 128 | name = "fun" 129 | prefix = None 130 | 131 | def check_permissions(self, message: discord.Message) -> bool: 132 | return True 133 | 134 | 135 | class TagCategory(Category): 136 | """A command category instance.""" 137 | 138 | name = "tag" 139 | prefix = "tag" 140 | 141 | def check_permissions(self, message: discord.Message) -> bool: 142 | return True 143 | 144 | 145 | class LevelCategory(Category): 146 | """A command category instance.""" 147 | 148 | name = "levels" 149 | prefix = None 150 | 151 | def check_permissions(self, message: discord.Message) -> bool: 152 | return True 153 | 154 | 155 | class GameCategory(Category): 156 | """A command category instance.""" 157 | 158 | name = "games" 159 | prefix = None 160 | commands: List[Command] = [] 161 | 162 | def check_permissions(self, message: discord.Message) -> bool: 163 | return True 164 | 165 | 166 | class PollsCategory(Category): 167 | """A command category instance.""" 168 | 169 | name = "polls" 170 | prefix = "poll" 171 | commands: List[Command] = [] 172 | 173 | def check_permissions(self, message: discord.Message) -> bool: 174 | return True 175 | 176 | 177 | class MinecraftCategory(Category): 178 | """A command category instance.""" 179 | 180 | name = "minecraft" 181 | prefix = "minecraft" 182 | commands: List[Command] = [] 183 | 184 | def check_permissions(self, message: discord.Message) -> bool: 185 | return True 186 | 187 | 188 | if __name__ == "__main__": 189 | print( 190 | "I had a dream where I was fighting Chuck Norris. That day I woke up with scars." 191 | ) 192 | -------------------------------------------------------------------------------- /bot/commands/binary.py: -------------------------------------------------------------------------------- 1 | # Replies with the given message in binary. 2 | 3 | from bot.base import Command 4 | from bot.config import Embed 5 | 6 | 7 | class cmd(Command): 8 | # Our 'binary' command. 9 | name = "binary" 10 | usage = "binary