├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .pylintrc ├── LICENSE ├── README.md ├── cogs ├── config.py ├── embeds.py ├── img │ └── color_examples.png ├── resource │ └── embeds.yaml ├── roles.py ├── testing.py └── utils │ ├── __init__.py │ ├── checks.py │ ├── db.py │ ├── generic.py │ ├── migration.py │ └── migration │ ├── 001.sql │ ├── 002.sql │ ├── 003.sql │ ├── 004.sql │ ├── 005.sql │ └── 006.sql ├── main.py └── requirements.txt /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: DEADBEAR 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: 'Bug Report: ' 5 | labels: bug 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 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: 'Feature Request: ' 5 | labels: enhancement 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | *.log 26 | *.db* 27 | 28 | # OS generated files # 29 | ###################### 30 | .DS_Store 31 | .DSStore? 32 | . 33 | .Spotlight-V100 34 | .Trashes 35 | ehthumbs.db 36 | Thumbs.db 37 | __pycache__ 38 | 39 | # Files needed for operation # 40 | ###################### 41 | *.env 42 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | disable = no-member,relative-beyond-top-level -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Austin Brandt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DeadbearBot 2 | ![Example Image](https://i.imgur.com/p5hxboO.gif) 3 | 4 | ## What's it do? 5 | Currently, DeadbearBot can: 6 | * Track and display configurable user profiles! 7 | * Award XP, levels, and currency to users automatically! 8 | * Allow users to buy custom roles and submit custom emojis through a configurable shop! 9 | * Preserve popular messages (i.e. ones with lots of reactions) to a "Star Board"! 10 | * Assign or unassign roles, either manually or automatically! 11 | * Set custom greeting and/or leave messages! 12 | * Automatically change a user's roles when connecting to voice channels! 13 | * Allow users to give themselves roles by clicking emoji reactions! 14 | * Set custom messages to be sent when users gain or lose a role! 15 | * Take a message and put it in an nicely-formatted embed! 16 | 17 | ## What's planned for the future? 18 | The current to-do list can be found in Issues under the "Enhancement" tag. In addition to these, I'd also like to eventually add: 19 | * A deployment system that allows the bot to be easily set up with no fuss (docker maybe?) 20 | * A pre-hosted version of the bot that can simply be invited to a server, no self-hosting required 21 | 22 | ## How do I use it? 23 | 1. Create a bot application through the [Discord Developer Portal](https://discord.com/developers/applications/) 24 | 2. Under the "Bot" tab, toggle on the "Presence" and "Server Members" intents 25 | 3. Invite your bot to your server with administrator privileges 26 | 4. Clone this repository, or download and extract the files manually 27 | 5. Install Python (v3.8.6 is recommended) and the requirements from requirements.txt 28 | 6. In a terminal, navigate to the directory where you saved the repo and run `python main.py` 29 | 7. When prompted, enter your bot's secret token (found in the "Bot" tab of the Developer Portal, under "Token") 30 | 31 | ## Command List ## 32 | [-help] - Display information about available commands 33 | 34 | ### Profile Commands 35 | [-prof / -profile] - Display your profile information 36 | [-prof e / edit] - Bring up the profile management menu to change your profile display 37 | [-lb / -leaderboard] - Display the server leaderboard 38 | 39 | ### Shop Commands 40 | [-shop] - Bring up the shop 41 | [-daily / -cashme / -getmoney] - Get a free boost of credits, available once every 24 hours 42 | 43 | ### Basic Config Commands (owner only) 44 | [-prefix] - Change the [prefix] for bot commands (default: '-') 45 | [-stats] - Enable or disable XP tracking and level-ups for the server 46 | [-permrole / -permissionrole] - Set a [role_id] that's required to be able to use non-owner commands 47 | 48 | ### Utility Commands (owner only) 49 | [-say / -botsay] - Have the bot send some [text] in an embed 50 | [-say e / edit] - Edit a [message_id] with a different set of [text] 51 | [-tr / -togglerole] - Give or remove a [role_id] to yourself or a [member] 52 | [-roles] - Get a list of all roles on the guild 53 | [-channels] - Get a list of all channels on the guild 54 | [-emojis] - Get a list of all custom emojis on the guild 55 | 56 | ### Star Board Commands (owner only) 57 | [-star / -starboard] - Enable saving messages by setting a [channel_id] for them to go to 58 | [-star t / threshold] - Set [threshold] of emoji reacts before a message is starred 59 | 60 | ### Shop Config Commands (owner only) 61 | [-csymbol / -currencysymbol] - Set the [emoji] to use as the currency symbol 62 | [-shop a role / available role] - Set the number of custom roles available for purchase in the shop 63 | [-shop p role / price role] - Set the price of custom roles 64 | [-shop a emoji / available emoji] - Set the number of custom emoji slots available for purchase in the shop 65 | [-shop p emoji / price emoji] - Set the price of custom emoji slots 66 | 67 | ### Join/Leave Commands (owner only) 68 | [-gj / -guildjoin] - Set a [channel_id] for greeting messages to be sent when new people join 69 | [-gj msg / message] - Set the [text] to be sent when new people join 70 | [-gl / -guildleave] - Set a [channel_id] for greeting messages to be sent when new people join 71 | [-gl msg / message] - Set the [text] to be sent when new people join 72 | [-ar / -autorole] - Set a [role_id] to be added to any member that joins the server 73 | 74 | ### Role Alert Commands (owner only) 75 | [-ra / -rolealert] - Empty command, requires a subcommand. 76 | [-ra g / gain] - Pass a [role_id], [channel_id], and a [message] to have that message sent to that channel when a user gains that role 77 | [-ra l / lose] - Pass a [role_id], [channel_id], and a [message] to have that message sent to that channel when a user loses that role 78 | 79 | ### Reaction Role Commands (owner only) 80 | [-rr / -reactionrole] - Set a [channel-message] to have a react [emoji] that when clicked gives a role by its [role_id] 81 | [-rr d / delete] - Deletes a reaction role by its [unique_id] 82 | [-rr l / list] - Lists all reaction roles and their id's 83 | 84 | ### Voice Auto-role Commands (owner only) 85 | [-vr / -voicerole] - Set a [voice channel_id] to have a [role_id] that gets automatically added/removed when that channel is joined or left 86 | [-vr d / delete] - Deletes a voice role by its [unique_id] 87 | [-vr l / list] - Lists all voice roles and their id's 88 | 89 | ## Credits 90 | This bot is made possible thanks to: 91 | - The [discord.py project](https://github.com/Rapptz/discord.py) 92 | - The [aiosqlite module](https://github.com/jreese/aiosqlite) 93 | - The [python-dotenv module](https://github.com/theskumar/python-dotenv) 94 | - The [PyYAML module](https://github.com/yaml/pyyaml) 95 | - Inspiration from the project [NadekoBot](https://nadeko.bot/) 96 | 97 | If you like this project, consider donating to any of the projects listed above! 98 | -------------------------------------------------------------------------------- /cogs/config.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | from typing import Union, Optional 4 | import discord 5 | from discord.ext import commands 6 | from .utils import db 7 | from .utils import checks 8 | 9 | 10 | class Config(commands.Cog): 11 | def __init__(self, bot): 12 | self.bot = bot 13 | 14 | 15 | # Set an alias for the bot prefix 16 | @commands.command( 17 | name='PrefixAlias', 18 | description="Sets an alias for the default command prefix.", 19 | brief="Set command prefix alias.", 20 | aliases=['prefixalias', 'prefix', 'pre']) 21 | @commands.guild_only() 22 | @commands.is_owner() 23 | async def change_prefix(self, ctx, prefix): 24 | await db.set_cfg(ctx.guild.id, 'bot_alias', prefix) 25 | await ctx.channel.send(f"My command prefix is now \"{prefix}\".") 26 | 27 | 28 | # Set perm roles for public commands 29 | @commands.command( 30 | name='PermissionRole', 31 | description="Sets role that can use basic commands.", 32 | brief="Set permrole.", 33 | aliases=['permissionrole', 'permrole', 'pr']) 34 | @commands.guild_only() 35 | @commands.is_owner() 36 | async def set_perms(self, ctx, role: discord.Role): 37 | await db.set_cfg(ctx.guild.id, 'perm_role', role.id) 38 | await ctx.channel.send(f"Added \"{role.name}\" to perm roles.") 39 | 40 | 41 | # Set the channel for join messages 42 | @commands.group( 43 | name='GuildJoin', 44 | description="Enables or disables the automatic join message in a " 45 | "specified channel. Pass no channel to disable.", 46 | brief="Turn join messages on or off.", 47 | aliases=['guildjoin', 'gjoin', 'gj'], 48 | invoke_without_command=True) 49 | @commands.guild_only() 50 | @commands.is_owner() 51 | async def guild_join(self, ctx, channel: discord.TextChannel=None): 52 | if channel: 53 | await db.set_cfg(ctx.guild.id, 'join_channel', channel.id) 54 | await ctx.channel.send(f"Greeting enabled for \"{channel.name}\".") 55 | else: 56 | await db.set_cfg(ctx.guild.id, 'join_channel', None) 57 | await ctx.channel.send("Greeting disabled.") 58 | 59 | 60 | # Set the join message 61 | @guild_join.command( 62 | name='Message', 63 | description="Sets the automatic greeting message.", 64 | brief="Modify join message.", 65 | aliases=['message', 'msg']) 66 | @commands.guild_only() 67 | @commands.is_owner() 68 | async def gjoin_message(self, ctx, *, message: str): 69 | await db.set_cfg(ctx.guild.id, 'join_message', message) 70 | await ctx.channel.send(f"The join message is now: \"{message}\"") 71 | 72 | 73 | # Set the channel for leave messages 74 | @commands.group( 75 | name='GuildLeave', 76 | description="Enables or disables the automatic leave message in a " 77 | "specified channel. Pass no channel to disable.", 78 | brief="Turn leave message on or off.", 79 | aliases=['guildleave', 'gleave', 'gl'], 80 | invoke_without_command=True) 81 | @commands.guild_only() 82 | @commands.is_owner() 83 | async def guild_leave(self, ctx, channel: discord.TextChannel=None): 84 | if channel: 85 | await db.set_cfg(ctx.guild.id, 'leave_channel', channel.id) 86 | await ctx.channel.send(f"Farewells enabled for \"{channel.name}\".") 87 | else: 88 | await db.set_cfg(ctx.guild.id, 'leave_channel', None) 89 | await ctx.channel.send("Farewells disabled.") 90 | 91 | 92 | # Set the leave message 93 | @guild_leave.command( 94 | name='Message', 95 | description="Sets the automatic leave message.", 96 | brief="Modify leave message.", 97 | aliases=['message', 'msg']) 98 | @commands.guild_only() 99 | @commands.is_owner() 100 | async def gleave_message(self, ctx, *, message: str): 101 | await db.set_cfg(ctx.guild.id, 'leave_message', message) 102 | await ctx.channel.send(f"The farewell message is now: \"{message}\"") 103 | 104 | 105 | # Set the currency symbol 106 | @commands.command( 107 | name='CurrencySymbol', 108 | description="Sets the server currency symbol.", 109 | aliases=['currencysymbol', 'csymbol']) 110 | @commands.guild_only() 111 | @commands.is_owner() 112 | async def set_currency(self, ctx, emoji: Union[discord.Emoji, str]): 113 | if type(emoji) is str: 114 | await db.set_cfg(ctx.guild.id, 'currency', emoji) 115 | else: 116 | await db.set_cfg(ctx.guild.id, 'currency', emoji.id) 117 | await ctx.channel.send(f"The currency symbol is now: \"{emoji}\"") 118 | 119 | 120 | # Toggle guild stat tracking 121 | @commands.command( 122 | name='Stats', 123 | description="Toggles guild stats.", 124 | aliases=['stats']) 125 | @commands.guild_only() 126 | @commands.is_owner() 127 | async def stats(self, ctx): 128 | stats = await db.get_cfg(ctx.guild.id, 'guild_stats') 129 | if stats: 130 | reply = "Guild stats have been disabled!" 131 | await db.set_cfg(ctx.guild.id, 'guild_stats', None) 132 | else: 133 | reply = "Guild stats have been enabled!" 134 | await db.set_cfg(ctx.guild.id, 'guild_stats', 'enabled') 135 | await ctx.channel.send(reply) 136 | 137 | 138 | # Manage starboard settings 139 | @commands.group( 140 | name='Starboard', 141 | description="Sets the configuration for starred messages.", 142 | brief="Modify starboard settings.", 143 | aliases=['starboard', 'star']) 144 | @commands.guild_only() 145 | @commands.is_owner() 146 | async def starboard(self, ctx, channel: discord.TextChannel=None): 147 | starboard = await db.get_cfg(ctx.guild.id, 'star_channel') 148 | if starboard is None: 149 | await db.set_cfg(ctx.guild.id, 'star_channel', channel.id) 150 | await ctx.channel.send(f"Set \"{channel.name}\" as the star board.") 151 | else: 152 | await db.set_cfg(ctx.guild.id, 'star_channel', None) 153 | await ctx.channel.send(f"Starboard disabled.") 154 | 155 | 156 | # Change starboard threshold 157 | @starboard.command( 158 | name='Threshold', 159 | description="Sets the configuration for starred messages.", 160 | brief="Modify starboard settings.", 161 | aliases=['threshold', 't']) 162 | @commands.guild_only() 163 | @commands.is_owner() 164 | async def star_threshold(self, ctx, threshold): 165 | await db.set_cfg(ctx.guild.id, 'star_threshold', threshold) 166 | await ctx.channel.send(f"Starboard threshold set to {threshold}") 167 | 168 | 169 | # Event hook for reactions being added to messages 170 | @commands.Cog.listener() 171 | async def on_raw_reaction_add(self, payload): 172 | if payload.user_id == self.bot.user.id: 173 | return 174 | elif payload.guild_id: 175 | await self.star_check(payload, 'add') 176 | 177 | 178 | # Event hook for reactions being removed from messages 179 | @commands.Cog.listener() 180 | async def on_raw_reaction_remove(self, payload): 181 | if payload.user_id == self.bot.user.id: 182 | return 183 | elif payload.guild_id: 184 | await self.star_check(payload, 'rem') 185 | 186 | 187 | # Do stuff when a message is sent 188 | @commands.Cog.listener() 189 | async def on_message(self, message): 190 | if not message.author.bot and message.guild: 191 | dbcfg = await db.get_cfg(message.guild.id) 192 | if dbcfg['guild_stats'] == 'enabled': 193 | guildID = message.guild.id 194 | member = message.author 195 | profile = await db.get_member(guildID, member.id) 196 | cashaward = random.randrange( 197 | dbcfg['min_cash'], 198 | dbcfg['max_cash']) 199 | await db.add_currency(message.guild.id, member.id, cashaward) 200 | curxp = profile['xp'] + 1 201 | await db.set_member(guildID, member.id, 'xp', curxp) 202 | nextlevel = profile['lvl'] + 1 203 | levelup = math.floor(curxp / ((2 * nextlevel) ** 2)) 204 | if levelup == 1: 205 | channel = message.channel 206 | await channel.send(f"**{member.name}** has leveled up to " 207 | f"**level {nextlevel}!**") 208 | await db.set_member(guildID, member.id, 'lvl', nextlevel) 209 | 210 | 211 | # Handler for guild reaction events 212 | async def star_check(self, payload, event): 213 | dbcfg = await db.get_cfg(payload.guild_id) 214 | if not dbcfg['star_channel']: 215 | return 216 | guild = self.bot.get_guild(payload.guild_id) 217 | channel = guild.get_channel(payload.channel_id) 218 | if channel.is_nsfw() and not dbcfg['star_nsfw']: 219 | return 220 | message = await channel.fetch_message(payload.message_id) 221 | if message.author.bot: 222 | return 223 | prevstar = await db.get_starred(message.id) 224 | starchannel = guild.get_channel(dbcfg['star_channel']) 225 | if not prevstar: 226 | for react in message.reactions: 227 | if react.count >= dbcfg['star_threshold']: 228 | await self.star_add(message, starchannel) 229 | break 230 | else: 231 | if len(message.reactions) < 2: 232 | await self.star_remove(starchannel, prevstar) 233 | else: 234 | for react in message.reactions: 235 | if react.count < dbcfg['star_threshold']: 236 | await self.star_remove(starchannel, prevstar) 237 | break 238 | 239 | 240 | # Add star to starboard 241 | async def star_add(self, message, starchannel): 242 | star = discord.Embed(description=message.content, 243 | color=0xf1c40f) 244 | star.set_author(name=message.author.display_name, 245 | icon_url=message.author.avatar_url) 246 | if message.attachments: 247 | images = [] 248 | files = [] 249 | filetypes = ('png', 'jpeg', 'jpg', 'gif', 'webp') 250 | for attachment in message.attachments: 251 | if attachment.url.lower().endswith(filetypes): 252 | images.append(attachment) 253 | else: 254 | files.append(attachment) 255 | for i, file in enumerate(files): 256 | star.add_field(name=f"Attachment {i + 1}", 257 | value=f"[{file.filename}]({file.url})", 258 | inline=True) 259 | star.set_thumbnail(url=files[0].url) 260 | star.add_field(name="--", 261 | value=f"[Jump to original...]({message.jump_url})", 262 | inline=False) 263 | star.set_footer(text="Originally sent") 264 | star.timestamp = message.created_at 265 | newstar = await starchannel.send(embed=star) 266 | await db.add_starred(message.guild.id, message.id, newstar.id) 267 | 268 | 269 | # Remove star from starboard 270 | async def star_remove(self, starchannel, starred): 271 | oldstar = await starchannel.fetch_message(starred['starred_id']) 272 | await oldstar.delete() 273 | await db.del_starred(starred['original_id']) 274 | -------------------------------------------------------------------------------- /cogs/embeds.py: -------------------------------------------------------------------------------- 1 | import math 2 | import sys 3 | import asyncio 4 | 5 | if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith('win'): 6 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 7 | 8 | from datetime import datetime, date, timedelta 9 | from pathlib import Path 10 | from yaml import load, dump 11 | try: 12 | from yaml import CLoader as Loader, CDumper as Dumper 13 | except ImportError: 14 | from yaml import Loader, Dumper 15 | import discord 16 | from discord.ext import commands 17 | from .utils import db 18 | from .utils import checks 19 | 20 | 21 | # Path to images directory 22 | IMAGES = Path(__file__).parent / "img" 23 | STRINGPATH = Path(__file__).parent / "resource" / "embeds.yaml" 24 | 25 | 26 | class Embeds(commands.Cog): 27 | def __init__(self, bot): 28 | self.bot = bot 29 | 30 | 31 | # Class definition for embedded menus 32 | class MenuEmbed(discord.Embed): 33 | def __init__(self, user, head, desc, fields, numbers=False): 34 | self.numbtns = [f"1\N{combining enclosing keycap}", 35 | f"2\N{combining enclosing keycap}", 36 | f"3\N{combining enclosing keycap}", 37 | f"4\N{combining enclosing keycap}", 38 | f"5\N{combining enclosing keycap}", 39 | f"6\N{combining enclosing keycap}", 40 | f"7\N{combining enclosing keycap}", 41 | f"8\N{combining enclosing keycap}", 42 | f"9\N{combining enclosing keycap}", 43 | f"\N{keycap ten}"] 44 | self.numbers = numbers 45 | self.navbtns = [u"\u25C0", u"\u25B6"] 46 | self.nav = False 47 | self.closebtn = u"\U0001F1FD" 48 | max = len(self.numbtns) 49 | self.pages = [] 50 | for i in range((len(fields)+max-1)//max): 51 | self.pages.append(fields[i*max:(i+1)*max]) 52 | self.page = 1 53 | self.selected = None 54 | self.user = user 55 | self.validemoji = None 56 | super().__init__(title=head, description=desc) 57 | super().set_footer(text=f"Page {self.page}/{len(self.pages)}") 58 | 59 | 60 | async def add_fields(self): 61 | if self.numbers: 62 | for i, item in enumerate(self.pages[self.page-1]): 63 | super().add_field( 64 | name=f"{i+1}. {item['fname']}", 65 | value=item['fdesc'], 66 | inline=item['inline']) 67 | self.validemoji = self.numbtns[:len(self.pages[self.page-1])] 68 | else: 69 | for item in self.pages[self.page - 1]: 70 | super().add_field( 71 | name=f"{item['fname']}", 72 | value=item['fdesc'], 73 | inline=item['inline']) 74 | 75 | 76 | async def add_control(self, message): 77 | self.message = message 78 | content = "**Updating reactji buttons, please wait...**" 79 | await self.message.edit(content=content) 80 | await self.message.add_reaction(self.closebtn) 81 | if not self.nav and len(self.pages) > 1: 82 | for item in self.navbtns: 83 | await self.message.add_reaction(item) 84 | self.nav = True 85 | if self.numbers: 86 | for index, item in enumerate(self.pages[self.page-1]): 87 | await self.message.add_reaction(self.numbtns[index]) 88 | await self.message.edit(content=None) 89 | return True 90 | 91 | 92 | async def process_reaction(self, emoji): 93 | if emoji == self.closebtn: 94 | return False 95 | elif self.numbers: 96 | if emoji in self.validemoji: 97 | index = self.validemoji.index(emoji) 98 | self.selected = self.pages[self.page-1][index] 99 | elif self.nav and (emoji in self.navbtns): 100 | if emoji == self.navbtns[0]: 101 | if self.page > 1: 102 | self.page -= 1 103 | elif self.page < len(self.pages): 104 | self.page += 1 105 | self.clear_fields() 106 | await self.add_fields() 107 | self.set_footer(text=f"Page {self.page}/{len(self.pages)}") 108 | await self.message.edit(embed=self) 109 | 110 | 111 | 112 | # Make the bot say things in a nice embedded way 113 | @commands.group( 114 | name='BotSay', 115 | description="Make the bot create an embedded message.", 116 | brief="Make the bot talk.", 117 | aliases=['botsay', 'say'], 118 | invoke_without_command=True) 119 | @commands.guild_only() 120 | @commands.is_owner() 121 | async def say(self, ctx, *, content: str=None): 122 | embed = discord.Embed(description=content) 123 | # Create a list of valid image formats for upload 124 | validfiles = [".jpg", ".jpeg", ".gif", ".png", ".bmp"] 125 | # If the message had an attachment, make sure it's a valid image 126 | # If it is, preserve the original message so the url stays valid 127 | try: 128 | if ctx.message.attachments[0]: 129 | attachment = Path(ctx.message.attachments[0].filename) 130 | if attachment.suffix in validfiles: 131 | imageurl = ctx.message.attachments[0].url 132 | embed.set_image(url=imageurl) 133 | else: 134 | await ctx.message.delete() 135 | except: 136 | await ctx.message.delete() 137 | await ctx.channel.send(embed=embed) 138 | 139 | 140 | # Edit an embedded message from the bot 141 | @say.command( 142 | name='Edit', 143 | description="Edit a previously created embedded message.", 144 | brief="Change a bot message.", 145 | aliases=['edit', 'e']) 146 | @commands.guild_only() 147 | @commands.is_owner() 148 | async def edit_say(self, ctx, message: discord.Message, *, content: str=None): 149 | # Only allow editing of the bot's messages 150 | if message.author != self.bot.user: 151 | await ctx.channel.send("I can't edit messages that aren't mine!") 152 | return 153 | # If no new text is passed in, preserve the original text as-is 154 | if content: 155 | embed = discord.Embed(description=content) 156 | else: 157 | embedlist = message.embeds 158 | content = embedlist[0].description 159 | if content: 160 | embed = discord.Embed(description=content) 161 | else: 162 | embed = discord.Embed() 163 | # If changing the attachment, get new image and leave invoke message 164 | if ctx.message.attachments: 165 | imageurl = ctx.message.attachments[0].url 166 | embed.set_image(url=imageurl) 167 | elif message.embeds[0].image: 168 | imageurl = message.embeds[0].image.url 169 | embed.set_image(url=imageurl) 170 | await ctx.message.delete() 171 | await message.edit(embed=embed) 172 | 173 | 174 | # Command to return a user's profile 175 | @commands.group( 176 | name='Profile', 177 | description="Display your profile information.", 178 | brief="Get your profile.", 179 | aliases=['profile', 'prof'], 180 | invoke_without_command=True) 181 | @commands.guild_only() 182 | @checks.check_perms() 183 | async def profile(self, ctx, *, member: discord.Member=None): 184 | # If a member was passed in, make sure it's not a bot 185 | if member: 186 | if member.bot: 187 | await ctx.channel.send("Bots don't have profiles!") 188 | return 189 | # If no member was passed in or found, assume invoker is target 190 | else: 191 | member = ctx.author 192 | # Get the DB profile info of the target 193 | dbprof = await db.get_member(ctx.guild.id, member.id) 194 | # Get the strings for the Profile menu from resources 195 | strings = await self.get_strings('Profile') 196 | # Input target's level and guild name 197 | head = strings['head'].format(dbprof['lvl']) 198 | desc = strings['desc'].format(ctx.guild.name) 199 | # If the target has set their birthday, replace the date with their age 200 | if dbprof['birthday']: 201 | dbprof = dict(dbprof) 202 | dt = datetime.strptime(dbprof['birthday'], '%Y-%m-%d') 203 | now = date.today() 204 | age = now.year-dt.year-((now.month, now.day)<(dt.month, dt.day)) 205 | dbprof['birthday'] = age 206 | # Add any existing profile info to strings, then append it to fields 207 | fields = [] 208 | for field in strings['fields']: 209 | key = field['data'] 210 | if dbprof[key] is not None: 211 | field['fdesc'] = f"{field['fdesc']}{dbprof[key]}" 212 | fields.append(field) 213 | # Create Profile menu using the updated strings 214 | Profile = self.MenuEmbed(ctx.author, head, desc, fields) 215 | await Profile.add_fields() 216 | # Fill out remaining un-set fields using target info 217 | Profile.set_author(name=f"{member.name}'s Profile") 218 | avatar = member.avatar_url_as(format='png') 219 | Profile.set_thumbnail(url=avatar) 220 | # Send embed, add controls, and then loop 221 | message = await ctx.channel.send(embed=Profile) 222 | controls = await Profile.add_control(message) 223 | if controls: 224 | await self.wait_for_select(Profile, 55.0) 225 | 226 | 227 | # Command to fill out profile info via DM 228 | @profile.command( 229 | name='Edit', 230 | description="Edit your member profile information.", 231 | brief="Edit profile information.", 232 | aliases=['edit', 'e']) 233 | @commands.guild_only() 234 | @checks.check_perms() 235 | async def profile_manager(self, ctx): 236 | strings = await self.get_strings('Manager') 237 | head = strings['head'] 238 | desc = strings['desc'] 239 | fields = strings['fields'] 240 | PM = self.MenuEmbed(ctx.author, head, desc, fields, True) 241 | await PM.add_fields() 242 | message = await ctx.channel.send(embed=PM) 243 | controls = await PM.add_control(message) 244 | if controls: 245 | await self.wait_for_select(PM, 55.0) 246 | if not PM.selected: 247 | return 248 | edit = "**Instructions being sent via DM, check your messages.**" 249 | await message.edit(content=edit, delete_after=5.0) 250 | reply = (f"{PM.selected['prompt']}\n\n" 251 | f"Character limit: {PM.selected['limit']}\n") 252 | await ctx.author.send(content=reply) 253 | await db.add_temp(ctx.guild.id, 254 | ctx.author.id, 255 | "Manager", 256 | PM.selected['data']) 257 | 258 | 259 | # Command for obtaining a daily cash award 260 | @commands.command( 261 | name='DailyCredits', 262 | aliases=['dailycredits', 'daily', 'credits', 'cashme', 'getmoney']) 263 | @commands.guild_only() 264 | @checks.check_perms() 265 | async def daily_cash(self, ctx): 266 | gID = ctx.guild.id 267 | member = ctx.author 268 | now = ctx.message.created_at 269 | dbprof = await db.get_member(gID, member.id) 270 | timeformat = '%Y-%m-%d %H:%M:%S.%f' 271 | if dbprof['daily_timestamp']: 272 | lastdaily = datetime.strptime(dbprof['daily_timestamp'], 273 | timeformat) 274 | timesince = now - lastdaily 275 | delta = timedelta(hours=24) 276 | else: 277 | timesince = 2 278 | delta = 1 279 | if timesince < delta: 280 | timeleft = delta - timesince 281 | hours = timeleft.seconds//3600 282 | minutes = (timeleft.seconds//60)%60 283 | seconds = math.ceil(timeleft.seconds) 284 | if hours: 285 | str = (f"I already gave you money, {member.mention}! If you " 286 | f"want more, you'll have to wait {hours} hour(s) and " 287 | f"{minutes} minute(s).") 288 | elif seconds > 60: 289 | str = (f"I already gave you money, {member.mention}! If you " 290 | f"want more, you'll have to wait {minutes} minute(s) " 291 | f"and {seconds} second(s).") 292 | else: 293 | str = (f"I already gave you money, {member.mention}! If you " 294 | f"want more, you'll have to wait {seconds} second(s).") 295 | await ctx.channel.send(str) 296 | else: 297 | newcash = dbprof['cash'] + 500 298 | await db.set_member(gID, member.id, 'cash', newcash) 299 | await db.set_member(gID, member.id, 'daily_timestamp', now) 300 | await ctx.channel.send( 301 | f"Here's a 500 credit freebie, {member.mention}!") 302 | 303 | 304 | # Command to transfer currency from one user to another 305 | @commands.command( 306 | name='Transfer', 307 | description="Transfer credits from one user to another.", 308 | aliases=['transfer']) 309 | @commands.guild_only() 310 | @checks.check_perms() 311 | async def transfer(self, ctx, amount: int, member: discord.Member): 312 | if member.bot: 313 | await ctx.channel.send("Bots don't have profiles!") 314 | return 315 | if amount < 1: 316 | await ctx.channel.send( 317 | "Minumum amount of credits to transfer is 1.") 318 | return 319 | if ctx.author == member: 320 | await ctx.channel.send("Can't transfer credits to yourself.") 321 | return 322 | dbprof_author = await db.get_member(ctx.author.guild.id, ctx.author.id) 323 | if amount > dbprof_author['cash']: 324 | await ctx.channel.send( 325 | "Provided amount is higher than owned credits.") 326 | return 327 | await db.transfer_currency(ctx.guild.id, ctx.author.id, member.id, amount) 328 | await ctx.channel.send( 329 | f"{ctx.author.mention} has given {amount} credits to {member.mention}.") 330 | 331 | 332 | # Error handler for transfer 333 | @transfer.error 334 | async def transfer_handler(self, ctx, error): 335 | if isinstance(error, commands.MissingRequiredArgument): 336 | if error.param.name == 'amount': 337 | await ctx.send( 338 | "You forgot to give an amount of credits to transfer.") 339 | return 340 | elif error.param.name == 'member': 341 | await ctx.send( 342 | "You forgot to mention a member you wish to transfer.") 343 | return 344 | elif isinstance(error, commands.BadArgument): 345 | if error.args[0] == 'Converting to "int" failed for parameter "amount".': 346 | await ctx.channel.send( 347 | "Please provide a valid amount of credits.") 348 | return 349 | else: 350 | await ctx.send("Member not found.") 351 | return 352 | else: 353 | raise error 354 | 355 | 356 | # Command to award currency to a user 357 | @commands.command( 358 | name='Award', 359 | description="Award a user with credits.", 360 | aliases=['award']) 361 | @commands.guild_only() 362 | @commands.is_owner() 363 | async def award(self, ctx, amount: int, member: discord.Member): 364 | if member.bot: 365 | await ctx.channel.send("Bots don't have profiles!") 366 | return 367 | if amount < 1: 368 | await ctx.channel.send( 369 | "Minumum amount of credits to award is 1.") 370 | return 371 | await db.add_currency(ctx.guild.id, member.id, amount) 372 | await ctx.channel.send( 373 | f"Awarded {amount} credits to {member.mention}.") 374 | 375 | 376 | # Command to remove currency from a user 377 | @commands.command( 378 | name='Seize', 379 | description="Remove credits from a user.", 380 | aliases=['seize']) 381 | @commands.guild_only() 382 | @commands.is_owner() 383 | async def seize(self, ctx, amount: int, member: discord.Member): 384 | if member.bot: 385 | await ctx.channel.send("Bots don't have profiles!") 386 | return 387 | if amount < 1: 388 | await ctx.channel.send( 389 | "Minumum amount of credits to remove is 1.") 390 | return 391 | await db.remove_currency(ctx.guild.id, member.id, amount) 392 | await ctx.channel.send( 393 | f"Removed {amount} credits from {member.mention}.") 394 | 395 | 396 | # Shop menu function. This is hacky and needs a lot of refinement. 397 | @commands.group( 398 | name='GuildShop', 399 | aliases=['guildshop', 'shop'], 400 | invoke_without_command=True) 401 | @commands.guild_only() 402 | @checks.check_perms() 403 | async def shop(self, ctx): 404 | # Get invoker profile for display of current balance 405 | dbprof = await db.get_member(ctx.guild.id, ctx.author.id) 406 | # Get guild-specific data for the shop values and currency symbol 407 | dbcfg = await db.get_cfg(ctx.guild.id) 408 | if dbcfg['currency'].isdigit(): 409 | try: 410 | dbcfg['currency'] = await commands.EmojiConverter().convert( 411 | ctx, 412 | dbcfg['currency']) 413 | except: 414 | dbcfg['currency'] = u"\U0001F48E" 415 | elif not dbcfg['currency']: 416 | dbcfg['currency'] = u"\U0001F48E" 417 | # Get default strings for the shop menu 418 | strings = await self.get_strings('Shop') 419 | # Set header and description of embed based on guild and invoker info 420 | head = strings['head'].format(ctx.guild.name) 421 | desc = strings['desc'].format( 422 | ctx.author.name, 423 | dbcfg['currency'], 424 | dbprof['cash']) 425 | # Modify strings to include guild-specific data 426 | fields = [] 427 | for item in strings['fields']: 428 | # Set price and availability of item 429 | if item['data'] == 'role': 430 | item['available'] = dbcfg['crole_available'] 431 | item['price'] = dbcfg['crole_price'] 432 | elif item['data'] == 'emoji': 433 | item['available'] = dbcfg['cemoji_available'] 434 | item['price'] = dbcfg['cemoji_price'] 435 | # Add availability to item description 436 | if item['available'] == 0: 437 | item['fdesc'] = ( 438 | f"{item['fdesc']}\n\n" 439 | f"Available: None") 440 | elif item['available'] < 0: 441 | item['fdesc'] = ( 442 | f"{item['fdesc']}\n\n" 443 | f"Available: \u221E") 444 | else: 445 | item['fdesc'] = ( 446 | f"{item['fdesc']}\n\n" 447 | f"Available: {item['available']}") 448 | # Add price to item description 449 | if item['price'] == 0: 450 | item['fdesc'] = ( 451 | f"{item['fdesc']}\n" 452 | f"Price: {dbcfg['currency']} Free") 453 | else: 454 | item['fdesc'] = ( 455 | f"{item['fdesc']}\n" 456 | f"Price: {dbcfg['currency']} {item['price']}") 457 | # Add file size and type limits to prompt 458 | if item['format'] == 'text': 459 | item['prompt'] = ( 460 | f"{item['prompt']}\n\n" 461 | f"Max length: {item['limit']} characters") 462 | elif item['format'] == 'image': 463 | item['prompt'] = ( 464 | f"{item['prompt']}\n\n" 465 | f"Max size: {int(item['limit']/1000)}kb\n" 466 | f"File types accepted: {item['types']}") 467 | # Append the modified item to fields list 468 | fields.append(item) 469 | # Create instance of Shop menu, add fields and controls, and then loop 470 | Shop = self.MenuEmbed(ctx.author, head, desc, fields, True) 471 | await Shop.add_fields() 472 | message = await ctx.channel.send(embed=Shop) 473 | controls = await Shop.add_control(message) 474 | if controls: 475 | bought = False 476 | while not bought: 477 | await self.wait_for_select(Shop, 55.0) 478 | # If no item was purchased, exit the command 479 | if not Shop.selected: 480 | break 481 | # If none of selected item are available, throw error to user 482 | elif Shop.selected['available'] == 0: 483 | edit = (f"**There are no {Shop.selected['fname']}'s " 484 | f"available. Please select another item.**") 485 | await message.edit(content=edit) 486 | # If user doesn't have enough credits, throw error to user 487 | elif dbprof['cash'] < Shop.selected['price']: 488 | edit = (f"**You don't have enough credits for a " 489 | f"{Shop.selected['fname']}!**") 490 | await message.edit(content=edit) 491 | # If checks pass, process purchase and set bought to true 492 | else: 493 | await db.remove_currency( 494 | ctx.guild.id, 495 | ctx.author.id, 496 | Shop.selected['price']) 497 | if Shop.selected['data'] == 'role': 498 | if Shop.selected['available'] > 0: 499 | await db.set_cfg( 500 | ctx.guild.id, 501 | 'crole_available', 502 | Shop.selected['available'] - 1) 503 | bought = True 504 | if not bought: 505 | return 506 | # If the item purchased requires a second step, send prompt2 507 | if Shop.selected['prompt2']: 508 | edit = f"**{Shop.selected['fname']} bought! Check your DM's.**" 509 | await message.edit( 510 | content=edit, 511 | suppress=True, 512 | delete_after=5.0) 513 | await db.add_temp( 514 | ctx.guild.id, 515 | ctx.author.id, 516 | "Shop", 517 | Shop.selected['data']) 518 | await ctx.author.send(content=Shop.selected['prompt']) 519 | else: 520 | edit = f"**{Shop.selected['fname']} bought!**" 521 | await message.edit( 522 | content=edit, 523 | suppress=True, 524 | delete_after=5.0) 525 | 526 | 527 | # Command to set item availability in the shop 528 | @shop.command( 529 | name='Available', 530 | aliases=['available', 'a']) 531 | @commands.guild_only() 532 | @checks.check_perms() 533 | async def available(self, ctx, data: str, input: int): 534 | strings = await self.get_strings('Shop') 535 | for item in strings['fields']: 536 | if item['data'] == data.lower(): 537 | selected = item 538 | if not selected: 539 | await ctx.channel.send("Shop item not found!") 540 | return 541 | if selected['data'] == 'emoji': 542 | if 'MORE_EMOJI' in ctx.guild.features: 543 | emojilimit = 200 544 | else: 545 | emojilimit = 50 546 | if (emojilimit - len(ctx.guild.emojis)) > 0: 547 | max = emojilimit - len(ctx.guild.emojis) 548 | if input > max or input < 0: 549 | await db.set_cfg(ctx.guild.id, 'cemoji_available', max) 550 | reply = (f"Available custom emoji in the shop set to " 551 | f"{max} (maximum available for this guild).") 552 | else: 553 | await db.set_cfg(ctx.guild.id, 'cemoji_available', input) 554 | reply = (f"Available custom emoji in the shop set to " 555 | f"{input}.") 556 | else: 557 | await ctx.channel.send("Max custom emoji slots reached!") 558 | return 559 | elif selected['data'] == 'role': 560 | await db.set_cfg(ctx.guild.id, 'crole_available', input) 561 | if input < 0: 562 | reply = (f"Set number of {selected['fname']} available in " 563 | f"the shop to infinite.") 564 | else: 565 | reply = (f"Set number of {selected['fname']} available in " 566 | f"the shop to {input}.") 567 | await ctx.channel.send(content=reply) 568 | 569 | 570 | # Command to set item price in the shop 571 | @shop.command( 572 | name='Price', 573 | aliases=['price', 'Cost', 'cost', 'p', 'c']) 574 | @commands.guild_only() 575 | @checks.check_perms() 576 | async def price(self, ctx, data: str, input: int): 577 | strings = await self.get_strings('Shop') 578 | for item in strings['fields']: 579 | if item['data'] == data.lower(): 580 | selected = item 581 | if not selected: 582 | await ctx.channel.send("Shop item not found!") 583 | return 584 | if input <= 0: 585 | await db.set_cfg(ctx.guild.id, 'crole_price', 0) 586 | reply = f"Set price of {selected['fname']} to free." 587 | else: 588 | await db.set_cfg(ctx.guild.id, 'crole_price', input) 589 | reply = f"Set price of {selected['fname']} to {input}" 590 | await ctx.channel.send(content=reply) 591 | 592 | 593 | # Command to display current guild leaderboard 594 | @commands.command( 595 | name='Leaderboard', 596 | description="Display the guild leaderboard", 597 | aliases=['leaderboard', 'lb']) 598 | @commands.guild_only() 599 | @checks.check_perms() 600 | async def leaderboard(self, ctx): 601 | head = f"Leaderboard for **{ctx.guild.name}**" 602 | members = await db.get_all_members(ctx.guild.id) 603 | members.sort(key=lambda member : member['xp'], reverse=True) 604 | fields = [] 605 | rank = 1 606 | for member in members: 607 | gmember = ctx.guild.get_member(member['member_id']) 608 | if not gmember: 609 | continue 610 | # fields.append({'fname': f"#{rank} - " 611 | # f"{member['member_id']} (Not Found)", 612 | # 'fdesc': f"**Level: {member['lvl']}** " 613 | # f"- {member['xp']} xp", 614 | # 'inline': False}) 615 | else: 616 | fields.append({'fname': f"#{rank} - {gmember.display_name}", 617 | 'fdesc': f"**Level: {member['lvl']}** " 618 | f"- {member['xp']} xp", 619 | 'inline': False}) 620 | if gmember == ctx.author: 621 | desc = f"Your rank: **#{rank} - {gmember.display_name}**" 622 | rank += 1 623 | LB = self.MenuEmbed(ctx.author, head, desc, fields) 624 | await LB.add_fields() 625 | message = await ctx.channel.send(embed=LB) 626 | controls = await LB.add_control(message) 627 | if controls: 628 | await self.wait_for_select(LB, 55.0) 629 | 630 | 631 | # Do stuff when a message is sent 632 | @commands.Cog.listener() 633 | async def on_message(self, message): 634 | if not message.author.bot and not message.guild: 635 | temp = await db.get_temp(message.author.id) 636 | if temp: 637 | strings = await self.get_strings(temp['menu']) 638 | for item in strings['fields']: 639 | if item['data'] == temp['selected']: 640 | selected = item 641 | break 642 | if temp['menu'] == 'Shop': 643 | await self.purchase(message, selected, temp) 644 | elif temp['menu'] == 'Manager': 645 | await self.manage_profile(message, selected, temp) 646 | 647 | 648 | # Remove a custom role when it's deleted 649 | @commands.Cog.listener() 650 | async def on_guild_role_delete(self, role): 651 | await db.del_custom_role(role.guild.id, role.id) 652 | 653 | 654 | # Adjust custom emoji available in shop when emojis are modified 655 | @commands.Cog.listener() 656 | async def on_guild_emojis_update(self, guild, before, after): 657 | curshoplimit = await db.get_cfg(guild.id, 'cemoji_available') 658 | if curshoplimit == 0: 659 | return 660 | elif curshoplimit < 0: 661 | if 'MORE_EMOJI' in guild.features: 662 | emojilimit = 200 663 | else: 664 | emojilimit = 50 665 | if (emojilimit - len(guild.emojis)) > 0: 666 | newshoplimit = emojilimit - len(guild.emojis) 667 | else: 668 | newshoplimit = 0 669 | else: 670 | if len(before) == len(after): 671 | return 672 | elif len(before) < len(after): 673 | newshoplimit = curshoplimit - 1 674 | else: 675 | newshoplimit = curshoplimit + 1 676 | await db.set_cfg(guild.id, 'cemoji_available', newshoplimit) 677 | 678 | 679 | # Looping to wait for a raw_reaction event that meets the expected criteria 680 | async def wait_for_select(self, Menu, time=None): 681 | def check(payload): 682 | return (payload.user_id == Menu.user.id and 683 | payload.message_id == Menu.message.id) 684 | 685 | while not Menu.selected: 686 | try: 687 | payload = await self.bot.wait_for( 688 | 'raw_reaction_add', 689 | timeout=time, 690 | check=check) 691 | await Menu.message.remove_reaction(payload.emoji, Menu.user) 692 | if payload.emoji.name == Menu.closebtn: 693 | raise asyncio.TimeoutError() 694 | except asyncio.TimeoutError: 695 | await Menu.message.clear_reactions() 696 | await Menu.message.edit( 697 | content="Menu closing in 5 seconds...", 698 | delete_after=5.0) 699 | break 700 | else: 701 | await Menu.process_reaction(payload.emoji.name) 702 | 703 | 704 | # Method for processing profile changes 705 | async def manage_profile(self, message, selected, temp): 706 | if len(message.content) > selected['limit']: 707 | await message.author.send(content="Over character limit!") 708 | return 709 | if message.content.lower() == "clear": 710 | await db.set_member(temp['guild_id'], 711 | temp['member_id'], 712 | selected['data'], 713 | None) 714 | await db.del_temp(message.author.id) 715 | alert = f"Your profile's `{selected['fname']}` has been cleared!" 716 | await message.author.send(content=alert) 717 | return 718 | if selected['format'] == 'date': 719 | try: 720 | entered = datetime.strptime(message.content, '%Y-%m-%d') 721 | option = entered.date() 722 | alert = f"Your profile's `{selected['fname']}` has been set!" 723 | except ValueError: 724 | await message.author.send("Date in wrong format!") 725 | return 726 | elif selected['format'] == 'list': 727 | option = message.content.replace(", ", "\n") 728 | alert = f"Profile `{selected['fname']}` will show as `{option}`!" 729 | else: 730 | option = message.content 731 | alert = f"Profile `{selected['fname']}` will show as `{option}`!" 732 | await db.set_member(temp['guild_id'], 733 | temp['member_id'], 734 | selected['data'], 735 | str(option)) 736 | await db.del_temp(message.author.id) 737 | await message.author.send(content=alert) 738 | 739 | 740 | # Method to process multi-stage shop purchases 741 | async def purchase(self, message, selected, temp): 742 | if selected['data'] == 'role': 743 | if not temp['storage']: 744 | if len(message.content) > selected['limit']: 745 | await message.author.send(content="Over character limit!") 746 | return 747 | file = discord.File(IMAGES / "color_examples.png") 748 | await message.author.send(content=selected['prompt2'], 749 | file=file) 750 | await db.update_temp(temp, message.content) 751 | else: 752 | color = message.content.lower() 753 | converted = await self.convert_color(color) 754 | if not converted: 755 | fail = "That's not a valid color! Please try again." 756 | await message.author.send(content=fail) 757 | return 758 | guild = self.bot.get_guild(temp['guild_id']) 759 | role = await guild.create_role(reason="Shop purchase", 760 | name=temp['storage'], 761 | color=converted, 762 | mentionable=True) 763 | botRoleID = await db.get_cfg(guild.id, 'bot_role') 764 | botRole = guild.get_role(botRoleID) 765 | position = botRole.position - 1 766 | try: 767 | await role.edit(position=position, reason="Shop purchase") 768 | except: 769 | print("bing") 770 | member = guild.get_member(temp['member_id']) 771 | await db.add_custom_role(guild.id, member.id, role.id) 772 | await member.add_roles(role, reason="Shop purchase") 773 | await db.del_temp(member.id) 774 | await member.send("Role set! Enjoy your fancy new title!") 775 | elif selected['data'] == 'emoji': 776 | if not temp['storage']: 777 | upload = message.attachments[0] 778 | filetype = Path(upload.filename).suffix 779 | if filetype not in selected['types']: 780 | await message.author.send("Wrong filetype!") 781 | return 782 | elif upload.size > selected['limit']: 783 | await message.author.send("File too big!") 784 | return 785 | emoji = await upload.read() 786 | await message.author.send(content=selected['prompt2']) 787 | await db.update_temp(temp, emoji) 788 | else: 789 | name = message.content.replace(":", "").replace(" ", "") 790 | guild = self.bot.get_guild(temp['guild_id']) 791 | await guild.create_custom_emoji(name=name, 792 | image=temp['storage'], 793 | reason="Shop purchase") 794 | await db.del_temp(message.author.id) 795 | await message.author.send("Emoji set! Try it out in the server!") 796 | elif selected['data'] == 'ticket': 797 | pass 798 | else: 799 | pass 800 | 801 | 802 | # Method to convert a user-entered color to a discord.Color object 803 | async def convert_color(self, color): 804 | colors = {'green': 0x2ecc71, 805 | 'dark green': 0x1f8b4c, 806 | 'teal': 0x1abc9c, 807 | 'dark teal': 0x11806a, 808 | 'blue': 0x3498db, 809 | 'dark blue': 0x206694, 810 | 'blurple': 0x7289da, 811 | 'purple': 0x9b59b6, 812 | 'dark purple': 0x71368a, 813 | 'magenta': 0xe91e63, 814 | 'dark magenta': 0xad1457, 815 | 'red': 0xe74c3c, 816 | 'dark red': 0x992d22, 817 | 'orange': 0xe67e22, 818 | 'dark orange': 0xa84300, 819 | 'gold': 0xf1c40f, 820 | 'dark gold': 0xc27c0e, 821 | 'white': 0xdcddde, 822 | 'lighter grey': 0x95a5a6, 823 | 'light grey': 0x979c9f, 824 | 'greyple': 0x99aab5, 825 | 'dark grey': 0x607d8b, 826 | 'darker grey': 0x546e7a, 827 | 'black': 0x18191c} 828 | if color in colors.keys(): 829 | convert = discord.Color(colors[color]) 830 | else: 831 | try: 832 | color = int(f"0x{color}", 0) 833 | convert = discord.Color(color) 834 | except ValueError: 835 | try: 836 | color = int(f"{color}", 0) 837 | convert = discord.Color(color) 838 | except TypeError: 839 | return 840 | return convert 841 | 842 | 843 | # Method for grabbing fields for embed strings. 844 | async def get_strings(self, name): 845 | with open(STRINGPATH, 'r') as stream: 846 | loaded = load(stream, Loader=Loader) 847 | for item in loaded['embeds']: 848 | if item['name'] == name: 849 | return item 850 | 851 | 852 | # Method for modifying fields for embed strings. Not currently used. 853 | async def set_strings(self, name, data, key, value): 854 | with open(STRINGPATH, 'r') as stream: 855 | loaded = load(stream, Loader=Loader) 856 | for item in loaded['embeds']: 857 | if item['name'] == name: 858 | for field in item['fields']: 859 | if field['data'] == data: 860 | field[key] = value 861 | with open(STRINGPATH, 'w') as stream: 862 | dump(loaded, stream, Dumper=Dumper, sort_keys=False, indent=4) 863 | -------------------------------------------------------------------------------- /cogs/img/color_examples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atbrandt/DeadbearBot/46801ee27cafe73fa876332f87f8b07b652af8bd/cogs/img/color_examples.png -------------------------------------------------------------------------------- /cogs/resource/embeds.yaml: -------------------------------------------------------------------------------- 1 | embeds: 2 | - name: "Shop" 3 | head: "**{} Shop**" 4 | desc: "*Buy somethin, will ya!* 5 | 6 | 7 | **{}** balance: {} {}" 8 | fields: 9 | - fname: "Custom Role" 10 | fdesc: "Buy a custom role! You'll be able to pick both the name and the 11 | color of the role after purchase." 12 | inline: true 13 | prompt: "Custom role purchased! Please enter a name for your new role." 14 | limit: 40 15 | format: 'text' 16 | prompt2: "Next, select a color for your role. You may enter a name from 17 | the example image below, or a hex code for a specific custom color, 18 | e.g. `0F0F0F`" 19 | data: 'role' 20 | - fname: "Custom Emoji" 21 | fdesc: "Buy a custom emoji! You'll be able to pick both the image and 22 | name for the emoji after purchase." 23 | inline: true 24 | prompt: "Custom emoji purchased! Please upload the image you'd like to 25 | use for your new emoji." 26 | limit: 256000 27 | format: 'image' 28 | types: 29 | - '.jpeg' 30 | - '.jpg' 31 | - '.gif' 32 | - '.png' 33 | prompt2: "Next, enter the name you want to use for the emoji, 34 | with or without `::` around it." 35 | data: 'emoji' 36 | - name: "Profile" 37 | head: "Level: **{}**" 38 | desc: "Member of *{}*" 39 | fields: 40 | - fname: "Total XP" 41 | fdesc: "\U0001F4D6 " 42 | inline: false 43 | data: 'xp' 44 | - fname: "Total Credits" 45 | fdesc: "\U0001F48E " 46 | inline: false 47 | data: 'cash' 48 | - fname: "Name" 49 | fdesc: "" 50 | inline: false 51 | data: 'name' 52 | - fname: "Nickname" 53 | fdesc: "" 54 | inline: false 55 | data: 'nickname' 56 | - fname: "Age" 57 | fdesc: "" 58 | inline: false 59 | data: 'birthday' 60 | - fname: "Gender" 61 | fdesc: "" 62 | inline: false 63 | data: 'gender' 64 | - fname: "Location" 65 | fdesc: "" 66 | inline: false 67 | data: 'location' 68 | - fname: "Description" 69 | fdesc: "" 70 | inline: false 71 | data: 'description' 72 | - fname: "Likes" 73 | fdesc: "" 74 | inline: false 75 | data: 'likes' 76 | - fname: "Dislikes" 77 | fdesc: "" 78 | inline: false 79 | data: 'dislikes' 80 | - name: "Manager" 81 | head: "**Profile Customizer**" 82 | desc: "Click the emoji that matches to the option you want to change, then 83 | follow the instructions that appear above. Use the arrows to navigate 84 | to another page, if available." 85 | fields: 86 | - fname: "Name" 87 | fdesc: "Your **name**, real or otherwise." 88 | inline: true 89 | prompt: "Submit a name, or send `clear` to reset." 90 | limit: 40 91 | format: 'text' 92 | data: 'name' 93 | - fname: "Nickname" 94 | fdesc: "The **nickname** you like to be called." 95 | inline: true 96 | prompt: "Submit a nickname, or send `clear` to reset." 97 | limit: 60 98 | format: 'text' 99 | data: 'nickname' 100 | - fname: "Age" 101 | fdesc: "Your **age**, if you want it known." 102 | inline: true 103 | prompt: "Submit a date, using the format `YYYY-MM-DD`, or send `clear` 104 | to reset. Note that this date will *not* show up on your profile, 105 | it's only used to calculate your age!" 106 | limit: 10 107 | format: 'date' 108 | data: 'birthday' 109 | - fname: "Gender" 110 | fdesc: "Your **gender**, whatever it may be." 111 | inline: true 112 | prompt: "Submit a gender, or send `clear` to reset." 113 | limit: 60 114 | format: 'text' 115 | data: 'gender' 116 | - fname: "Location" 117 | fdesc: "Your **location**, if you want it known." 118 | inline: true 119 | prompt: "Submit a location, or send `clear` to reset." 120 | limit: 80 121 | format: 'text' 122 | data: 'location' 123 | - fname: "Description" 124 | fdesc: "Your **description**, for general info." 125 | inline: true 126 | prompt: "Submit a description, or send `clear` to reset." 127 | limit: 1024 128 | format: 'text' 129 | data: 'description' 130 | - fname: "Likes" 131 | fdesc: "A list of **likes**, loves, and interests." 132 | inline: true 133 | prompt: "Submit a list of things you like, separated by commas, e.g. 134 | `one thing, something, this_thing`, or send `clear` to reset." 135 | limit: 1024 136 | format: 'list' 137 | data: 'likes' 138 | - fname: "Dislikes" 139 | fdesc: "A list of **dislikes**, despises, and disinterests." 140 | inline: true 141 | prompt: "Submit a list of things you don't like, separated by commas, 142 | e.g. `one thing, something, this_thing`, or send `clear` to reset." 143 | limit: 1024 144 | format: 'list' 145 | data: 'dislikes' 146 | -------------------------------------------------------------------------------- /cogs/roles.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Optional 2 | import discord 3 | from discord.ext import commands 4 | from .utils import db 5 | from .utils import checks 6 | 7 | 8 | class Roles(commands.Cog): 9 | def __init__(self, bot): 10 | self.bot = bot 11 | 12 | 13 | # Assign specific roles to specific users 14 | @commands.command( 15 | name='ToggleRole', 16 | description="Toggles a role for a member. Pass a role's `name` " 17 | "or `id` and the member's `name` or `id` to add or " 18 | "remove the role.", 19 | brief="Assign or remove member role by name or ID", 20 | aliases=['togglerole', 'trole', 'tr']) 21 | @commands.guild_only() 22 | @commands.is_owner() 23 | async def toggle_role(self, ctx, role: discord.Role, member: discord.Member): 24 | author = ctx.author.name 25 | if role not in member.roles: 26 | await member.add_roles(role, reason=f"ToggleRole by {author}") 27 | await ctx.channel.send(f"Gave {member.name} the \"{role.name}" 28 | f"\" role.") 29 | else: 30 | await member.remove_roles(role, reason=f"ToggleRole by {author}") 31 | await ctx.channel.send(f"Removed the \"{role.name}\" role from " 32 | f"{member.name}.") 33 | 34 | 35 | # Manage a role to be assigned upon joining guild 36 | @commands.command( 37 | name='AutoRole', 38 | description="Sets a role that users get automatically when " 39 | "joining the guild. Pass a role's `name` or `id` to " 40 | "enable or disable the auto-role.", 41 | brief="Modify auto-role settings.", 42 | aliases=['autorole', 'arole', 'ar']) 43 | @commands.guild_only() 44 | @commands.is_owner() 45 | async def auto_role(self, ctx, role: discord.Role): 46 | autorole = await db.get_cfg(ctx.guild.id, 'auto_role') 47 | if autorole is None: 48 | await db.set_cfg(ctx.guild.id, 'auto_role', role.id) 49 | await ctx.channel.send(f"Added \"{role.name}\" to auto-role.") 50 | else: 51 | await db.set_cfg(ctx.guild.id, 'auto_role', None) 52 | await ctx.channel.send(f"Removed \"{role.name}\" from auto-role.") 53 | # ctx.channel.send("No role found! Check the name or ID entered.") 54 | 55 | 56 | # Command to set a reaction role 57 | @commands.group( 58 | name='ReactionRole', 59 | description="Create a reaction role using a channel-messsage id, " 60 | "emoji, and role id. To get a channel-message ID, " 61 | "open the 3-dot menu for a message and shift-click " 62 | "the \"Copy ID\" button.", 63 | brief="Create a reaction role", 64 | aliases=['reactionrole', 'rr'], 65 | invoke_without_command=True) 66 | @commands.guild_only() 67 | @commands.is_owner() 68 | async def reaction_role(self, ctx, message: discord.Message, emoji: Union[discord.Emoji, str], role: discord.Role): 69 | gID = ctx.guild.id 70 | hookID = str(message.channel.id) + "-" + str(message.id) 71 | if type(emoji) is str: 72 | exists, rrID = await db.add_react_role(gID, hookID, emoji, role.id) 73 | else: 74 | exists, rrID = await db.add_react_role(gID, hookID, emoji.id, role.id) 75 | if not exists: 76 | await message.add_reaction(emoji) 77 | await ctx.channel.send(f"Set \"{emoji}\" to give the \"{role.name}\" " 78 | f"role.\nID = {rrID}") 79 | else: 80 | await ctx.channel.send(f"That already exists! ID = {rrID}") 81 | # Note to self: Add warning for emoji that the bot doesn't have access to. 82 | 83 | 84 | # Command to delete a reaction role 85 | @reaction_role.command( 86 | name='Delete', 87 | description="Removes a reaction role by its ID.", 88 | brief="Remove a reaction role", 89 | aliases=['delete', 'del', 'd']) 90 | @commands.guild_only() 91 | @commands.is_owner() 92 | async def rr_delete(self, ctx, rrID): 93 | exists = await db.del_react_role(rrID) 94 | if exists: 95 | await ctx.channel.send(f"Removed reaction role entry {rrID}.") 96 | else: 97 | await ctx.channel.send("No reaction role found for that ID!") 98 | 99 | 100 | # Command to list all reaction roles 101 | @reaction_role.command( 102 | name='List', 103 | description="Lists all reaction roles for this guild.", 104 | brief="List reaction roles", 105 | aliases=['list', 'l']) 106 | @commands.guild_only() 107 | @commands.is_owner() 108 | async def rr_list(self, ctx): 109 | rrlist = await db.get_react_roles(ctx.guild.id) 110 | formatted = "" 111 | line = "" 112 | for rr in rrlist: 113 | for key, value in rr.items(): 114 | line += f"{key}: {value}\n" 115 | formatted += f"{line}\n\n" 116 | line = "" 117 | await ctx.channel.send(formatted) 118 | 119 | 120 | # Command to add a voice chat role 121 | @commands.group( 122 | name='VoiceRole', 123 | description="Sets a role to be added to anyone that joins a " 124 | "specified voice channel. Pass a voice channel " 125 | "`name` or `id` and a role `name` or `id`.", 126 | brief="Add a voice role", 127 | aliases=['voicerole', 'vr'], 128 | invoke_without_command=True) 129 | @commands.guild_only() 130 | @commands.is_owner() 131 | async def voice_role(self, ctx, vchannel: discord.VoiceChannel, role: discord.Role): 132 | gID = ctx.guild.id 133 | exists, vrID = await db.add_voice_role(gID, vchannel.id, role.id) 134 | if not exists: 135 | await ctx.channel.send(f"Users joining \"{vchannel.name}\" will " 136 | f"automatically get the \"{role.name}\" " 137 | f"role.\nID = {vrID}") 138 | else: 139 | await ctx.channel.send(f"That already exists! ID = {vrID}") 140 | 141 | 142 | # Command to delete a voice chat role 143 | @voice_role.command( 144 | name='Delete', 145 | description="Removes a voice role by its ID.", 146 | brief="Remove a vc role", 147 | aliases=['delete', 'del', 'd']) 148 | @commands.guild_only() 149 | @commands.is_owner() 150 | async def vr_delete(self, ctx, vrID): 151 | exists = await db.del_voice_role(vrID) 152 | if exists: 153 | await ctx.channel.send(f"Removed reaction role entry {vrID}.") 154 | else: 155 | await ctx.channel.send("No reaction role found for that ID!") 156 | 157 | 158 | # Command to list all voice chat roles 159 | @voice_role.command( 160 | name='List', 161 | description="Lists all voice chat roles for this guild.", 162 | brief="List voice chat roles", 163 | aliases=['list', 'l']) 164 | @commands.guild_only() 165 | @commands.is_owner() 166 | async def vr_list(self, ctx): 167 | roles = dict(await db.get_voice_roles(ctx.guild.id)) 168 | await ctx.channel.send(roles) 169 | 170 | 171 | # Group command to add a role alert 172 | @commands.group( 173 | name='RoleAlert', 174 | description="Sets an alert for a role", 175 | brief="Sets an alert for a role", 176 | aliases=['rolealert', 'ralert', 'ra']) 177 | @commands.guild_only() 178 | @commands.is_owner() 179 | async def role_alert(self, ctx): 180 | pass 181 | 182 | 183 | # Command to add a role alert when a role is gained 184 | @role_alert.command( 185 | name='Gain', 186 | aliases=['gain', 'g']) 187 | @commands.guild_only() 188 | @commands.is_owner() 189 | async def alert_gain(self, ctx, role: discord.Role, channel: discord.TextChannel, *, message: str): 190 | arID = await db.add_role_alert(ctx.guild.id, 191 | role.id, 192 | 'gain_role', 193 | channel.id, 194 | message) 195 | await ctx.channel.send(f"When a user gains \"{role.name}\", an alert will " 196 | f"be sent to \"{channel.name}\" with the message: " 197 | f"{message}.\nID = {arID}") 198 | 199 | 200 | # Command to add a role alert when a role is lost 201 | @role_alert.command( 202 | name='Lose', 203 | aliases=['lose', 'l']) 204 | @commands.guild_only() 205 | @commands.is_owner() 206 | async def alert_lose(self, ctx, role: discord.Role, channel: discord.TextChannel, *, message: str): 207 | arID = await db.add_role_alert(ctx.guild.id, 208 | role.id, 209 | 'lose_role', 210 | channel.id, 211 | message) 212 | await ctx.channel.send(f"When a user loses \"{role.name}\", an alert will " 213 | f"be sent to \"{channel.name}\" with the message: " 214 | f"{message}.\nID = {arID}") 215 | 216 | 217 | # Command to delete a role alert 218 | @role_alert.command( 219 | name='Delete', 220 | description="Removes a role alert by its ID.", 221 | brief="Remove a role alert", 222 | aliases=['delete', 'del', 'd']) 223 | @commands.guild_only() 224 | @commands.is_owner() 225 | async def delete_alert(self, ctx, uuID): 226 | exists = await db.del_role_alert(uuID) 227 | if exists: 228 | await ctx.channel.send(f"Removed reaction role entry {uuID}.") 229 | else: 230 | await ctx.channel.send("No reaction role found for that ID!") 231 | 232 | 233 | # Do stuff to members upon joining guild 234 | @commands.Cog.listener() 235 | async def on_member_join(self, member): 236 | dbcfg = await db.get_cfg(member.guild.id) 237 | if dbcfg['auto_role']: 238 | role = member.guild.get_role(dbcfg['auto_role']) 239 | await member.add_roles(role, reason="AutoRole") 240 | if dbcfg['join_channel']: 241 | channel = self.bot.get_channel(dbcfg['join_channel']) 242 | await channel.send(dbcfg['join_message'].format(member=member)) 243 | 244 | 245 | # Do stuff to members upon leaving guild 246 | @commands.Cog.listener() 247 | async def on_member_remove(self, member): 248 | dbcfg = await db.get_cfg(member.guild.id) 249 | if dbcfg['leave_channel']: 250 | channel = self.bot.get_channel(dbcfg['leave_channel']) 251 | await channel.send(dbcfg['leave_message'].format(member=member)) 252 | 253 | 254 | # Do stuff when members are updated 255 | @commands.Cog.listener() 256 | async def on_member_update(self, before, after): 257 | if before.roles == after.roles: 258 | return 259 | if len(before.roles) < len(after.roles): 260 | s = set(before.roles) 261 | roles = [i for i in after.roles if i not in s] 262 | alerts = [] 263 | for role in roles: 264 | alert = await db.get_role_alert(role.id, 'gain_role') 265 | if alert: 266 | alerts.append(alert) 267 | else: 268 | s = set(after.roles) 269 | roles = [i for i in before.roles if i not in s] 270 | alerts = [] 271 | for role in roles: 272 | alert = await db.get_role_alert(role.id, 'lose_role') 273 | if alert: 274 | alerts.append(alert) 275 | for alert in alerts: 276 | channel = after.guild.get_channel(alert['channel_id']) 277 | await channel.send(alert['message'].format(member=after)) 278 | 279 | 280 | # Voice Role hook function 281 | @commands.Cog.listener() 282 | async def on_voice_state_update(self, member, before, after): 283 | roles = await db.get_voice_roles(member.guild.id) 284 | if not roles: 285 | return 286 | if before.channel: 287 | for i in roles: 288 | if i['hook_id'] == before.channel.id: 289 | role = member.guild.get_role(i['role_id']) 290 | await member.remove_roles(role, reason="VoiceRoleDisconnect") 291 | if after.channel: 292 | for i in roles: 293 | if i['hook_id'] == after.channel.id: 294 | role = member.guild.get_role(i['role_id']) 295 | await member.add_roles(role, reason="VoiceRoleConnect") 296 | 297 | 298 | # Event hook for reactions being added to messages 299 | @commands.Cog.listener() 300 | async def on_raw_reaction_add(self, payload): 301 | if payload.user_id == self.bot.user.id: 302 | return 303 | elif payload.guild_id: 304 | await self.rr_check(payload, 'add') 305 | 306 | 307 | # Event hook for reactions being removed from messages 308 | @commands.Cog.listener() 309 | async def on_raw_reaction_remove(self, payload): 310 | if payload.user_id == self.bot.user.id: 311 | return 312 | elif payload.guild_id: 313 | await self.rr_check(payload, 'rem') 314 | 315 | 316 | # Handler for guild reaction events 317 | async def rr_check(self, payload, event): 318 | reactionroles = await db.get_react_roles(payload.guild_id) 319 | if reactionroles: 320 | hookID = f"{payload.channel_id}-{payload.message_id}" 321 | if payload.emoji.is_custom_emoji(): 322 | emoji = payload.emoji.id 323 | else: 324 | emoji = payload.emoji.name 325 | for item in reactionroles: 326 | if hookID == item['hook_id'] and str(emoji) == item['emoji']: 327 | guild = self.bot.get_guild(payload.guild_id) 328 | member = guild.get_member(payload.user_id) 329 | role = guild.get_role(item['role_id']) 330 | if event == 'add': 331 | await member.add_roles(role, reason="ReactionRole") 332 | else: 333 | await member.remove_roles(role, reason="ReactionRole") 334 | 335 | -------------------------------------------------------------------------------- /cogs/testing.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atbrandt/DeadbearBot/46801ee27cafe73fa876332f87f8b07b652af8bd/cogs/testing.py -------------------------------------------------------------------------------- /cogs/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atbrandt/DeadbearBot/46801ee27cafe73fa876332f87f8b07b652af8bd/cogs/utils/__init__.py -------------------------------------------------------------------------------- /cogs/utils/checks.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | from . import db 4 | 5 | 6 | # Checking perms before executing command 7 | def check_perms(): 8 | async def predicate(ctx): 9 | return await check_perm_role(ctx) 10 | return commands.check(predicate) 11 | 12 | 13 | async def check_perm_role(ctx): 14 | if ctx.author.id == ctx.bot.owner_id: 15 | return True 16 | permrole = await db.get_cfg(ctx.guild.id, 'perm_role') 17 | if not permrole: 18 | return True 19 | gotRole = ctx.guild.get_role(permrole) 20 | roles = ctx.author.roles 21 | return gotRole in roles -------------------------------------------------------------------------------- /cogs/utils/db.py: -------------------------------------------------------------------------------- 1 | import aiosqlite 2 | from uuid import uuid4 3 | from pathlib import Path 4 | 5 | 6 | DBPATH = "" 7 | for path in Path(__file__).parents[2].rglob('bot.db'): 8 | DBPATH = path 9 | if not DBPATH: 10 | DBPATH = Path(__file__).parents[1] / 'bot.db' 11 | 12 | 13 | # Function for creating a connection to db 14 | async def db_connect(): 15 | conn = await aiosqlite.connect(str(DBPATH)) 16 | conn.row_factory = aiosqlite.Row 17 | return conn 18 | 19 | 20 | # Adds a guild to the guilds table 21 | async def add_guild(guildID): 22 | conn = await db_connect() 23 | c = await conn.cursor() 24 | sql = """ 25 | INSERT OR IGNORE INTO guilds ( 26 | id 27 | ) 28 | VALUES 29 | (?);""" 30 | await c.execute(sql, (guildID,)) 31 | await conn.commit() 32 | await conn.close() 33 | 34 | 35 | # Returns all guilds that the bot has logged 36 | async def get_all_guilds(): 37 | conn = await db_connect() 38 | c = await conn.cursor() 39 | sql = """ 40 | SELECT id 41 | FROM guilds;""" 42 | await c.execute(sql) 43 | fetched = await c.fetchall() 44 | await conn.close() 45 | guilds = [] 46 | for item in fetched: 47 | guilds.append(item['id']) 48 | return guilds 49 | 50 | 51 | # Returns a guild's config option from the guilds table 52 | async def get_cfg(guildID, option=None): 53 | conn = await db_connect() 54 | c = await conn.cursor() 55 | sql = """ 56 | SELECT * 57 | FROM guilds 58 | WHERE id = ?;""" 59 | await c.execute(sql, (guildID,)) 60 | fetched = await c.fetchone() 61 | await conn.close() 62 | if option: 63 | return fetched[option] 64 | else: 65 | return dict(fetched) 66 | 67 | 68 | # Sets a config option for a guild in the guilds table 69 | async def set_cfg(guildID, option, value): 70 | conn = await db_connect() 71 | c = await conn.cursor() 72 | sql = """ 73 | SELECT * 74 | FROM guilds;""" 75 | await c.execute(sql) 76 | row = await c.fetchone() 77 | names = row.keys() 78 | # To avoid the risk of injection attacks, this fstring is only performed 79 | # if the option passed in exactly matches an existing column name. 80 | for header in names: 81 | if option == header: 82 | sql = f""" 83 | UPDATE guilds 84 | SET {option} = ? 85 | WHERE id = ?;""" 86 | await c.execute(sql, (value, guildID,)) 87 | await conn.commit() 88 | await conn.close() 89 | return 90 | await conn.close() 91 | raise Exception(f"No column found for {option}.") 92 | 93 | 94 | # Resets a guild's config values to default 95 | async def clear_cfg(guildID): 96 | conn = await db_connect() 97 | c = await conn.cursor() 98 | sql = """ 99 | UPDATE guilds 100 | SET bot_alias = NULL, 101 | star_channel = NULL, 102 | auto_role = NULL, 103 | join_channel = NULL, 104 | join_message = NULL, 105 | leave_channel = NULL, 106 | leave_message = NULL, 107 | perm_role = NULL 108 | WHERE id = ?;""" 109 | await c.execute(sql, (guildID,)) 110 | await conn.commit() 111 | await conn.close() 112 | 113 | 114 | # Adds a member to the members table 115 | async def add_member(guildID, memberID, created, joined): 116 | conn = await db_connect() 117 | c = await conn.cursor() 118 | sql = """ 119 | INSERT OR IGNORE INTO members ( 120 | guild_id, 121 | member_id, 122 | created_at, 123 | joined_at, 124 | lvl, 125 | xp, 126 | cash 127 | ) 128 | VALUES 129 | (?, ?, ?, ?, ?, ?, ?);""" 130 | await c.execute(sql, (guildID, memberID, created, joined, 0, 0, 0,)) 131 | await conn.commit() 132 | await conn.close() 133 | 134 | 135 | # Removes a member from the members table 136 | async def del_member(guildID, memberID): 137 | conn = await db_connect() 138 | c = await conn.cursor() 139 | sql = """ 140 | DELETE FROM members 141 | WHERE guild_id = ? 142 | AND member_id = ?;""" 143 | await c.execute(sql, (guildID, memberID,)) 144 | await conn.commit() 145 | await conn.close() 146 | 147 | 148 | # Returns all members of a given guild 149 | async def get_all_members(guildID): 150 | conn = await db_connect() 151 | c = await conn.cursor() 152 | sql = """ 153 | SELECT * 154 | FROM members 155 | WHERE guild_id = ?;""" 156 | await c.execute(sql, (guildID,)) 157 | fetched = await c.fetchall() 158 | await conn.close() 159 | return fetched 160 | 161 | 162 | # Returns a specific member of a given guild 163 | async def get_member(guildID, memberID): 164 | conn = await db_connect() 165 | c = await conn.cursor() 166 | sql = """ 167 | SELECT * 168 | FROM members 169 | WHERE guild_id = ? 170 | AND member_id = ?;""" 171 | await c.execute(sql, (guildID, memberID,)) 172 | fetched = await c.fetchone() 173 | await conn.close() 174 | return fetched 175 | 176 | 177 | # Updates a specific member of a given guild 178 | async def set_member(guildID, memberID, option, value): 179 | conn = await db_connect() 180 | c = await conn.cursor() 181 | sql = """ 182 | SELECT * 183 | FROM members;""" 184 | await c.execute(sql) 185 | row = await c.fetchone() 186 | names = row.keys() 187 | # To avoid the risk of injection attacks, this fstring is only performed 188 | # if the option passed in exactly matches an existing column name. 189 | for header in names: 190 | if option == header: 191 | sql = f""" 192 | UPDATE members 193 | SET {option} = ? 194 | WHERE guild_id = ? 195 | AND member_id = ?;""" 196 | await c.execute(sql, (value, guildID, memberID,)) 197 | await conn.commit() 198 | await conn.close() 199 | return 200 | await conn.close() 201 | raise Exception(f"No column found for {option}.") 202 | 203 | 204 | # Returns members who have birthday on a given date 205 | async def get_members_by_bday(date): 206 | conn = await db_connect() 207 | c = await conn.cursor() 208 | date_format = f"%{date.month}-{date.day}" 209 | sql = """ 210 | SELECT * 211 | FROM members 212 | WHERE birthday LIKE ? 213 | GROUP BY member_id;""" 214 | await c.execute(sql, (date_format,)) 215 | fetched = await c.fetchall() 216 | await conn.close() 217 | return fetched 218 | 219 | 220 | # Adds currency to a members balance 221 | async def add_currency(guild_id, member_id, amount): 222 | conn = await db_connect() 223 | c = await conn.cursor() 224 | sql = """ 225 | UPDATE members 226 | SET cash = cash + ? 227 | WHERE guild_id = ? 228 | AND member_id = ?;""" 229 | await c.execute(sql, (amount, guild_id, member_id,)) 230 | await conn.commit() 231 | await conn.close() 232 | 233 | 234 | # Removes currency from a members balance 235 | async def remove_currency(guild_id, member_id, amount): 236 | conn = await db_connect() 237 | c = await conn.cursor() 238 | sql = """ 239 | UPDATE members 240 | SET cash = cash - ? 241 | WHERE guild_id = ? 242 | AND member_id = ?;""" 243 | await c.execute(sql, (amount, guild_id, member_id,)) 244 | await conn.commit() 245 | await conn.close() 246 | 247 | 248 | # Transfers currency from one member to another 249 | async def transfer_currency(guild_id, author_id, member_id, amount): 250 | conn = await db_connect() 251 | c = await conn.cursor() 252 | sql = """ 253 | UPDATE members 254 | SET cash = cash - ? 255 | WHERE guild_id = ? 256 | AND member_id = ?;""" 257 | await c.execute(sql, (amount, guild_id, author_id,)) 258 | sql = """ 259 | UPDATE members 260 | SET cash = cash + ? 261 | WHERE guild_id = ? 262 | AND member_id = ?;""" 263 | await c.execute(sql, (amount, guild_id, member_id,)) 264 | await conn.commit() 265 | await conn.close() 266 | 267 | 268 | # Returns all reaction roles of a given guild 269 | async def get_react_roles(guildID): 270 | conn = await db_connect() 271 | c = await conn.cursor() 272 | sql = """ 273 | SELECT uuid, hook_id, emoji, role_id 274 | FROM reaction_roles 275 | WHERE guild_id = ?;""" 276 | await c.execute(sql, (guildID,)) 277 | fetched = await c.fetchall() 278 | await conn.close() 279 | converted = [] 280 | for row in fetched: 281 | converted.append(dict(row)) 282 | return converted 283 | 284 | 285 | # Adds a reaction role to database, excepting on exact duplicates 286 | async def add_react_role(guildID, hookID, emoji, roleID): 287 | conn = await db_connect() 288 | c = await conn.cursor() 289 | sql = """ 290 | SELECT uuid, hook_id, emoji, role_id 291 | FROM reaction_roles 292 | WHERE hook_id = ? 293 | AND emoji = ? 294 | AND role_id = ?;""" 295 | await c.execute(sql, (hookID, emoji, roleID,)) 296 | exists = await c.fetchone() 297 | if not exists: 298 | UUID = str(uuid4()) 299 | sql = """ 300 | INSERT INTO reaction_roles ( 301 | uuid, 302 | guild_id, 303 | hook_id, 304 | emoji, 305 | role_id 306 | ) 307 | VALUES 308 | (?, ?, ?, ?, ?);""" 309 | await c.execute(sql, (UUID, guildID, hookID, emoji, roleID,)) 310 | await conn.commit() 311 | await conn.close() 312 | return (False, UUID) 313 | else: 314 | await conn.close() 315 | return (True, exists['uuid']) 316 | 317 | 318 | # Removes a reaction role from database by its unique ID, if it exists 319 | async def del_react_role(UUID): 320 | conn = await db_connect() 321 | c = await conn.cursor() 322 | sql = """ 323 | SELECT uuid 324 | FROM reaction_roles 325 | WHERE uuid = ?;""" 326 | await c.execute(sql, (UUID,)) 327 | exists = await c.fetchone() 328 | if exists: 329 | sql = """ 330 | DELETE FROM reaction_roles 331 | WHERE uuid = ?;""" 332 | await c.execute(sql, (UUID,)) 333 | await conn.commit() 334 | await conn.close() 335 | return True 336 | else: 337 | await conn.close() 338 | return False 339 | 340 | 341 | # Returns all voice roles for a given guild 342 | async def get_voice_roles(guildID): 343 | conn = await db_connect() 344 | c = await conn.cursor() 345 | sql = """ 346 | SELECT uuid, hook_id, role_id 347 | FROM voice_roles 348 | WHERE guild_id = ?;""" 349 | await c.execute(sql, (guildID,)) 350 | fetched = await c.fetchall() 351 | await conn.close() 352 | return fetched 353 | 354 | 355 | # Adds a voice role to database, excepting on exact duplicates 356 | async def add_voice_role(guildID, hookID, roleID): 357 | conn = await db_connect() 358 | c = await conn.cursor() 359 | sql = """ 360 | SELECT uuid, hook_id, role_id 361 | FROM voice_roles 362 | WHERE hook_id = ? 363 | AND role_id = ?;""" 364 | await c.execute(sql, (hookID, roleID,)) 365 | exists = await c.fetchone() 366 | if not exists: 367 | UUID = str(uuid4()) 368 | sql = """ 369 | INSERT INTO voice_roles ( 370 | uuid, 371 | guild_id, 372 | hook_id, 373 | role_id 374 | ) 375 | VALUES 376 | (?, ?, ?, ?);""" 377 | await c.execute(sql, (UUID, guildID, hookID, roleID,)) 378 | await conn.commit() 379 | await conn.close() 380 | return (False, UUID) 381 | else: 382 | await conn.close() 383 | return (True, exists['uuid']) 384 | 385 | 386 | # Removes a voice role from database by its unique ID, if it exists 387 | async def del_voice_role(UUID): 388 | conn = await db_connect() 389 | c = await conn.cursor() 390 | sql = """ 391 | SELECT uuid 392 | FROM voice_roles 393 | WHERE uuid = ?;""" 394 | await c.execute(sql, (UUID,)) 395 | check = await c.fetchone() 396 | if check: 397 | sql = """ 398 | DELETE FROM voice_roles 399 | WHERE uuid = ?;""" 400 | await c.execute(sql, (UUID,)) 401 | await conn.commit() 402 | await conn.close() 403 | return True 404 | else: 405 | await conn.close() 406 | return False 407 | 408 | 409 | # Creates a starred message for a given guild 410 | async def add_starred(guildID, originalID, starredID): 411 | conn = await db_connect() 412 | c = await conn.cursor() 413 | sql = """ 414 | INSERT INTO starboard ( 415 | guild_id, 416 | original_id, 417 | starred_id 418 | ) 419 | VALUES 420 | (?,?,?);""" 421 | await c.execute(sql, (guildID, originalID, starredID,)) 422 | await conn.commit() 423 | await conn.close() 424 | 425 | 426 | # Returns a starred message of a given guild 427 | async def get_starred(messageID): 428 | conn = await db_connect() 429 | c = await conn.cursor() 430 | sql = """ 431 | SELECT * 432 | FROM starboard 433 | WHERE original_id = ?;""" 434 | await c.execute(sql, (messageID,)) 435 | fetched = await c.fetchone() 436 | await conn.close() 437 | return fetched 438 | 439 | 440 | # Deletes a starred message of a given guild 441 | async def del_starred(messageID): 442 | conn = await db_connect() 443 | c = await conn.cursor() 444 | sql = """ 445 | DELETE FROM starboard 446 | WHERE original_id = ?;""" 447 | await c.execute(sql, (messageID,)) 448 | await conn.commit() 449 | await conn.close() 450 | 451 | 452 | # Creates a role alert for a given guild 453 | async def add_role_alert(guildID, roleID, event, channelID, message): 454 | conn = await db_connect() 455 | c = await conn.cursor() 456 | sql = """ 457 | INSERT INTO role_alerts ( 458 | uuid, 459 | guild_id, 460 | role_id, 461 | event, 462 | channel_id, 463 | message 464 | ) 465 | VALUES 466 | (?, ?, ?, ?, ?, ?);""" 467 | UUID = str(uuid4()) 468 | await c.execute(sql, (UUID, guildID, roleID, event, channelID, message,)) 469 | await conn.commit() 470 | await conn.close() 471 | return UUID 472 | 473 | 474 | # Returns a role alert for a given guild 475 | async def get_role_alert(roleID, event): 476 | conn = await db_connect() 477 | c = await conn.cursor() 478 | sql = """ 479 | SELECT * 480 | FROM role_alerts 481 | WHERE role_id = ? 482 | AND event = ?;""" 483 | await c.execute(sql, (roleID, event,)) 484 | fetched = await c.fetchone() 485 | await conn.close() 486 | return fetched 487 | 488 | 489 | # Deletes a role alert for a given guild 490 | async def del_role_alert(UUID): 491 | conn = await db_connect() 492 | c = await conn.cursor() 493 | sql = """ 494 | SELECT uuid 495 | FROM role_alerts 496 | WHERE uuid = ?;""" 497 | await c.execute(sql, (UUID,)) 498 | check = await c.fetchone() 499 | if check: 500 | sql = """ 501 | DELETE FROM role_alerts 502 | WHERE uuid = ?;""" 503 | await c.execute(sql, (UUID,)) 504 | await conn.commit() 505 | await conn.close() 506 | return True 507 | else: 508 | await conn.close() 509 | return False 510 | 511 | 512 | # Add custom role when purchased 513 | async def add_custom_role(guildID, memberID, roleID): 514 | conn = await db_connect() 515 | c = await conn.cursor() 516 | sql = """ 517 | INSERT INTO custom_roles ( 518 | guild_id, 519 | member_id, 520 | role_id 521 | ) 522 | VALUES 523 | (?, ?, ?);""" 524 | await c.execute(sql, (guildID, memberID, roleID,)) 525 | await conn.commit() 526 | await conn.close() 527 | 528 | 529 | # Get custom role 530 | async def get_custom_role(guildID, memberID): 531 | conn = await db_connect() 532 | c = await conn.cursor() 533 | sql = """ 534 | SELECT * 535 | FROM custom_roles 536 | WHERE guild_id = ? 537 | AND member_id = ?;""" 538 | await c.execute(sql, (guildID, memberID,)) 539 | fetched = await c.fetchone() 540 | await conn.close() 541 | return fetched 542 | 543 | 544 | # Delete custom role entry when the role itself is deleted 545 | async def del_custom_role(guildID, roleID): 546 | conn = await db_connect() 547 | c = await conn.cursor() 548 | sql = """ 549 | DELETE FROM custom_roles 550 | WHERE guild_id = ? 551 | AND role_id = ?;""" 552 | await c.execute(sql, (guildID, roleID,)) 553 | await conn.commit() 554 | await conn.close() 555 | 556 | 557 | # Set temp data in case the bot goes down mid-process 558 | async def add_temp(guildID, memberID, menu, selected): 559 | conn = await db_connect() 560 | c = await conn.cursor() 561 | sql = """ 562 | INSERT OR REPLACE INTO temp ( 563 | guild_id, 564 | member_id, 565 | menu, 566 | selected 567 | ) 568 | VALUES 569 | (?, ?, ?, ?);""" 570 | await c.execute(sql, (guildID, memberID, menu, selected,)) 571 | await conn.commit() 572 | await conn.close() 573 | 574 | 575 | # Updates temp data 576 | async def update_temp(temp, value): 577 | conn = await db_connect() 578 | c = await conn.cursor() 579 | sql = """ 580 | UPDATE temp 581 | SET storage = ? 582 | WHERE guild_id = ? 583 | AND member_id = ?;""" 584 | await c.execute(sql, (value, temp['guild_id'], temp['member_id'],)) 585 | await conn.commit() 586 | await conn.close() 587 | 588 | 589 | # Get temp data 590 | async def get_temp(memberID): 591 | conn = await db_connect() 592 | c = await conn.cursor() 593 | sql = """ 594 | SELECT * 595 | FROM temp 596 | WHERE member_id = ?;""" 597 | await c.execute(sql, (memberID,)) 598 | fetched = await c.fetchone() 599 | await conn.close() 600 | return fetched 601 | 602 | 603 | # Delete temp data when finished 604 | async def del_temp(memberID): 605 | conn = await db_connect() 606 | c = await conn.cursor() 607 | sql = """ 608 | DELETE FROM temp 609 | WHERE member_id = ?;""" 610 | await c.execute(sql, (memberID,)) 611 | await conn.commit() 612 | await conn.close() 613 | -------------------------------------------------------------------------------- /cogs/utils/generic.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | import discord 4 | from discord.ext import commands, tasks 5 | 6 | from .db import get_members_by_bday 7 | 8 | 9 | class Generic(commands.Cog): 10 | def __init__(self, bot): 11 | self.bot = bot 12 | self.birthday_alert.start() 13 | 14 | 15 | # Testing command 16 | @commands.command( 17 | name='test') 18 | async def hello_world(self, ctx): 19 | print(f"Message sent in {ctx.channel} from {ctx.author.id}") 20 | await ctx.channel.send(f"Hello {ctx.author}!") 21 | 22 | 23 | # Get list of roles (with IDs) on guild 24 | @commands.command( 25 | name='GetRoleIDs', 26 | description="Returns list of roles on server with IDs.", 27 | brief="Get all roles with IDs", 28 | aliases=['roles']) 29 | @commands.guild_only() 30 | @commands.is_owner() 31 | async def get_roles(self, ctx): 32 | output = "" 33 | for role in ctx.guild.roles: 34 | output += f"{role.id} {role.name}\n" 35 | await ctx.channel.send(f"```{output}```") 36 | 37 | 38 | # Get list of emojis (with IDs) on guild 39 | @commands.command( 40 | name='GetEmojiIDs', 41 | description="Returns list of emojis on server with IDs.", 42 | brief="Get all emojis with IDs", 43 | aliases=['emojis']) 44 | @commands.guild_only() 45 | @commands.is_owner() 46 | async def get_emojis(self, ctx): 47 | output = "" 48 | for emoji in ctx.guild.emojis: 49 | output += f"{emoji.id} {emoji.name}\n" 50 | await ctx.channel.send(f"```{output}```") 51 | 52 | 53 | # Get list of channels (with IDs) on guild 54 | @commands.command( 55 | name='GetChannelIDs', 56 | description="Returns list of channels on server with IDs.", 57 | brief="Get all channels with IDs", 58 | aliases=['channels']) 59 | @commands.guild_only() 60 | @commands.is_owner() 61 | async def get_channels(self, ctx): 62 | output = "" 63 | for channel in ctx.guild.channels: 64 | output += f"{channel.id} {channel.name}\n" 65 | await ctx.channel.send(f"```{output}```") 66 | 67 | 68 | # Do stuff when a message is deleted 69 | @commands.Cog.listener() 70 | async def on_message_delete(self, message): 71 | pass 72 | 73 | 74 | # Send a message to a user on their birthday 75 | @tasks.loop(hours=24) 76 | async def birthday_alert(self): 77 | members = await get_members_by_bday(date.today()) 78 | for member in members: 79 | member_id = member['member_id'] 80 | user = self.bot.get_user(member_id) 81 | if user: 82 | reply = f"Happy birthday, **{user.name}**!" 83 | await user.send(content=reply) 84 | 85 | 86 | # Wait until the bot is ready before the birthday_alert loop starts 87 | @birthday_alert.before_loop 88 | async def before_birthday_alert(self): 89 | await self.bot.wait_until_ready() 90 | 91 | # add blurbs, reminders -------------------------------------------------------------------------------- /cogs/utils/migration.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from pathlib import Path 3 | 4 | 5 | # Set platform-independent path to db file and migrations folder 6 | DBPATH = "" 7 | for path in Path(__file__).parents[2].rglob('bot.db'): 8 | DBPATH = path 9 | if not DBPATH: 10 | DBPATH = Path(__file__).parents[1] / 'bot.db' 11 | 12 | MIGPATH = Path(__file__).parent / "migration" 13 | 14 | 15 | def migrate(): 16 | conn = sqlite3.connect(str(DBPATH)) 17 | c = conn.cursor() 18 | c.execute("PRAGMA user_version") 19 | dbver = c.fetchone() 20 | 21 | # Get list of migrations available 22 | migrations = [] 23 | for child in sorted(MIGPATH.iterdir()): 24 | if child.suffix == ".sql": 25 | migrations.append(child) 26 | 27 | # Check if db matches latest version available, then update if not 28 | print("Checking DB version...") 29 | latest = sorted(migrations, reverse=True) 30 | latestver = int(latest[0].stem) 31 | if dbver[0] < latestver: 32 | print("Database is out of date! Migrating...\n") 33 | for item in migrations: 34 | print(f"Checking DB migration file {item.stem}") 35 | scriptver = int(item.stem) 36 | if scriptver > dbver[0]: 37 | print(f"Migrating DB to version {scriptver}") 38 | sqlfile = open(item, 'r').read() 39 | try: 40 | conn.executescript(sqlfile) 41 | except Exception as e: 42 | print(e) 43 | else: 44 | conn.execute(f"PRAGMA user_version={scriptver};") 45 | print(f"Done! DB at version {scriptver}\n") 46 | else: 47 | print(f"Migration {item.stem} already applied, skipping...\n") 48 | conn.commit() 49 | conn.close() 50 | -------------------------------------------------------------------------------- /cogs/utils/migration/001.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS guilds( 2 | id INTEGER PRIMARY KEY UNIQUE, 3 | bot_alias TEXT, 4 | auto_role INTEGER, 5 | greet_channel INTEGER, 6 | greet_message TEXT, 7 | bye_channel INTEGER, 8 | bye_message TEXT, 9 | perm_role INTEGER 10 | ); 11 | 12 | CREATE TABLE IF NOT EXISTS members( 13 | id INTEGER PRIMARY KEY, 14 | guild_id INTEGER, 15 | member_id INTEGER, 16 | created_at INTEGER, 17 | joined_at INTEGER UNIQUE, 18 | level INTEGER, 19 | xp INTEGER, 20 | cash INTEGER, 21 | name TEXT, 22 | nickname TEXT, 23 | birthday INTEGER, 24 | gender TEXT, 25 | location TEXT, 26 | description TEXT, 27 | likes TEXT, 28 | dislikes TEXT, 29 | FOREIGN KEY (guild_id) REFERENCES guilds (id) 30 | ); 31 | 32 | CREATE TABLE IF NOT EXISTS reaction_roles( 33 | uuid TEXT PRIMARY KEY UNIQUE, 34 | guild_id INTEGER, 35 | hook_id TEXT, 36 | emoji TEXT, 37 | role_id INTEGER, 38 | FOREIGN KEY (guild_id) REFERENCES guilds (id) 39 | ); 40 | 41 | CREATE TABLE IF NOT EXISTS voice_roles( 42 | uuid TEXT PRIMARY KEY UNIQUE, 43 | guild_id INTEGER, 44 | hook_id INTEGER, 45 | role_id INTEGER, 46 | FOREIGN KEY (guild_id) REFERENCES guilds (id) 47 | ); 48 | 49 | CREATE TABLE IF NOT EXISTS blurbs( 50 | uuid TEXT PRIMARY KEY UNIQUE, 51 | guild_id INTEGER, 52 | command TEXT UNIQUE, 53 | message TEXT, 54 | FOREIGN KEY (guild_id) REFERENCES guilds (id) 55 | ); 56 | 57 | CREATE TABLE IF NOT EXISTS temp( 58 | id INTEGER PRIMARY KEY, 59 | guild_id INTEGER, 60 | member_id INTEGER UNIQUE, 61 | menu TEXT, 62 | selected TEXT, 63 | FOREIGN KEY (guild_id) REFERENCES guilds (id) 64 | ); 65 | -------------------------------------------------------------------------------- /cogs/utils/migration/002.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE guildstemp( 2 | id INTEGER PRIMARY KEY UNIQUE, 3 | bot_alias TEXT, 4 | guild_stats TEXT DEFAULT('enabled'), 5 | auto_role INTEGER, 6 | star_channel INTEGER, 7 | star_threshold INTEGER NOT NULL DEFAULT(3), 8 | join_channel INTEGER, 9 | join_message TEXT, 10 | leave_channel INTEGER, 11 | leave_message TEXT, 12 | perm_role INTEGER 13 | ); 14 | 15 | INSERT INTO guildstemp(id,bot_alias,auto_role,join_channel,join_message,leave_channel,leave_message,perm_role) 16 | SELECT id,bot_alias,auto_role,greet_channel,greet_message,bye_channel,bye_message,perm_role 17 | FROM guilds; 18 | 19 | DROP TABLE guilds; 20 | 21 | ALTER TABLE guildstemp 22 | RENAME TO guilds; 23 | 24 | CREATE TABLE memberstemp( 25 | id INTEGER PRIMARY KEY, 26 | guild_id INTEGER, 27 | member_id INTEGER, 28 | created_at INTEGER, 29 | joined_at INTEGER UNIQUE, 30 | lvl INTEGER, 31 | xp INTEGER, 32 | cash INTEGER, 33 | name TEXT, 34 | nickname TEXT, 35 | birthday INTEGER, 36 | gender TEXT, 37 | location TEXT, 38 | description TEXT, 39 | likes TEXT, 40 | dislikes TEXT, 41 | FOREIGN KEY (guild_id) REFERENCES guilds (id) 42 | ); 43 | 44 | INSERT INTO memberstemp(id,guild_id,member_id,created_at,joined_at,lvl,xp,cash,name,nickname,birthday,gender,location,description,likes,dislikes) 45 | SELECT id,guild_id,member_id,created_at,joined_at,level,xp,cash,name,nickname,birthday,gender,location,description,likes,dislikes 46 | FROM members; 47 | 48 | DROP TABLE members; 49 | 50 | ALTER TABLE memberstemp 51 | RENAME TO members; 52 | 53 | CREATE TABLE IF NOT EXISTS starboard( 54 | guild_id INTEGER, 55 | original_id INTEGER PRIMARY KEY UNIQUE, 56 | starred_id INTEGER UNIQUE, 57 | FOREIGN KEY (guild_id) REFERENCES guilds (id) 58 | ); 59 | -------------------------------------------------------------------------------- /cogs/utils/migration/003.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS role_alerts( 2 | uuid TEXT PRIMARY KEY UNIQUE, 3 | guild_id INTEGER, 4 | role_id INTEGER, 5 | event TEXT, 6 | channel_id INTEGER, 7 | message TEXT, 8 | FOREIGN KEY (guild_id) REFERENCES guilds (id) 9 | ); 10 | 11 | -------------------------------------------------------------------------------- /cogs/utils/migration/004.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS custom_roles( 2 | id INTEGER PRIMARY KEY UNIQUE, 3 | guild_id INTEGER, 4 | member_id INTEGER, 5 | role_id INTEGER, 6 | FOREIGN KEY (guild_id) REFERENCES guilds (id) 7 | ); 8 | 9 | CREATE TABLE guildstemp( 10 | id INTEGER PRIMARY KEY UNIQUE, 11 | bot_role INTEGER, 12 | bot_alias TEXT, 13 | guild_stats TEXT NOT NULL DEFAULT('enabled'), 14 | min_cash INTEGER NOT NULL DEFAULT(1), 15 | max_cash INTEGER NOT NULL DEFAULT(25), 16 | auto_role INTEGER, 17 | star_channel INTEGER, 18 | star_threshold INTEGER NOT NULL DEFAULT(3), 19 | join_channel INTEGER, 20 | join_message TEXT, 21 | leave_channel INTEGER, 22 | leave_message TEXT, 23 | perm_role INTEGER 24 | ); 25 | 26 | INSERT INTO guildstemp(id,bot_alias,guild_stats,auto_role,star_channel,star_threshold,join_channel,join_message,leave_channel,leave_message,perm_role) 27 | SELECT id,bot_alias,guild_stats,auto_role,star_channel,star_threshold,join_channel,join_message,leave_channel,leave_message,perm_role 28 | FROM guilds; 29 | 30 | DROP TABLE guilds; 31 | 32 | ALTER TABLE guildstemp 33 | RENAME TO guilds; 34 | 35 | ALTER TABLE members 36 | ADD COLUMN daily_timestamp TEXT; 37 | 38 | ALTER TABLE temp 39 | ADD COLUMN storage TEXT; -------------------------------------------------------------------------------- /cogs/utils/migration/005.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE guilds 2 | ADD COLUMN star_nsfw INTEGER NOT NULL DEFAULT(0); 3 | 4 | ALTER TABLE guilds 5 | ADD COLUMN currency TEXT NOT NULL DEFAULT('💎'); -------------------------------------------------------------------------------- /cogs/utils/migration/006.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE guilds 2 | ADD COLUMN crole_available INTEGER NOT NULL DEFAULT(-1); 3 | 4 | ALTER TABLE guilds 5 | ADD COLUMN crole_price INTEGER NOT NULL DEFAULT(10000); 6 | 7 | ALTER TABLE guilds 8 | ADD COLUMN cemoji_available INTEGER NOT NULL DEFAULT(-1); 9 | 10 | ALTER TABLE guilds 11 | ADD COLUMN cemoji_price INTEGER NOT NULL DEFAULT(10000); 12 | 13 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from dotenv import load_dotenv 4 | import discord 5 | from discord.ext import commands 6 | from cogs.utils import db 7 | from cogs.utils import migration 8 | from cogs.utils import generic 9 | from cogs import config 10 | from cogs import embeds 11 | from cogs import roles 12 | # from cogs import testing 13 | 14 | 15 | # Path to environment variables 16 | ENVFILE = Path(__file__).parent / "secret.env" 17 | 18 | 19 | # Loading environment variables and checking for secret token presence 20 | if ENVFILE.exists(): 21 | load_dotenv(dotenv_path=ENVFILE) 22 | token = os.getenv('DEADBEAR_TOKEN') 23 | else: 24 | print("No bot token found!") 25 | token = input("Enter your bot's token: ") 26 | with ENVFILE.open('w', encoding='utf-8') as f: 27 | f.write(f"export DEADBEAR_TOKEN=\'{token}\'") 28 | 29 | 30 | # Create callable to obtain guild-specific alias for command prefix 31 | async def get_alias(bot, message): 32 | if message.guild: 33 | guild = message.guild.id 34 | prefix = await db.get_cfg(guild, 'bot_alias') 35 | if prefix: 36 | return prefix 37 | return "-" 38 | 39 | 40 | # Set up the bot, its cogs, and its command prefix alias 41 | intents = discord.Intents.all() 42 | bot = commands.Bot(command_prefix=get_alias, intents=intents) 43 | bot.add_cog(config.Config(bot)) 44 | bot.add_cog(generic.Generic(bot)) 45 | bot.add_cog(embeds.Embeds(bot)) 46 | bot.add_cog(roles.Roles(bot)) 47 | 48 | 49 | # Command to gracefully shut down the bot 50 | @bot.command( 51 | name='Shutdown', 52 | description="Shut down the bot and close all connections.", 53 | brief="Shut down the bot.", 54 | aliases=['shutdown', 'die']) 55 | @commands.is_owner() 56 | async def shutdown(ctx): 57 | await ctx.channel.send("Shutting down...") 58 | await bot.logout() 59 | 60 | 61 | # Do stuff to members upon joining guild 62 | @bot.event 63 | async def on_member_join(member): 64 | await filter_member(member) 65 | 66 | 67 | # Add guild when joining new guild 68 | @bot.event 69 | async def on_guild_join(guild): 70 | await add_guild(guild) 71 | 72 | 73 | # Make the bot ignore commands until fully initialized 74 | @bot.event 75 | async def on_connect(): 76 | print(f"{bot.user.name} connected, ID is {bot.user.id}. Getting ready...") 77 | await bot.wait_until_ready() 78 | 79 | 80 | # Output info to console once bot is initialized and ready 81 | @bot.event 82 | async def on_ready(): 83 | for guild in bot.guilds: 84 | print(f"Ready in {guild.name}") 85 | await add_guild(guild) 86 | print("------Bot Ready------") 87 | 88 | 89 | # Filter out bots from the database and add new members 90 | async def filter_member(member): 91 | dbmember = await db.get_member(member.guild.id, member.id) 92 | if member.bot: 93 | if dbmember: 94 | await db.del_member(member.guild.id, member.id) 95 | elif not dbmember: 96 | await db.add_member( 97 | member.guild.id, 98 | member.id, 99 | member.created_at, 100 | member.joined_at) 101 | 102 | 103 | # Add guild function 104 | async def add_guild(guild): 105 | print("Checking Guilds...") 106 | await db.add_guild(guild.id) 107 | botrole = await db.get_cfg(guild.id, 'bot_role') 108 | if not botrole: 109 | botmember = guild.get_member(bot.user.id) 110 | for role in botmember.roles: 111 | if role.managed and role.name == "DeadbearBot": 112 | await db.set_cfg(guild.id, 'bot_role', role.id) 113 | break 114 | for member in guild.members: 115 | await filter_member(member) 116 | 117 | 118 | # Run the program 119 | if __name__ == '__main__': 120 | migration.migrate() 121 | try: 122 | bot.run(token) 123 | except discord.PrivilegedIntentsRequired: 124 | print( 125 | "Privileged Intents are required to use this bot. " 126 | "Enable them through the Discord Developer Portal.") 127 | except discord.DiscordException as e: 128 | print(e) 129 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | discord.py==2.4.0 2 | aiosqlite==0.20.0 3 | python-dotenv==1.0.1 4 | PyYAML==6.0.2 5 | --------------------------------------------------------------------------------