├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── data ├── db │ └── build.sql ├── images │ └── profile.png └── profanity.txt ├── launcher.py ├── lib ├── bot │ └── __init__.py ├── cogs │ ├── exp.py │ ├── fun.py │ ├── help.py │ ├── info.py │ ├── log.py │ ├── meta.py │ ├── misc.py │ ├── mod.py │ ├── reactions.py │ └── welcome.py └── db │ ├── __init__.py │ └── db.py └── utils └── xptest.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.py] 10 | indent_size = 4 11 | indent_style = space 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Custom ### 2 | lib/bot/token.0 3 | 4 | ### Database ### 5 | *.accdb 6 | *.db 7 | *.dbf 8 | *.mdb 9 | *.pdb 10 | *.sqlite3 11 | 12 | ### Python ### 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Distribution / packaging 22 | .Python 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | wheels/ 35 | share/python-wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .nox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | *.py,cover 62 | .hypothesis/ 63 | .pytest_cache/ 64 | cover/ 65 | 66 | # Translations 67 | *.mo 68 | *.pot 69 | 70 | # Django stuff: 71 | *.log 72 | local_settings.py 73 | db.sqlite3 74 | db.sqlite3-journal 75 | 76 | # Flask stuff: 77 | instance/ 78 | .webassets-cache 79 | 80 | # Scrapy stuff: 81 | .scrapy 82 | 83 | # Sphinx documentation 84 | docs/_build/ 85 | 86 | # PyBuilder 87 | .pybuilder/ 88 | target/ 89 | 90 | # Jupyter Notebook 91 | .ipynb_checkpoints 92 | 93 | # IPython 94 | profile_default/ 95 | ipython_config.py 96 | 97 | # pyenv 98 | # For a library or package, you might want to ignore these files since the code is 99 | # intended to run in multiple environments; otherwise, check them in: 100 | # .python-version 101 | 102 | # pipenv 103 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 104 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 105 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 106 | # install all needed dependencies. 107 | #Pipfile.lock 108 | 109 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 110 | __pypackages__/ 111 | 112 | # Celery stuff 113 | celerybeat-schedule 114 | celerybeat.pid 115 | 116 | # SageMath parsed files 117 | *.sage.py 118 | 119 | # Environments 120 | .env 121 | .venv 122 | env/ 123 | venv/ 124 | ENV/ 125 | env.bak/ 126 | venv.bak/ 127 | 128 | # Spyder project settings 129 | .spyderproject 130 | .spyproject 131 | 132 | # Rope project settings 133 | .ropeproject 134 | 135 | # mkdocs documentation 136 | /site 137 | 138 | # mypy 139 | .mypy_cache/ 140 | .dmypy.json 141 | dmypy.json 142 | 143 | # Pyre type checker 144 | .pyre/ 145 | 146 | # pytype static type analyzer 147 | .pytype/ 148 | 149 | # Cython debug symbols 150 | cython_debug/ 151 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Carberra Tutorials 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Building a discord.py bot (2020) 2 | 3 | Welcome to the official GitHub repository for the [Building a discord.py bot (2020)](https://www.youtube.com/playlist?list=PLYeOw6sTSy6ZGyygcbta7GcpI8a5-Cooc) series by [Carberra Tutorials](https://youtube.carberra.xyz)! 4 | 5 | This repository is designed purely as a supplementary aid to the series, and should **NOT** be downloaded without having watched it first. 6 | 7 | You can [browse the tags](https://github.com/Carberra/updated-discord.py-tutorial/releases) to view the code as it was after a specific episode. 8 | 9 | ## Series requirements 10 | 11 | A [separate webpage](https://files.carberra.xyz/requirements/discord-bot-2020) has been created to outline all the required programs and libraries. You can also watch the [introduction video](https://www.youtube.com/watch?v=F1HbEOp-jdg&list=PLYeOw6sTSy6ZGyygcbta7GcpI8a5-Cooc&index=2) for an installation walkthrough. 12 | 13 | ## License 14 | 15 | This repository is made available via the [BSD 3-Clause License](https://github.com/Carberra/updated-discord.py-tutorial/blob/master/LICENSE). 16 | 17 | ## Help and further information 18 | 19 | If you need help using this repository, [watch the series](https://www.youtube.com/playlist?list=PLYeOw6sTSy6ZGyygcbta7GcpI8a5-Cooc). If you still need help beyond that, [join the Carberra Tutorials Discord server](https://discord.carberra.xyz). 20 | -------------------------------------------------------------------------------- /data/db/build.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS guilds ( 2 | GuildID integer PRIMARY KEY, 3 | Prefix text DEFAULT "+" 4 | ); 5 | 6 | CREATE TABLE IF NOT EXISTS exp ( 7 | UserID integer PRIMARY KEY, 8 | XP integer DEFAULT 0, 9 | Level integer DEFAULT 0, 10 | XPLock text DEFAULT CURRENT_TIMESTAMP 11 | ); 12 | 13 | CREATE TABLE IF NOT EXISTS mutes ( 14 | UserID integer PRIMARY KEY, 15 | RoleIDs text, 16 | EndTime text 17 | ); 18 | 19 | CREATE TABLE IF NOT EXISTS starboard ( 20 | RootMessageID integer PRIMARY KEY, 21 | StarMessageID integer, 22 | Stars integer DEFAULT 1 23 | ); -------------------------------------------------------------------------------- /data/images/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carberra/updated-discord.py-tutorial/085113e9bff69a699a25ed1cd91db5744b8755ea/data/images/profile.png -------------------------------------------------------------------------------- /data/profanity.txt: -------------------------------------------------------------------------------- 1 | bum 2 | you are bad 3 | poo 4 | -------------------------------------------------------------------------------- /launcher.py: -------------------------------------------------------------------------------- 1 | from lib.bot import bot 2 | 3 | VERSION = "0.1.4" # Part R4 4 | 5 | bot.run(VERSION) -------------------------------------------------------------------------------- /lib/bot/__init__.py: -------------------------------------------------------------------------------- 1 | from asyncio import sleep 2 | from datetime import datetime 3 | from glob import glob 4 | 5 | from apscheduler.schedulers.asyncio import AsyncIOScheduler 6 | from apscheduler.triggers.cron import CronTrigger 7 | from discord import Embed, File, DMChannel 8 | from discord.errors import HTTPException, Forbidden 9 | from discord.ext.commands import Bot as BotBase 10 | from discord.ext.commands import Context 11 | from discord.ext.commands import (CommandNotFound, BadArgument, MissingRequiredArgument, 12 | CommandOnCooldown) 13 | from discord.ext.commands import when_mentioned_or, command, has_permissions 14 | 15 | from ..db import db 16 | 17 | OWNER_IDS = [385807530913169426] 18 | COGS = [path.split("\\")[-1][:-3] for path in glob("./lib/cogs/*.py")] 19 | IGNORE_EXCEPTIONS = (CommandNotFound, BadArgument) 20 | 21 | 22 | def get_prefix(bot, message): 23 | prefix = db.field("SELECT Prefix FROM guilds WHERE GuildID = ?", message.guild.id) 24 | return when_mentioned_or(prefix)(bot, message) 25 | 26 | 27 | class Ready(object): 28 | def __init__(self): 29 | for cog in COGS: 30 | setattr(self, cog, False) 31 | 32 | def ready_up(self, cog): 33 | setattr(self, cog, True) 34 | print(f" {cog} cog ready") 35 | 36 | def all_ready(self): 37 | return all([getattr(self, cog) for cog in COGS]) 38 | 39 | 40 | class Bot(BotBase): 41 | def __init__(self): 42 | self.ready = False 43 | self.cogs_ready = Ready() 44 | 45 | self.guild = None 46 | self.scheduler = AsyncIOScheduler() 47 | 48 | try: 49 | with open("./data/banlist.txt", "r", encoding="utf-8") as f: 50 | self.banlist = [int(line.strip()) for line in f.readlines()] 51 | except FileNotFoundError: 52 | self.banlist = [] 53 | 54 | db.autosave(self.scheduler) 55 | super().__init__(command_prefix=get_prefix, owner_ids=OWNER_IDS) 56 | 57 | def setup(self): 58 | for cog in COGS: 59 | self.load_extension(f"lib.cogs.{cog}") 60 | print(f" {cog} cog loaded") 61 | 62 | print("setup complete") 63 | 64 | def update_db(self): 65 | db.multiexec("INSERT OR IGNORE INTO guilds (GuildID) VALUES (?)", 66 | ((guild.id,) for guild in self.guilds)) 67 | 68 | db.multiexec("INSERT OR IGNORE INTO exp (UserID) VALUES (?)", 69 | ((member.id,) for member in self.guild.members if not member.bot)) 70 | 71 | to_remove = [] 72 | stored_members = db.column("SELECT UserID FROM exp") 73 | for id_ in stored_members: 74 | if not self.guild.get_member(id_): 75 | to_remove.append(id_) 76 | 77 | db.multiexec("DELETE FROM exp WHERE UserID = ?", 78 | ((id_,) for id_ in to_remove)) 79 | 80 | db.commit() 81 | 82 | def run(self, version): 83 | self.VERSION = version 84 | 85 | print("running setup...") 86 | self.setup() 87 | 88 | with open("./lib/bot/token.0", "r", encoding="utf-8") as tf: 89 | self.TOKEN = tf.read() 90 | 91 | print("running bot...") 92 | super().run(self.TOKEN, reconnect=True) 93 | 94 | async def process_commands(self, message): 95 | ctx = await self.get_context(message, cls=Context) 96 | 97 | if ctx.command is not None and ctx.guild is not None: 98 | if message.author.id in self.banlist: 99 | await ctx.send("You are banned from using commands.") 100 | 101 | elif not self.ready: 102 | await ctx.send("I'm not ready to receive commands. Please wait a few seconds.") 103 | 104 | else: 105 | await self.invoke(ctx) 106 | 107 | async def rules_reminder(self): 108 | await self.stdout.send("Remember to adhere to the rules!") 109 | 110 | async def on_connect(self): 111 | print(" bot connected") 112 | 113 | async def on_disconnect(self): 114 | print("bot disconnected") 115 | 116 | async def on_error(self, err, *args, **kwargs): 117 | if err == "on_command_error": 118 | await args[0].send("Something went wrong.") 119 | 120 | await self.stdout.send("An error occured.") 121 | raise 122 | 123 | async def on_command_error(self, ctx, exc): 124 | if any([isinstance(exc, error) for error in IGNORE_EXCEPTIONS]): 125 | pass 126 | 127 | elif isinstance(exc, MissingRequiredArgument): 128 | await ctx.send("One or more required arguments are missing.") 129 | 130 | elif isinstance(exc, CommandOnCooldown): 131 | await ctx.send(f"That command is on {str(exc.cooldown.type).split('.')[-1]} cooldown. Try again in {exc.retry_after:,.2f} secs.") 132 | 133 | elif hasattr(exc, "original"): 134 | # if isinstance(exc.original, HTTPException): 135 | # await ctx.send("Unable to send message.") 136 | 137 | if isinstance(exc.original, Forbidden): 138 | await ctx.send("I do not have permission to do that.") 139 | 140 | else: 141 | raise exc.original 142 | 143 | else: 144 | raise exc 145 | 146 | async def on_ready(self): 147 | if not self.ready: 148 | self.guild = self.get_guild(626608699942764544) 149 | self.stdout = self.get_channel(759434903145152533) 150 | self.scheduler.add_job(self.rules_reminder, CronTrigger(day_of_week=0, hour=12, minute=0, second=0)) 151 | self.scheduler.start() 152 | 153 | self.update_db() 154 | 155 | # embed = Embed(title="Now online!", description="Carberretta is now online.", 156 | # colour=0xFF0000, timestamp=datetime.utcnow()) 157 | # fields = [("Name", "Value", True), 158 | # ("Another field", "This field is next to the other one.", True), 159 | # ("A non-inline field", "This field will appear on it's own row.", False)] 160 | # for name, value, inline in fields: 161 | # embed.add_field(name=name, value=value, inline=inline) 162 | # embed.set_author(name="Carberra Tutorials", icon_url=self.guild.icon_url) 163 | # embed.set_footer(text="This is a footer!") 164 | # await channel.send(embed=embed) 165 | 166 | # await channel.send(file=File("./data/images/profile.png")) 167 | 168 | while not self.cogs_ready.all_ready(): 169 | await sleep(0.5) 170 | 171 | await self.stdout.send("Now online!") 172 | self.ready = True 173 | print(" bot ready") 174 | 175 | meta = self.get_cog("Meta") 176 | await meta.set() 177 | 178 | else: 179 | print("bot reconnected") 180 | 181 | async def on_message(self, message): 182 | if not message.author.bot: 183 | if isinstance(message.channel, DMChannel): 184 | if len(message.content) < 50: 185 | await message.channel.send("Your message should be at least 50 characters in length.") 186 | 187 | else: 188 | member = self.guild.get_member(message.author.id) 189 | embed = Embed(title="Modmail", 190 | colour=member.colour, 191 | timestamp=datetime.utcnow()) 192 | 193 | embed.set_thumbnail(url=member.avatar_url) 194 | 195 | fields = [("Member", member.display_name, False), 196 | ("Message", message.content, False)] 197 | 198 | for name, value, inline in fields: 199 | embed.add_field(name=name, value=value, inline=inline) 200 | 201 | mod = self.get_cog("Mod") 202 | await mod.log_channel.send(embed=embed) 203 | await message.channel.send("Message relayed to moderators.") 204 | 205 | else: 206 | await self.process_commands(message) 207 | 208 | 209 | bot = Bot() -------------------------------------------------------------------------------- /lib/cogs/exp.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from random import randint 3 | from typing import Optional 4 | 5 | from discord import Member, Embed 6 | from discord.ext.commands import Cog 7 | from discord.ext.commands import CheckFailure 8 | from discord.ext.commands import command, has_permissions 9 | from discord.ext.menus import MenuPages, ListPageSource 10 | 11 | from ..db import db 12 | 13 | 14 | class HelpMenu(ListPageSource): 15 | def __init__(self, ctx, data): 16 | self.ctx = ctx 17 | 18 | super().__init__(data, per_page=10) 19 | 20 | async def write_page(self, menu, offset, fields=[]): 21 | len_data = len(self.entries) 22 | 23 | embed = Embed(title="XP Leaderboard", 24 | colour=self.ctx.author.colour) 25 | embed.set_thumbnail(url=self.ctx.guild.icon_url) 26 | embed.set_footer(text=f"{offset:,} - {min(len_data, offset+self.per_page-1):,} of {len_data:,} members.") 27 | 28 | for name, value in fields: 29 | embed.add_field(name=name, value=value, inline=False) 30 | 31 | return embed 32 | 33 | async def format_page(self, menu, entries): 34 | offset = (menu.current_page*self.per_page) + 1 35 | 36 | fields = [] 37 | table = ("\n".join(f"{idx+offset}. {self.ctx.bot.guild.get_member(entry[0]).display_name} (XP: {entry[1]} | Level: {entry[2]})" 38 | for idx, entry in enumerate(entries))) 39 | 40 | fields.append(("Ranks", table)) 41 | 42 | return await self.write_page(menu, offset, fields) 43 | 44 | 45 | class Exp(Cog): 46 | def __init__(self, bot): 47 | self.bot = bot 48 | 49 | async def process_xp(self, message): 50 | xp, lvl, xplock = db.record("SELECT XP, Level, XPLock FROM exp WHERE UserID = ?", message.author.id) 51 | 52 | if datetime.utcnow() > datetime.fromisoformat(xplock): 53 | await self.add_xp(message, xp, lvl) 54 | 55 | async def add_xp(self, message, xp, lvl): 56 | xp_to_add = randint(10, 20) 57 | new_lvl = int(((xp+xp_to_add)//42) ** 0.55) 58 | 59 | db.execute("UPDATE exp SET XP = XP + ?, Level = ?, XPLock = ? WHERE UserID = ?", 60 | xp_to_add, new_lvl, (datetime.utcnow()+timedelta(seconds=60)).isoformat(), message.author.id) 61 | 62 | if new_lvl > lvl: 63 | await self.levelup_channel.send(f"Congrats {message.author.mention} - you reached level {new_lvl:,}!") 64 | await self.check_lvl_rewards(message, new_lvl) 65 | 66 | async def check_lvl_rewards(self, message, lvl): 67 | if lvl >= 50: # Red 68 | if (new_role := message.guild.get_role(653940117680947232)) not in message.author.roles: 69 | await message.author.add_roles(new_role) 70 | await message.author.remove_roles(message.guild.get_role(653940192780222515)) 71 | 72 | elif 40 <= lvl < 50: # Yellow 73 | if (new_role := message.guild.get_role(653940192780222515)) not in message.author.roles: 74 | await message.author.add_roles(new_role) 75 | await message.author.remove_roles(message.guild.get_role(653940254293622794)) 76 | 77 | elif 30 <= lvl < 40: # Green 78 | if (new_role := message.guild.get_role(653940254293622794)) not in message.author.roles: 79 | await message.author.add_roles(new_role) 80 | await message.author.remove_roles(message.guild.get_role(653940277761015809)) 81 | 82 | elif 20 <= lvl < 30: # Blue 83 | if (new_role := message.guild.get_role(653940277761015809)) not in message.author.roles: 84 | await message.author.add_roles(new_role) 85 | await message.author.remove_roles(message.guild.get_role(653940305300815882)) 86 | 87 | elif 10 <= lvl < 20: # Purple 88 | if (new_role := message.guild.get_role(653940305300815882)) not in message.author.roles: 89 | await message.author.add_roles(new_role) 90 | await message.author.remove_roles(message.guild.get_role(653940328453373952)) 91 | 92 | elif 5 <= lvl < 9: # Black 93 | if (new_role := message.guild.get_role(653940328453373952)) not in message.author.roles: 94 | await message.author.add_roles(new_role) 95 | 96 | @command(name="level") 97 | async def display_level(self, ctx, target: Optional[Member]): 98 | target = target or ctx.author 99 | 100 | xp, lvl = db.record("SELECT XP, Level FROM exp WHERE UserID = ?", target.id) or (None, None) 101 | 102 | if lvl is not None: 103 | await ctx.send(f"{target.display_name} is on level {lvl:,} with {xp:,} XP.") 104 | 105 | else: 106 | await ctx.send("That member is not tracked by the experience system.") 107 | 108 | @command(name="rank") 109 | async def display_rank(self, ctx, target: Optional[Member]): 110 | target = target or ctx.author 111 | 112 | ids = db.column("SELECT UserID FROM exp ORDER BY XP DESC") 113 | 114 | try: 115 | await ctx.send(f"{target.display_name} is rank {ids.index(target.id)+1} of {len(ids)}.") 116 | 117 | except ValueError: 118 | await ctx.send("That member is not tracked by the experience system.") 119 | 120 | @command(name="leaderboard", aliases=["lb"]) 121 | async def display_leaderboard(self, ctx): 122 | records = db.records("SELECT UserID, XP, Level FROM exp ORDER BY XP DESC") 123 | 124 | menu = MenuPages(source=HelpMenu(ctx, records), 125 | clear_reactions_after=True, 126 | timeout=60.0) 127 | await menu.start(ctx) 128 | 129 | @Cog.listener() 130 | async def on_ready(self): 131 | if not self.bot.ready: 132 | self.levelup_channel = self.bot.get_channel(759432499221889034) 133 | self.bot.cogs_ready.ready_up("exp") 134 | 135 | @Cog.listener() 136 | async def on_message(self, message): 137 | if not message.author.bot: 138 | await self.process_xp(message) 139 | 140 | 141 | def setup(bot): 142 | bot.add_cog(Exp(bot)) -------------------------------------------------------------------------------- /lib/cogs/fun.py: -------------------------------------------------------------------------------- 1 | from random import choice, randint 2 | from typing import Optional 3 | 4 | from aiohttp import request 5 | from discord import Member, Embed 6 | from discord.ext.commands import Cog, BucketType 7 | from discord.ext.commands import BadArgument 8 | from discord.ext.commands import command, cooldown 9 | 10 | 11 | class Fun(Cog): 12 | def __init__(self, bot): 13 | self.bot = bot 14 | 15 | @command(name="hello", aliases=["hi"]) 16 | async def say_hello(self, ctx): 17 | await ctx.send(f"{choice(('Hello', 'Hi', 'Hey', 'Hiya'))} {ctx.author.mention}!") 18 | 19 | @command(name="dice", aliases=["roll"]) 20 | @cooldown(1, 60, BucketType.user) 21 | async def roll_dice(self, ctx, die_string: str): 22 | dice, value = (int(term) for term in die_string.split("d")) 23 | 24 | if dice <= 25: 25 | rolls = [randint(1, value) for i in range(dice)] 26 | 27 | await ctx.send(" + ".join([str(r) for r in rolls]) + f" = {sum(rolls)}") 28 | 29 | else: 30 | await ctx.send("I can't roll that many dice. Please try a lower number.") 31 | 32 | @command(name="slap", aliases=["hit"]) 33 | async def slap_member(self, ctx, member: Member, *, reason: Optional[str] = "for no reason"): 34 | await ctx.send(f"{ctx.author.display_name} slapped {member.mention} {reason}!") 35 | 36 | @slap_member.error 37 | async def slap_member_error(self, ctx, exc): 38 | if isinstance(exc, BadArgument): 39 | await ctx.send("I can't find that member.") 40 | 41 | @command(name="echo", aliases=["say"]) 42 | @cooldown(1, 15, BucketType.guild) 43 | async def echo_message(self, ctx, *, message): 44 | await ctx.message.delete() 45 | await ctx.send(message) 46 | 47 | @command(name="fact") 48 | @cooldown(3, 60, BucketType.guild) 49 | async def animal_fact(self, ctx, animal: str): 50 | if (animal := animal.lower()) in ("dog", "cat", "panda", "fox", "bird", "koala"): 51 | fact_url = f"https://some-random-api.ml/facts/{animal}" 52 | image_url = f"https://some-random-api.ml/img/{'birb' if animal == 'bird' else animal}" 53 | 54 | async with request("GET", image_url, headers={}) as response: 55 | if response.status == 200: 56 | data = await response.json() 57 | image_link = data["link"] 58 | 59 | else: 60 | image_link = None 61 | 62 | async with request("GET", fact_url, headers={}) as response: 63 | if response.status == 200: 64 | data = await response.json() 65 | 66 | embed = Embed(title=f"{animal.title()} fact", 67 | description=data["fact"], 68 | colour=ctx.author.colour) 69 | if image_link is not None: 70 | embed.set_image(url=image_link) 71 | await ctx.send(embed=embed) 72 | 73 | else: 74 | await ctx.send(f"API returned a {response.status} status.") 75 | 76 | else: 77 | await ctx.send("No facts are available for that animal.") 78 | 79 | @Cog.listener() 80 | async def on_ready(self): 81 | if not self.bot.ready: 82 | self.bot.cogs_ready.ready_up("fun") 83 | 84 | 85 | def setup(bot): 86 | bot.add_cog(Fun(bot)) -------------------------------------------------------------------------------- /lib/cogs/help.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from discord import Embed 4 | from discord.utils import get 5 | from discord.ext.menus import MenuPages, ListPageSource 6 | from discord.ext.commands import Cog 7 | from discord.ext.commands import command 8 | 9 | 10 | def syntax(command): 11 | cmd_and_aliases = "|".join([str(command), *command.aliases]) 12 | params = [] 13 | 14 | for key, value in command.params.items(): 15 | if key not in ("self", "ctx"): 16 | params.append(f"[{key}]" if "NoneType" in str(value) else f"<{key}>") 17 | 18 | params = " ".join(params) 19 | 20 | return f"`{cmd_and_aliases} {params}`" 21 | 22 | 23 | class HelpMenu(ListPageSource): 24 | def __init__(self, ctx, data): 25 | self.ctx = ctx 26 | 27 | super().__init__(data, per_page=3) 28 | 29 | async def write_page(self, menu, fields=[]): 30 | offset = (menu.current_page*self.per_page) + 1 31 | len_data = len(self.entries) 32 | 33 | embed = Embed(title="Help", 34 | description="Welcome to the Carberretta help dialog!", 35 | colour=self.ctx.author.colour) 36 | embed.set_thumbnail(url=self.ctx.guild.me.avatar_url) 37 | embed.set_footer(text=f"{offset:,} - {min(len_data, offset+self.per_page-1):,} of {len_data:,} commands.") 38 | 39 | for name, value in fields: 40 | embed.add_field(name=name, value=value, inline=False) 41 | 42 | return embed 43 | 44 | async def format_page(self, menu, entries): 45 | fields = [] 46 | 47 | for entry in entries: 48 | fields.append((entry.brief or "No description", syntax(entry))) 49 | 50 | return await self.write_page(menu, fields) 51 | 52 | 53 | class Help(Cog): 54 | def __init__(self, bot): 55 | self.bot = bot 56 | self.bot.remove_command("help") 57 | 58 | async def cmd_help(self, ctx, command): 59 | embed = Embed(title=f"Help with `{command}`", 60 | description=syntax(command), 61 | colour=ctx.author.colour) 62 | embed.add_field(name="Command description", value=command.help) 63 | await ctx.send(embed=embed) 64 | 65 | @command(name="help") 66 | async def show_help(self, ctx, cmd: Optional[str]): 67 | """Shows this message.""" 68 | if cmd is None: 69 | menu = MenuPages(source=HelpMenu(ctx, list(self.bot.commands)), 70 | delete_message_after=True, 71 | timeout=60.0) 72 | await menu.start(ctx) 73 | 74 | else: 75 | if (command := get(self.bot.commands, name=cmd)): 76 | await self.cmd_help(ctx, command) 77 | 78 | else: 79 | await ctx.send("That command does not exist.") 80 | 81 | @Cog.listener() 82 | async def on_ready(self): 83 | if not self.bot.ready: 84 | self.bot.cogs_ready.ready_up("help") 85 | 86 | 87 | def setup(bot): 88 | bot.add_cog(Help(bot)) -------------------------------------------------------------------------------- /lib/cogs/info.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Optional 3 | 4 | from discord import Embed, Member 5 | from discord.ext.commands import Cog 6 | from discord.ext.commands import command 7 | 8 | class Info(Cog): 9 | def __init__(self, bot): 10 | self.bot = bot 11 | 12 | @command(name="userinfo", aliases=["memberinfo", "ui", "mi"]) 13 | async def user_info(self, ctx, target: Optional[Member]): 14 | target = target or ctx.author 15 | 16 | embed = Embed(title="User information", 17 | colour=target.colour, 18 | timestamp=datetime.utcnow()) 19 | 20 | embed.set_thumbnail(url=target.avatar_url) 21 | 22 | fields = [("Name", str(target), True), 23 | ("ID", target.id, True), 24 | ("Bot?", target.bot, True), 25 | ("Top role", target.top_role.mention, True), 26 | ("Status", str(target.status).title(), True), 27 | ("Activity", f"{str(target.activity.type).split('.')[-1].title() if target.activity else 'N/A'} {target.activity.name if target.activity else ''}", True), 28 | ("Created at", target.created_at.strftime("%d/%m/%Y %H:%M:%S"), True), 29 | ("Joined at", target.joined_at.strftime("%d/%m/%Y %H:%M:%S"), True), 30 | ("Boosted", bool(target.premium_since), True)] 31 | 32 | for name, value, inline in fields: 33 | embed.add_field(name=name, value=value, inline=inline) 34 | 35 | await ctx.send(embed=embed) 36 | 37 | @command(name="serverinfo", aliases=["guildinfo", "si", "gi"]) 38 | async def server_info(self, ctx): 39 | embed = Embed(title="Server information", 40 | colour=ctx.guild.owner.colour, 41 | timestamp=datetime.utcnow()) 42 | 43 | embed.set_thumbnail(url=ctx.guild.icon_url) 44 | 45 | statuses = [len(list(filter(lambda m: str(m.status) == "online", ctx.guild.members))), 46 | len(list(filter(lambda m: str(m.status) == "idle", ctx.guild.members))), 47 | len(list(filter(lambda m: str(m.status) == "dnd", ctx.guild.members))), 48 | len(list(filter(lambda m: str(m.status) == "offline", ctx.guild.members)))] 49 | 50 | fields = [("ID", ctx.guild.id, True), 51 | ("Owner", ctx.guild.owner, True), 52 | ("Region", ctx.guild.region, True), 53 | ("Created at", ctx.guild.created_at.strftime("%d/%m/%Y %H:%M:%S"), True), 54 | ("Members", len(ctx.guild.members), True), 55 | ("Humans", len(list(filter(lambda m: not m.bot, ctx.guild.members))), True), 56 | ("Bots", len(list(filter(lambda m: m.bot, ctx.guild.members))), True), 57 | ("Banned members", len(await ctx.guild.bans()), True), 58 | ("Statuses", f"🟢 {statuses[0]} 🟠 {statuses[1]} 🔴 {statuses[2]} ⚪ {statuses[3]}", True), 59 | ("Text channels", len(ctx.guild.text_channels), True), 60 | ("Voice channels", len(ctx.guild.voice_channels), True), 61 | ("Categories", len(ctx.guild.categories), True), 62 | ("Roles", len(ctx.guild.roles), True), 63 | ("Invites", len(await ctx.guild.invites()), True), 64 | ("\u200b", "\u200b", True)] 65 | 66 | for name, value, inline in fields: 67 | embed.add_field(name=name, value=value, inline=inline) 68 | 69 | await ctx.send(embed=embed) 70 | 71 | @Cog.listener() 72 | async def on_ready(self): 73 | if not self.bot.ready: 74 | self.bot.cogs_ready.ready_up("info") 75 | 76 | 77 | def setup(bot): 78 | bot.add_cog(Info(bot)) -------------------------------------------------------------------------------- /lib/cogs/log.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from discord import Embed 4 | from discord.ext.commands import Cog 5 | from discord.ext.commands import command 6 | 7 | 8 | class Log(Cog): 9 | def __init__(self, bot): 10 | self.bot = bot 11 | 12 | @Cog.listener() 13 | async def on_ready(self): 14 | if not self.bot.ready: 15 | self.log_channel = self.bot.get_channel(759432499221889034) 16 | self.bot.cogs_ready.ready_up("log") 17 | 18 | @Cog.listener() 19 | async def on_user_update(self, before, after): 20 | if before.name != after.name: 21 | embed = Embed(title="Username change", 22 | colour=after.colour, 23 | timestamp=datetime.utcnow()) 24 | 25 | fields = [("Before", before.name, False), 26 | ("After", after.name, False)] 27 | 28 | for name, value, inline in fields: 29 | embed.add_field(name=name, value=value, inline=inline) 30 | 31 | await self.log_channel.send(embed=embed) 32 | 33 | if before.discriminator != after.discriminator: 34 | embed = Embed(title="Discriminator change", 35 | colour=after.colour, 36 | timestamp=datetime.utcnow()) 37 | 38 | fields = [("Before", before.discriminator, False), 39 | ("After", after.discriminator, False)] 40 | 41 | for name, value, inline in fields: 42 | embed.add_field(name=name, value=value, inline=inline) 43 | 44 | await self.log_channel.send(embed=embed) 45 | 46 | if before.avatar_url != after.avatar_url: 47 | embed = Embed(title="Avatar change", 48 | description="New image is below, old to the right.", 49 | colour=self.log_channel.guild.get_member(after.id).colour, 50 | timestamp=datetime.utcnow()) 51 | 52 | embed.set_thumbnail(url=before.avatar_url) 53 | embed.set_image(url=after.avatar_url) 54 | 55 | await self.log_channel.send(embed=embed) 56 | 57 | @Cog.listener() 58 | async def on_member_update(self, before, after): 59 | if before.display_name != after.display_name: 60 | embed = Embed(title="Nickname change", 61 | colour=after.colour, 62 | timestamp=datetime.utcnow()) 63 | 64 | fields = [("Before", before.display_name, False), 65 | ("After", after.display_name, False)] 66 | 67 | for name, value, inline in fields: 68 | embed.add_field(name=name, value=value, inline=inline) 69 | 70 | await self.log_channel.send(embed=embed) 71 | 72 | elif before.roles != after.roles: 73 | embed = Embed(title="Role updates", 74 | colour=after.colour, 75 | timestamp=datetime.utcnow()) 76 | 77 | fields = [("Before", ", ".join([r.mention for r in before.roles]), False), 78 | ("After", ", ".join([r.mention for r in after.roles]), False)] 79 | 80 | for name, value, inline in fields: 81 | embed.add_field(name=name, value=value, inline=inline) 82 | 83 | await self.log_channel.send(embed=embed) 84 | 85 | @Cog.listener() 86 | async def on_message_edit(self, before, after): 87 | if not after.author.bot: 88 | if before.content != after.content: 89 | embed = Embed(title="Message edit", 90 | description=f"Edit by {after.author.display_name}.", 91 | colour=after.author.colour, 92 | timestamp=datetime.utcnow()) 93 | 94 | fields = [("Before", before.content, False), 95 | ("After", after.content, False)] 96 | 97 | for name, value, inline in fields: 98 | embed.add_field(name=name, value=value, inline=inline) 99 | 100 | await self.log_channel.send(embed=embed) 101 | 102 | @Cog.listener() 103 | async def on_message_delete(self, message): 104 | if not message.author.bot: 105 | embed = Embed(title="Message deletion", 106 | description=f"Action by {message.author.display_name}.", 107 | colour=message.author.colour, 108 | timestamp=datetime.utcnow()) 109 | 110 | fields = [("Content", message.content, False)] 111 | 112 | for name, value, inline in fields: 113 | embed.add_field(name=name, value=value, inline=inline) 114 | 115 | await self.log_channel.send(embed=embed) 116 | 117 | 118 | def setup(bot): 119 | bot.add_cog(Log(bot)) -------------------------------------------------------------------------------- /lib/cogs/meta.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from platform import python_version 3 | from time import time 4 | 5 | from apscheduler.triggers.cron import CronTrigger 6 | from discord import Activity, ActivityType, Embed 7 | from discord import __version__ as discord_version 8 | from discord.ext.commands import Cog 9 | from discord.ext.commands import command 10 | from psutil import Process, virtual_memory 11 | 12 | from ..db import db 13 | 14 | 15 | class Meta(Cog): 16 | def __init__(self, bot): 17 | self.bot = bot 18 | 19 | self._message = "watching +help | {users:,} users in {guilds:,} servers" 20 | 21 | bot.scheduler.add_job(self.set, CronTrigger(second=0)) 22 | 23 | @property 24 | def message(self): 25 | return self._message.format(users=len(self.bot.users), guilds=len(self.bot.guilds)) 26 | 27 | @message.setter 28 | def message(self, value): 29 | if value.split(" ")[0] not in ("playing", "watching", "listening", "streaming"): 30 | raise ValueError("Invalid activity type.") 31 | 32 | self._message = value 33 | 34 | async def set(self): 35 | _type, _name = self.message.split(" ", maxsplit=1) 36 | 37 | await self.bot.change_presence(activity=Activity( 38 | name=_name, type=getattr(ActivityType, _type, ActivityType.playing) 39 | )) 40 | 41 | @command(name="setactivity") 42 | async def set_activity_message(self, ctx, *, text: str): 43 | self.message = text 44 | await self.set() 45 | 46 | @command(name="ping") 47 | async def ping(self, ctx): 48 | start = time() 49 | message = await ctx.send(f"Pong! DWSP latency: {self.bot.latency*1000:,.0f} ms.") 50 | end = time() 51 | 52 | await message.edit(content=f"Pong! DWSP latency: {self.bot.latency*1000:,.0f} ms. Response time: {(end-start)*1000:,.0f} ms.") 53 | 54 | @command(name="stats") 55 | async def show_bot_stats(self, ctx): 56 | embed = Embed(title="Bot stats", 57 | colour=ctx.author.colour, 58 | thumbnail=self.bot.user.avatar_url, 59 | timestamp=datetime.utcnow()) 60 | 61 | proc = Process() 62 | with proc.oneshot(): 63 | uptime = timedelta(seconds=time()-proc.create_time()) 64 | cpu_time = timedelta(seconds=(cpu := proc.cpu_times()).system + cpu.user) 65 | mem_total = virtual_memory().total / (1024**2) 66 | mem_of_total = proc.memory_percent() 67 | mem_usage = mem_total * (mem_of_total / 100) 68 | 69 | fields = [ 70 | ("Bot version", self.bot.VERSION, True), 71 | ("Python version", python_version(), True), 72 | ("discord.py version", discord_version, True), 73 | ("Uptime", uptime, True), 74 | ("CPU time", cpu_time, True), 75 | ("Memory usage", f"{mem_usage:,.3f} / {mem_total:,.0f} MiB ({mem_of_total:.0f}%)", True), 76 | ("Users", f"{self.bot.guild.member_count:,}", True) 77 | ] 78 | 79 | for name, value, inline in fields: 80 | embed.add_field(name=name, value=value, inline=inline) 81 | 82 | await ctx.send(embed=embed) 83 | 84 | @command(name="shutdown") 85 | async def shutdown(self, ctx): 86 | await ctx.send("Shutting down...") 87 | 88 | with open("./data/banlist.txt", "w", encoding="utf-8") as f: 89 | f.writelines([f"{item}\n" for item in self.bot.banlist]) 90 | 91 | db.commit() 92 | self.bot.scheduler.shutdown() 93 | await self.bot.logout() 94 | 95 | @Cog.listener() 96 | async def on_ready(self): 97 | if not self.bot.ready: 98 | self.bot.cogs_ready.ready_up("meta") 99 | 100 | 101 | def setup(bot): 102 | bot.add_cog(Meta(bot)) -------------------------------------------------------------------------------- /lib/cogs/misc.py: -------------------------------------------------------------------------------- 1 | from discord import Member 2 | from discord.ext.commands import Cog, Greedy 3 | from discord.ext.commands import CheckFailure 4 | from discord.ext.commands import command, has_permissions 5 | 6 | from ..db import db 7 | 8 | class Misc(Cog): 9 | def __init__(self, bot): 10 | self.bot = bot 11 | 12 | @command(name="prefix") 13 | @has_permissions(manage_guild=True) 14 | async def change_prefix(self, ctx, new: str): 15 | if len(new) > 5: 16 | await ctx.send("The prefix can not be more than 5 characters in length.") 17 | 18 | else: 19 | db.execute("UPDATE guilds SET Prefix = ? WHERE GuildID = ?", new, ctx.guild.id) 20 | await ctx.send(f"Prefix set to {new}.") 21 | 22 | @change_prefix.error 23 | async def change_prefix_error(self, ctx, exc): 24 | if isinstance(exc, CheckFailure): 25 | await ctx.send("You need the Manage Server permission to do that.") 26 | 27 | @command(name="addban") 28 | @has_permissions(manage_guild=True) 29 | async def addban_command(self, ctx, targets: Greedy[Member]): 30 | if not targets: 31 | await ctx.send("No targets specified.") 32 | 33 | else: 34 | self.bot.banlist.extend([t.id for t in targets]) 35 | await ctx.send("Done.") 36 | 37 | @command(name="delban", aliases=["rmban"]) 38 | @has_permissions(manage_guild=True) 39 | async def delban_command(self, ctx, targets: Greedy[Member]): 40 | if not targets: 41 | await ctx.send("No targets specified.") 42 | 43 | else: 44 | for target in targets: 45 | self.bot.banlist.remove(target.id) 46 | await ctx.send("Done.") 47 | 48 | @Cog.listener() 49 | async def on_ready(self): 50 | if not self.bot.ready: 51 | self.bot.cogs_ready.ready_up("misc") 52 | 53 | 54 | def setup(bot): 55 | bot.add_cog(Misc(bot)) -------------------------------------------------------------------------------- /lib/cogs/mod.py: -------------------------------------------------------------------------------- 1 | from asyncio import sleep 2 | from datetime import datetime, timedelta 3 | from re import search 4 | from typing import Optional 5 | 6 | from better_profanity import profanity 7 | from discord import Embed, Member, NotFound, Object 8 | from discord.utils import find 9 | from discord.ext.commands import Cog, Greedy, Converter 10 | from discord.ext.commands import CheckFailure, BadArgument 11 | from discord.ext.commands import command, has_permissions, bot_has_permissions 12 | 13 | from ..db import db 14 | 15 | profanity.load_censor_words_from_file("./data/profanity.txt") 16 | 17 | 18 | class BannedUser(Converter): 19 | async def convert(self, ctx, arg): 20 | if ctx.guild.me.guild_permissions.ban_members: 21 | if arg.isdigit(): 22 | try: 23 | return (await ctx.guild.fetch_ban(Object(id=int(arg)))).user 24 | except NotFound: 25 | raise BadArgument 26 | 27 | banned = [e.user for e in await ctx.guild.bans()] 28 | if banned: 29 | if (user := find(lambda u: str(u) == arg, banned)) is not None: 30 | return user 31 | else: 32 | raise BadArgument 33 | 34 | 35 | class Mod(Cog): 36 | def __init__(self, bot): 37 | self.bot = bot 38 | 39 | self.url_regex = r"(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'\".,<>?«»“”‘’]))" 40 | self.links_allowed = (759432499221889034,) 41 | self.images_allowed = (759432499221889034,) 42 | 43 | async def kick_members(self, message, targets, reason): 44 | for target in targets: 45 | if (message.guild.me.top_role.position > target.top_role.position 46 | and not target.guild_permissions.administrator): 47 | await target.kick(reason=reason) 48 | 49 | embed = Embed(title="Member kicked", 50 | colour=0xDD2222, 51 | timestamp=datetime.utcnow()) 52 | 53 | embed.set_thumbnail(url=target.avatar_url) 54 | 55 | fields = [("Member", f"{target.name} a.k.a. {target.display_name}", False), 56 | ("Actioned by", message.author.display_name, False), 57 | ("Reason", reason, False)] 58 | 59 | for name, value, inline in fields: 60 | embed.add_field(name=name, value=value, inline=inline) 61 | 62 | await self.log_channel.send(embed=embed) 63 | 64 | @command(name="kick") 65 | @bot_has_permissions(kick_members=True) 66 | @has_permissions(kick_members=True) 67 | async def kick_command(self, ctx, targets: Greedy[Member], *, reason: Optional[str] = "No reason provided."): 68 | if not len(targets): 69 | await ctx.send("One or more required arguments are missing.") 70 | 71 | else: 72 | await self.kick_members(ctx.message, targets, reason) 73 | await ctx.send("Action complete.") 74 | 75 | @kick_command.error 76 | async def kick_command_error(self, ctx, exc): 77 | if isinstance(exc, CheckFailure): 78 | await ctx.send("Insufficient permissions to perform that task.") 79 | 80 | async def ban_members(self, message, targets, reason): 81 | for target in targets: 82 | if (message.guild.me.top_role.position > target.top_role.position 83 | and not target.guild_permissions.administrator): 84 | await target.ban(reason=reason) 85 | 86 | embed = Embed(title="Member banned", 87 | colour=0xDD2222, 88 | timestamp=datetime.utcnow()) 89 | 90 | embed.set_thumbnail(url=target.avatar_url) 91 | 92 | fields = [("Member", f"{target.name} a.k.a. {target.display_name}", False), 93 | ("Actioned by", message.author.display_name, False), 94 | ("Reason", reason, False)] 95 | 96 | for name, value, inline in fields: 97 | embed.add_field(name=name, value=value, inline=inline) 98 | 99 | await self.log_channel.send(embed=embed) 100 | 101 | @command(name="ban") 102 | @bot_has_permissions(ban_members=True) 103 | @has_permissions(ban_members=True) 104 | async def ban_command(self, ctx, targets: Greedy[Member], *, reason: Optional[str] = "No reason provided."): 105 | if not len(targets): 106 | await ctx.send("One or more required arguments are missing.") 107 | 108 | else: 109 | await self.ban_members(ctx.message, targets, reason) 110 | await ctx.send("Action complete.") 111 | 112 | @ban_command.error 113 | async def ban_command_error(self, ctx, exc): 114 | if isinstance(exc, CheckFailure): 115 | await ctx.send("Insufficient permissions to perform that task.") 116 | 117 | @command(name="unban") 118 | @bot_has_permissions(ban_members=True) 119 | @has_permissions(ban_members=True) 120 | async def unban_command(self, ctx, targets: Greedy[BannedUser], *, reason: Optional[str] = "No reason provided."): 121 | if not len(targets): 122 | await ctx.send("One or more required arguments are missing.") 123 | 124 | else: 125 | for target in targets: 126 | await ctx.guild.unban(target, reason=reason) 127 | 128 | embed = Embed(title="Member unbanned", 129 | colour=0xDD2222, 130 | timestamp=datetime.utcnow()) 131 | 132 | embed.set_thumbnail(url=target.avatar_url) 133 | 134 | fields = [("Member", target.name, False), 135 | ("Actioned by", ctx.author.display_name, False), 136 | ("Reason", reason, False)] 137 | 138 | for name, value, inline in fields: 139 | embed.add_field(name=name, value=value, inline=inline) 140 | 141 | await self.log_channel.send(embed=embed) 142 | 143 | await ctx.send("Action complete.") 144 | 145 | @command(name="clear", aliases=["purge"]) 146 | @bot_has_permissions(manage_messages=True) 147 | @has_permissions(manage_messages=True) 148 | async def clear_messages(self, ctx, targets: Greedy[Member], limit: Optional[int] = 1): 149 | def _check(message): 150 | return not len(targets) or message.author in targets 151 | 152 | if 0 < limit <= 100: 153 | with ctx.channel.typing(): 154 | await ctx.message.delete() 155 | deleted = await ctx.channel.purge(limit=limit, after=datetime.utcnow()-timedelta(days=14), 156 | check=_check) 157 | 158 | await ctx.send(f"Deleted {len(deleted):,} messages.", delete_after=5) 159 | 160 | else: 161 | await ctx.send("The limit provided is not within acceptable bounds.") 162 | 163 | async def mute_members(self, message, targets, hours, reason): 164 | unmutes = [] 165 | 166 | for target in targets: 167 | if not self.mute_role in target.roles: 168 | if message.guild.me.top_role.position > target.top_role.position: 169 | role_ids = ",".join([str(r.id) for r in target.roles]) 170 | end_time = datetime.utcnow() + timedelta(seconds=hours) if hours else None 171 | 172 | db.execute("INSERT INTO mutes VALUES (?, ?, ?)", 173 | target.id, role_ids, getattr(end_time, "isoformat", lambda: None)()) 174 | 175 | await target.edit(roles=[self.mute_role]) 176 | 177 | embed = Embed(title="Member muted", 178 | colour=0xDD2222, 179 | timestamp=datetime.utcnow()) 180 | 181 | embed.set_thumbnail(url=target.avatar_url) 182 | 183 | fields = [("Member", target.display_name, False), 184 | ("Actioned by", message.author.display_name, False), 185 | ("Duration", f"{hours:,} hour(s)" if hours else "Indefinite", False), 186 | ("Reason", reason, False)] 187 | 188 | for name, value, inline in fields: 189 | embed.add_field(name=name, value=value, inline=inline) 190 | 191 | await self.log_channel.send(embed=embed) 192 | 193 | if hours: 194 | unmutes.append(target) 195 | 196 | return unmutes 197 | 198 | @command(name="mute") 199 | @bot_has_permissions(manage_roles=True) 200 | @has_permissions(manage_roles=True, manage_guild=True) 201 | async def mute_command(self, ctx, targets: Greedy[Member], hours: Optional[int], *, 202 | reason: Optional[str] = "No reason provided."): 203 | if not len(targets): 204 | await ctx.send("One or more required arguments are missing.") 205 | 206 | else: 207 | unmutes = await self.mute_members(ctx.message, targets, hours, reason) 208 | await ctx.send("Action complete.") 209 | 210 | if len(unmutes): 211 | await sleep(hours) 212 | await self.unmute_members(ctx.guild, targets) 213 | 214 | @mute_command.error 215 | async def mute_command_error(self, ctx, exc): 216 | if isinstance(exc, CheckFailure): 217 | await ctx.send("Insufficient permissions to perform that task.") 218 | 219 | async def unmute_members(self, guild, targets, *, reason="Mute time expired."): 220 | for target in targets: 221 | if self.mute_role in target.roles: 222 | role_ids = db.field("SELECT RoleIDs FROM mutes WHERE UserID = ?", target.id) 223 | roles = [guild.get_role(int(id_)) for id_ in role_ids.split(",") if len(id_)] 224 | 225 | db.execute("DELETE FROM mutes WHERE UserID = ?", target.id) 226 | 227 | await target.edit(roles=roles) 228 | 229 | embed = Embed(title="Member unmuted", 230 | colour=0xDD2222, 231 | timestamp=datetime.utcnow()) 232 | 233 | embed.set_thumbnail(url=target.avatar_url) 234 | 235 | fields = [("Member", target.display_name, False), 236 | ("Reason", reason, False)] 237 | 238 | for name, value, inline in fields: 239 | embed.add_field(name=name, value=value, inline=inline) 240 | 241 | await self.log_channel.send(embed=embed) 242 | 243 | @command(name="unmute") 244 | @bot_has_permissions(manage_roles=True) 245 | @has_permissions(manage_roles=True, manage_guild=True) 246 | async def unmute_command(self, ctx, targets: Greedy[Member], *, reason: Optional[str] = "No reason provided."): 247 | if not len(targets): 248 | await ctx.send("One or more required arguments is missing.") 249 | 250 | else: 251 | await self.unmute_members(ctx.guild, targets, reason=reason) 252 | 253 | @command(name="addprofanity", aliases=["addswears", "addcurses"]) 254 | @has_permissions(manage_guild=True) 255 | async def add_profanity(self, ctx, *words): 256 | with open("./data/profanity.txt", "a", encoding="utf-8") as f: 257 | f.write("".join([f"{w}\n" for w in words])) 258 | 259 | profanity.load_censor_words_from_file("./data/profanity.txt") 260 | await ctx.send("Action complete.") 261 | 262 | @command(name="delprofanity", aliases=["delswears", "delcurses"]) 263 | @has_permissions(manage_guild=True) 264 | async def remove_profanity(self, ctx, *words): 265 | with open("./data/profanity.txt", "r", encoding="utf-8") as f: 266 | stored = [w.strip() for w in f.readlines()] 267 | 268 | with open("./data/profanity.txt", "w", encoding="utf-8") as f: 269 | f.write("".join([f"{w}\n" for w in stored if w not in words])) 270 | 271 | profanity.load_censor_words_from_file("./data/profanity.txt") 272 | await ctx.send("Action complete.") 273 | 274 | @Cog.listener() 275 | async def on_ready(self): 276 | if not self.bot.ready: 277 | self.log_channel = self.bot.get_channel(759432499221889034) 278 | self.mute_role = self.bot.guild.get_role(653941858128494600) 279 | 280 | self.bot.cogs_ready.ready_up("mod") 281 | 282 | @Cog.listener() 283 | async def on_message(self, message): 284 | def _check(m): 285 | return (m.author == message.author 286 | and len(m.mentions) 287 | and (datetime.utcnow()-m.created_at).seconds < 60) 288 | 289 | if not message.author.bot: 290 | if len(list(filter(lambda m: _check(m), self.bot.cached_messages))) >= 3: 291 | await message.channel.send("Don't spam mentions!", delete_after=10) 292 | unmutes = await self.mute_members(message, [message.author], 5, reason="Mention spam") 293 | 294 | if len(unmutes): 295 | await sleep(5) 296 | await self.unmute_members(message.guild, [message.author]) 297 | 298 | elif profanity.contains_profanity(message.content): 299 | await message.delete() 300 | await message.channel.send("You can't use that word here.", delete_after=10) 301 | 302 | #XX commented out so it doesn't interfere with the rest of the server while recording 303 | # elif message.channel.id not in self.links_allowed and search(self.url_regex, message.content): 304 | # await message.delete() 305 | # await message.channel.send("You can't send links in this channel.", delete_after=10) 306 | 307 | # elif (message.channel.id not in self.images_allowed 308 | # and any([hasattr(a, "width") for a in message.attachments])): 309 | # await message.delete() 310 | # await message.channel.send("You can't send images here.", delete_after=10) 311 | 312 | 313 | def setup(bot): 314 | bot.add_cog(Mod(bot)) 315 | -------------------------------------------------------------------------------- /lib/cogs/reactions.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from random import choice 3 | 4 | from discord import Embed 5 | from discord.ext.commands import Cog 6 | from discord.ext.commands import command, has_permissions 7 | 8 | from ..db import db 9 | 10 | # Here are all the number emotes. 11 | # 0⃣ 1️⃣ 2⃣ 3⃣ 4⃣ 5⃣ 6⃣ 7⃣ 8⃣ 9⃣ 12 | 13 | numbers = ("1️⃣", "2⃣", "3⃣", "4⃣", "5⃣", 14 | "6⃣", "7⃣", "8⃣", "9⃣", "🔟") 15 | 16 | 17 | class Reactions(Cog): 18 | def __init__(self, bot): 19 | self.bot = bot 20 | self.polls = [] 21 | self.giveaways = [] 22 | 23 | @Cog.listener() 24 | async def on_ready(self): 25 | if not self.bot.ready: 26 | self.colours = { 27 | "❤️": self.bot.guild.get_role(653940117680947232), # Red 28 | "💛": self.bot.guild.get_role(653940192780222515), # Yellow 29 | "💚": self.bot.guild.get_role(653940254293622794), # Green 30 | "💙": self.bot.guild.get_role(653940277761015809), # Blue 31 | "💜": self.bot.guild.get_role(653940305300815882), # Purple 32 | "🖤": self.bot.guild.get_role(653940328453373952), # Black 33 | } 34 | self.reaction_message = await self.bot.get_channel(759432499221889034).fetch_message(759434223802253362) 35 | self.starboard_channel = self.bot.get_channel(759432499221889034) 36 | self.bot.cogs_ready.ready_up("reactions") 37 | 38 | @command(name="createpoll", aliases=["mkpoll"]) 39 | @has_permissions(manage_guild=True) 40 | async def create_poll(self, ctx, hours: int, question: str, *options): 41 | if len(options) > 10: 42 | await ctx.send("You can only supply a maximum of 10 options.") 43 | 44 | else: 45 | embed = Embed(title="Poll", 46 | description=question, 47 | colour=ctx.author.colour, 48 | timestamp=datetime.utcnow()) 49 | 50 | fields = [("Options", "\n".join([f"{numbers[idx]} {option}" for idx, option in enumerate(options)]), False), 51 | ("Instructions", "React to cast a vote!", False)] 52 | 53 | for name, value, inline in fields: 54 | embed.add_field(name=name, value=value, inline=inline) 55 | 56 | message = await ctx.send(embed=embed) 57 | 58 | for emoji in numbers[:len(options)]: 59 | await message.add_reaction(emoji) 60 | 61 | self.polls.append((message.channel.id, message.id)) 62 | 63 | self.bot.scheduler.add_job(self.complete_poll, "date", run_date=datetime.now()+timedelta(seconds=hours), 64 | args=[message.channel.id, message.id]) 65 | 66 | @command(name="giveaway") 67 | @has_permissions(manage_guild=True) 68 | async def create_giveaway(self, ctx, mins: int, *, description: str): 69 | embed = Embed(title="Giveaway", 70 | description=description, 71 | colour=ctx.author.colour, 72 | timestamp=datetime.utcnow()) 73 | 74 | fields = [("End time", f"{datetime.utcnow()+timedelta(seconds=mins*60)} UTC", False)] 75 | 76 | for name, value, inline in fields: 77 | embed.add_field(name=name, value=value, inline=inline) 78 | 79 | message = await ctx.send(embed=embed) 80 | await message.add_reaction("✅") 81 | 82 | self.giveaways.append((message.channel.id, message.id)) 83 | 84 | self.bot.scheduler.add_job(self.complete_giveaway, "date", run_date=datetime.now()+timedelta(seconds=mins), 85 | args=[message.channel.id, message.id]) 86 | 87 | async def complete_poll(self, channel_id, message_id): 88 | message = await self.bot.get_channel(channel_id).fetch_message(message_id) 89 | 90 | most_voted = max(message.reactions, key=lambda r: r.count) 91 | 92 | await message.channel.send(f"The results are in and option {most_voted.emoji} was the most popular with {most_voted.count-1:,} votes!") 93 | self.polls.remove((message.channel.id, message.id)) 94 | 95 | async def complete_giveaway(self, channel_id, message_id): 96 | message = await self.bot.get_channel(channel_id).fetch_message(message_id) 97 | 98 | if len((entrants := [u for u in await message.reactions[0].users().flatten() if not u.bot])) > 0: 99 | winner = choice(entrants) 100 | await message.channel.send(f"Congratulations {winner.mention} - you won the giveaway!") 101 | self.giveaways.remove((message.channel.id, message.id)) 102 | 103 | else: 104 | await message.channel.send("Giveaway ended - no one entered!") 105 | self.giveaways.remove((message.channel.id, message.id)) 106 | 107 | @Cog.listener() 108 | async def on_raw_reaction_add(self, payload): 109 | if self.bot.ready and payload.message_id == self.reaction_message.id: 110 | current_colours = filter(lambda r: r in self.colours.values(), payload.member.roles) 111 | await payload.member.remove_roles(*current_colours, reason="Colour role reaction.") 112 | await payload.member.add_roles(self.colours[payload.emoji.name], reason="Colour role reaction.") 113 | await self.reaction_message.remove_reaction(payload.emoji, payload.member) 114 | 115 | elif payload.message_id in (poll[1] for poll in self.polls): 116 | message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) 117 | 118 | for reaction in message.reactions: 119 | if (not payload.member.bot 120 | and payload.member in await reaction.users().flatten() 121 | and reaction.emoji != payload.emoji.name): 122 | await message.remove_reaction(reaction.emoji, payload.member) 123 | 124 | elif payload.emoji.name == "⭐": 125 | message = await self.bot.get_channel(payload.channel_id).fetch_message(payload.message_id) 126 | 127 | if not message.author.bot and payload.member.id != message.author.id: 128 | msg_id, stars = db.record("SELECT StarMessageID, Stars FROM starboard WHERE RootMessageID = ?", 129 | message.id) or (None, 0) 130 | 131 | embed = Embed(title="Starred message", 132 | colour=message.author.colour, 133 | timestamp=datetime.utcnow()) 134 | 135 | fields = [("Author", message.author.mention, False), 136 | ("Content", message.content or "See attachment", False), 137 | ("Stars", stars+1, False)] 138 | 139 | for name, value, inline in fields: 140 | embed.add_field(name=name, value=value, inline=inline) 141 | 142 | if len(message.attachments): 143 | embed.set_image(url=message.attachments[0].url) 144 | 145 | if not stars: 146 | star_message = await self.starboard_channel.send(embed=embed) 147 | db.execute("INSERT INTO starboard (RootMessageID, StarMessageID) VALUES (?, ?)", 148 | message.id, star_message.id) 149 | 150 | else: 151 | star_message = await self.starboard_channel.fetch_message(msg_id) 152 | await star_message.edit(embed=embed) 153 | db.execute("UPDATE starboard SET Stars = Stars + 1 WHERE RootMessageID = ?", message.id) 154 | 155 | else: 156 | await message.remove_reaction(payload.emoji, payload.member) 157 | 158 | 159 | def setup(bot): 160 | bot.add_cog(Reactions(bot)) -------------------------------------------------------------------------------- /lib/cogs/welcome.py: -------------------------------------------------------------------------------- 1 | from discord import Forbidden 2 | from discord.ext.commands import Cog 3 | from discord.ext.commands import command 4 | 5 | from ..db import db 6 | 7 | 8 | class Welcome(Cog): 9 | def __init__(self, bot): 10 | self.bot = bot 11 | 12 | @Cog.listener() 13 | async def on_ready(self): 14 | if not self.bot.ready: 15 | self.bot.cogs_ready.ready_up("welcome") 16 | 17 | @Cog.listener() 18 | async def on_member_join(self, member): 19 | db.execute("INSERT INTO exp (UserID) VALUES (?)", member.id) 20 | await self.bot.get_channel(759432499221889034).send(f"Welcome to **{member.guild.name}** {member.mention}! Head over to <#626608699942764548> to say hi!") 21 | 22 | try: 23 | await member.send(f"Welcome to **{member.guild.name}**! Enjoy your stay!") 24 | 25 | except Forbidden: 26 | pass 27 | 28 | await member.add_roles(member.guild.get_role(626609604813651979), member.guild.get_role(626609649294114857)) 29 | 30 | @Cog.listener() 31 | async def on_member_remove(self, member): 32 | db.execute("DELETE FROM exp WHERE UserID = ?", member.id) 33 | await self.bot.get_channel(759432499221889034).send(f"{member.display_name} has left {member.guild.name}.") 34 | 35 | 36 | def setup(bot): 37 | bot.add_cog(Welcome(bot)) -------------------------------------------------------------------------------- /lib/db/__init__.py: -------------------------------------------------------------------------------- 1 | from . import db 2 | 3 | db.build() -------------------------------------------------------------------------------- /lib/db/db.py: -------------------------------------------------------------------------------- 1 | from os.path import isfile 2 | from sqlite3 import connect 3 | 4 | from apscheduler.triggers.cron import CronTrigger 5 | 6 | DB_PATH = "./data/db/database.db" 7 | BUILD_PATH = "./data/db/build.sql" 8 | 9 | cxn = connect(DB_PATH, check_same_thread=False) 10 | cur = cxn.cursor() 11 | 12 | 13 | def with_commit(func): 14 | def inner(*args, **kwargs): 15 | func(*args, **kwargs) 16 | commit() 17 | 18 | return inner 19 | 20 | 21 | @with_commit 22 | def build(): 23 | if isfile(BUILD_PATH): 24 | scriptexec(BUILD_PATH) 25 | 26 | 27 | def commit(): 28 | cxn.commit() 29 | 30 | 31 | def autosave(sched): 32 | sched.add_job(commit, CronTrigger(second=0)) 33 | 34 | 35 | def close(): 36 | cxn.close() 37 | 38 | 39 | def field(command, *values): 40 | cur.execute(command, tuple(values)) 41 | 42 | if (fetch := cur.fetchone()) is not None: 43 | return fetch[0] 44 | 45 | 46 | def record(command, *values): 47 | cur.execute(command, tuple(values)) 48 | 49 | return cur.fetchone() 50 | 51 | 52 | def records(command, *values): 53 | cur.execute(command, tuple(values)) 54 | 55 | return cur.fetchall() 56 | 57 | 58 | def column(command, *values): 59 | cur.execute(command, tuple(values)) 60 | 61 | return [item[0] for item in cur.fetchall()] 62 | 63 | 64 | def execute(command, *values): 65 | cur.execute(command, tuple(values)) 66 | 67 | 68 | def multiexec(command, valueset): 69 | cur.executemany(command, valueset) 70 | 71 | 72 | def scriptexec(path): 73 | with open(path, "r", encoding="utf-8") as script: 74 | cur.executescript(script.read()) -------------------------------------------------------------------------------- /utils/xptest.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | 3 | xp = 0 4 | lvl = 0 5 | 6 | for i in range(1000): 7 | xp += randint(10, 20) 8 | lvl = int((xp//42) ** 0.55) 9 | print(i+1, xp, lvl) --------------------------------------------------------------------------------