├── .github ├── CODEOWNERS └── ISSUE_TEMPLATE │ ├── bug.md │ ├── config.yml │ └── feature_request.md ├── .gitignore ├── LICENSE ├── README.md ├── announcement └── announcement.py ├── anti-steal-close └── anti-steal-close.py ├── backupdb ├── README.md └── backupdb.py ├── birthday ├── birthday.py └── requirements.txt ├── code ├── code.py └── util │ └── CodeBlock.py ├── dashboard └── dashboard.py ├── dm-on-join └── dm-on-join.py ├── fix └── fix.py ├── github └── github.py ├── giveaway └── giveaway.py ├── hastebin ├── README.md └── hastebin.py ├── jishaku ├── jishaku.py └── requirements.txt ├── leave-server ├── README.md └── leave-server.py ├── moderation ├── moderation.py └── utils │ └── Log.py ├── music ├── music.py └── requirements.txt ├── plugins.json ├── poll └── poll.py ├── private └── private.py ├── quote └── quote.py ├── react-to-contact └── react-to-contact.py ├── reboot └── reboot.py.txt ├── reminder └── reminder.py ├── report-user └── report-user.py ├── role-assignment └── role-assignment.py ├── rolereaction └── rolereaction.py ├── staff-stats └── staff-stats.py ├── starboard └── starboard.py ├── tags ├── README.md └── tags.py ├── translator ├── README.md ├── requirements.txt └── translator.py └── warn └── warn.py /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @officialpiyush 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: If any plugin is not working the way it should work 4 | labels: bug 5 | --- 6 | 7 | Please provide details about: 8 | 9 | * What command you're trying to run 10 | * What happened 11 | * What you expected to happen 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Piyush's Discord 4 | url: https://discord.gg/hzD72GE 5 | about: Piyush's official discord server 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggestions for new plugins or existing plugins 4 | labels: Suggestion 5 | --- 6 | 7 | Please provide us with: 8 | 9 | * Details about your feature request 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Logo 3 | A repository to store Modmail Plugins 4 |
5 |
6 | 7 | 8 | Support 9 | 10 |
11 | 12 | # Author 13 | 14 | * [Piyush](https://github.com/officialpiyush) 15 | 16 | > Support is provided on Piyush's discord server or #plugin-support channel in Development server of Modmail. 17 | 18 | # Avaialable Plugins 19 | 20 | | **Plugin** | **Description** | **How To Install?** | **Link To Code** | **Status** | **Downloads** | 21 | |:------------: |:---------------------------: |:---------------------------------------------------------: |:------------------------------------------------------------------------------------: |:--------------------------------------------------------------------------: |:----------------------------------------------------------------------------------: | 22 | | Tags | Create, edit, delete, and use tags | `plugins add officialpiyush/modmail-plugins/Tags` | [Source](https://github.com/officialpiyush/modmail-plugins/tree/master/Tags) | [![Status](https://img.shields.io/badge/Status-Broken-red.svg)](#) | [![Downloads](https://counter.modmail-plugins.piyush.codes/badge/tags/)](#) | 23 | | Announcement | Easily make announcements | `plugins add officialpiyush/modmail-plugins/announcement` | [Source](https://github.com/officialpiyush/modmail-plugins/tree/master/announcement) | [![Status](https://img.shields.io/badge/Status-Stable-brightgreen.svg)](#) | [![Downloads]( https://counter.modmail-plugins.piyush.codes/badge/announcement/)](#) | 24 | | Dm On Join | DM's new users when they join | `plugins add officialpiyush/modmail-plugins/dm-on-join` | [Source](https://github.com/officialpiyush/modmail-plugins/tree/master/dm-on-join) | [![Status](https://img.shields.io/badge/Status-Stable-brightgreen.svg)](#) | [![Downloads]( https://counter.modmail-plugins.piyush.codes/badge/dmonjoin/)](#) | 25 | | Hastebin | Upload text to hastebin | `plugins add officialpiyush/modmail-plugins/hastebin` | [Source](https://github.com/officialpiyush/modmail-plugins/tree/master/hastebin) | [![Status](https://img.shields.io/badge/Status-Stable-brightgreen.svg)](#) | [![Downloads]( https://counter.modmail-plugins.piyush.codes/badge/hastebin/)](#) | 26 | | Leave Server | Make the bot leave a server | `plugins add officialpiyush/modmail-plugins/leave-server` | [Source](https://github.com/officialpiyush/modmail-plugins/tree/master/leave-server) | [![Status](https://img.shields.io/badge/Status-Stable-brightgreen.svg)](#) | [![Downloads]( https://counter.modmail-plugins.piyush.codes/badge/leaveserver/)](#) | 27 | | Translator | Translate messages | `plugins add officialpiyush/modmail-plugins/translator` | [Source](https://github.com/officialpiyush/modmail-plugins/tree/master/translator) | [![Status](https://img.shields.io/badge/Status-Stable-brightgreen.svg)](#) | [![Downloads]( https://counter.modmail-plugins.piyush.codes/badge/translator/)](#) | 28 | 29 | 30 | # Contributors 31 | 32 | [![Contributors](https://counter.modmail-plugins.piyush.codes/contributors)](https://github.com/officialpiyush/modmail-plugins/graphs/contributors) 33 | -------------------------------------------------------------------------------- /announcement/announcement.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import typing 3 | import re 4 | from discord.ext import commands 5 | 6 | from core import checks 7 | from core.models import PermissionLevel 8 | 9 | 10 | class AnnoucementPlugin(commands.Cog): 11 | """ 12 | Easily create plain text or embedded announcements 13 | """ 14 | 15 | def __init__(self, bot): 16 | self.bot = bot 17 | 18 | @commands.group(aliases=["a"], invoke_without_command=True) 19 | @commands.guild_only() 20 | @checks.has_permissions(PermissionLevel.REGULAR) 21 | async def announcement(self, ctx: commands.Context): 22 | """ 23 | Make Announcements Easily 24 | """ 25 | await ctx.send_help(ctx.command) 26 | 27 | @announcement.command() 28 | @checks.has_permissions(PermissionLevel.ADMIN) 29 | async def start( 30 | self, 31 | ctx: commands.Context, 32 | role: typing.Optional[typing.Union[discord.Role, str]] = None, 33 | ): 34 | """ 35 | Start an interactive session to create announcement 36 | Add the role in the command if you want to enable mentions 37 | 38 | **Example:** 39 | __Announcement with role mention:__ 40 | {prefix}announcement start everyone 41 | 42 | __Announcement without role mention__ 43 | {prefix}announcement start 44 | """ 45 | 46 | # TODO: Enable use of reactions 47 | def check(msg: discord.Message): 48 | return ctx.author == msg.author and ctx.channel == msg.channel 49 | 50 | # def check_reaction(reaction: discord.Reaction, user: discord.Member): 51 | # return ctx.author == user and (str(reaction.emoji == "✅") or str(reaction.emoji) == "❌") 52 | 53 | def title_check(msg: discord.Message): 54 | return ( 55 | ctx.author == msg.author 56 | and ctx.channel == msg.channel 57 | and (len(msg.content) < 256) 58 | ) 59 | 60 | def description_check(msg: discord.Message): 61 | return ( 62 | ctx.author == msg.author 63 | and ctx.channel == msg.channel 64 | and (len(msg.content) < 2048) 65 | ) 66 | 67 | def footer_check(msg: discord.Message): 68 | return ( 69 | ctx.author == msg.author 70 | and ctx.channel == msg.channel 71 | and (len(msg.content) < 2048) 72 | ) 73 | 74 | # def author_check(msg: discord.Message): 75 | # return ( 76 | # ctx.author == msg.author and ctx.channel == msg.channel and (len(msg.content) < 256) 77 | # ) 78 | 79 | def cancel_check(msg: discord.Message): 80 | if msg.content == "cancel" or msg.content == f"{ctx.prefix}cancel": 81 | return True 82 | else: 83 | return False 84 | 85 | if isinstance(role, discord.Role): 86 | role_mention = f"<@&{role.id}>" 87 | guild: discord.Guild = ctx.guild 88 | grole: discord.Role = guild.get_role(role.id) 89 | await grole.edit(mentionable=True) 90 | elif isinstance(role, str): 91 | if role == "here" or role == "@here": 92 | role_mention = "@here" 93 | elif role == "everyone" or role == "@everyone": 94 | role_mention = "@everyone" 95 | else: 96 | role_mention = "" 97 | 98 | await ctx.send("Starting an interactive process to create an announcement") 99 | 100 | await ctx.send( 101 | embed=await self.generate_embed("Do you want it to be an embed? `[y/n]`") 102 | ) 103 | 104 | embed_res: discord.Message = await self.bot.wait_for("message", check=check) 105 | if cancel_check(embed_res) is True: 106 | await ctx.send("Cancelled!") 107 | return 108 | elif cancel_check(embed_res) is False and embed_res.content.lower() == "n": 109 | await ctx.send( 110 | embed=await self.generate_embed( 111 | "Okay, let's do a no-embed announcement." 112 | "\nWhat's the announcement?" 113 | ) 114 | ) 115 | announcement = await self.bot.wait_for("message", check=check) 116 | if cancel_check(announcement) is True: 117 | await ctx.send("Cancelled!") 118 | return 119 | else: 120 | await ctx.send( 121 | embed=await self.generate_embed( 122 | "To which channel should I send the announcement?" 123 | ) 124 | ) 125 | channel: discord.Message = await self.bot.wait_for( 126 | "message", check=check 127 | ) 128 | if cancel_check(channel) is True: 129 | await ctx.send("Cancelled!") 130 | return 131 | else: 132 | if channel.channel_mentions[0] is None: 133 | await ctx.send("Cancelled as no channel was provided") 134 | return 135 | else: 136 | await channel.channel_mentions[0].send( 137 | f"{role_mention}\n{announcement.content}" 138 | ) 139 | elif cancel_check(embed_res) is False and embed_res.content.lower() == "y": 140 | embed = discord.Embed() 141 | await ctx.send( 142 | embed=await self.generate_embed( 143 | "Should the embed have a title? `[y/n]`" 144 | ) 145 | ) 146 | t_res = await self.bot.wait_for("message", check=check) 147 | if cancel_check(t_res) is True: 148 | await ctx.send("Cancelled") 149 | return 150 | elif cancel_check(t_res) is False and t_res.content.lower() == "y": 151 | await ctx.send( 152 | embed=await self.generate_embed( 153 | "What should the title of the embed be?" 154 | "\n**Must not exceed 256 characters**" 155 | ) 156 | ) 157 | tit = await self.bot.wait_for("message", check=title_check) 158 | embed.title = tit.content 159 | await ctx.send( 160 | embed=await self.generate_embed( 161 | "Should the embed have a description?`[y/n]`" 162 | ) 163 | ) 164 | d_res: discord.Message = await self.bot.wait_for("message", check=check) 165 | if cancel_check(d_res) is True: 166 | await ctx.send("Cancelled") 167 | return 168 | elif cancel_check(d_res) is False and d_res.content.lower() == "y": 169 | await ctx.send( 170 | embed=await self.generate_embed( 171 | "What do you want as the description for the embed?" 172 | "\n**Must not exceed 2048 characters**" 173 | ) 174 | ) 175 | des = await self.bot.wait_for("message", check=description_check) 176 | embed.description = des.content 177 | 178 | await ctx.send( 179 | embed=await self.generate_embed( 180 | "Should the embed have a thumbnail?`[y/n]`" 181 | ) 182 | ) 183 | th_res: discord.Message = await self.bot.wait_for("message", check=check) 184 | if cancel_check(th_res) is True: 185 | await ctx.send("Cancelled") 186 | return 187 | elif cancel_check(th_res) is False and th_res.content.lower() == "y": 188 | await ctx.send( 189 | embed=await self.generate_embed( 190 | "What's the thumbnail of the embed? Enter a " "valid URL" 191 | ) 192 | ) 193 | thu = await self.bot.wait_for("message", check=check) 194 | embed.set_thumbnail(url=thu.content) 195 | 196 | await ctx.send( 197 | embed=await self.generate_embed("Should the embed have a image?`[y/n]`") 198 | ) 199 | i_res: discord.Message = await self.bot.wait_for("message", check=check) 200 | if cancel_check(i_res) is True: 201 | await ctx.send("Cancelled") 202 | return 203 | elif cancel_check(i_res) is False and i_res.content.lower() == "y": 204 | await ctx.send( 205 | embed=await self.generate_embed( 206 | "What's the image of the embed? Enter a " "valid URL" 207 | ) 208 | ) 209 | i = await self.bot.wait_for("message", check=check) 210 | embed.set_image(url=i.content) 211 | 212 | await ctx.send( 213 | embed=await self.generate_embed("Will the embed have a footer?`[y/n]`") 214 | ) 215 | f_res: discord.Message = await self.bot.wait_for("message", check=check) 216 | if cancel_check(f_res) is True: 217 | await ctx.send("Cancelled") 218 | return 219 | elif cancel_check(f_res) is False and f_res.content.lower() == "y": 220 | await ctx.send( 221 | embed=await self.generate_embed( 222 | "What do you want the footer of the embed to be?" 223 | "\n**Must not exceed 2048 characters**" 224 | ) 225 | ) 226 | foo = await self.bot.wait_for("message", check=footer_check) 227 | embed.set_footer(text=foo.content) 228 | 229 | await ctx.send( 230 | embed=await self.generate_embed( 231 | "Do you want it to have a color?`[y/n]`" 232 | ) 233 | ) 234 | c_res: discord.Message = await self.bot.wait_for("message", check=check) 235 | if cancel_check(c_res) is True: 236 | await ctx.send("Cancelled!") 237 | return 238 | elif cancel_check(c_res) is False and c_res.content.lower() == "y": 239 | await ctx.send( 240 | embed=await self.generate_embed( 241 | "What color should the embed have? " 242 | "Please provide a valid hex color" 243 | ) 244 | ) 245 | colo = await self.bot.wait_for("message", check=check) 246 | if cancel_check(colo) is True: 247 | await ctx.send("Cancelled!") 248 | return 249 | else: 250 | match = re.search( 251 | r"^#(?:[0-9a-fA-F]{3}){1,2}$", colo.content 252 | ) # uwu thanks stackoverflow 253 | if match: 254 | embed.colour = int( 255 | colo.content.replace("#", "0x"), 0 256 | ) # Basic Computer Science 257 | else: 258 | await ctx.send( 259 | "Failed! Not a valid hex color, get yours from " 260 | "https://www.google.com/search?q=color+picker" 261 | ) 262 | return 263 | 264 | await ctx.send( 265 | embed=await self.generate_embed( 266 | "In which channel should I send the announcement?" 267 | ) 268 | ) 269 | channel: discord.Message = await self.bot.wait_for("message", check=check) 270 | if cancel_check(channel) is True: 271 | await ctx.send("Cancelled!") 272 | return 273 | else: 274 | if channel.channel_mentions[0] is None: 275 | await ctx.send("Cancelled as no channel was provided") 276 | return 277 | else: 278 | schan = channel.channel_mentions[0] 279 | await ctx.send( 280 | "Here is how the embed looks like: Send it? `[y/n]`", embed=embed 281 | ) 282 | s_res = await self.bot.wait_for("message", check=check) 283 | if cancel_check(s_res) is True or s_res.content.lower() == "n": 284 | await ctx.send("Cancelled") 285 | return 286 | else: 287 | await schan.send(f"{role_mention}", embed=embed) 288 | if isinstance(role, discord.Role): 289 | guild: discord.Guild = ctx.guild 290 | grole: discord.Role = guild.get_role(role.id) 291 | if grole.mentionable is True: 292 | await grole.edit(mentionable=False) 293 | 294 | @announcement.command(aliases=["native", "n", "q"]) 295 | @checks.has_permissions(PermissionLevel.ADMIN) 296 | async def quick( 297 | self, 298 | ctx: commands.Context, 299 | channel: discord.TextChannel, 300 | role: typing.Optional[typing.Union[discord.Role, str]], 301 | *, 302 | msg: str, 303 | ): 304 | """ 305 | An old way of making announcements 306 | 307 | **Usage:** 308 | {prefix}announcement quick #channel message 309 | """ 310 | if isinstance(role, discord.Role): 311 | guild: discord.Guild = ctx.guild 312 | grole: discord.Role = guild.get_role(role.id) 313 | await grole.edit(mentionable=True) 314 | role_mention = f"<@&{role.id}>" 315 | elif isinstance(role, str): 316 | if role == "here" or role == "@here": 317 | role_mention = "@here" 318 | elif role == "everyone" or role == "@everyone": 319 | role_mention = "@everyone" 320 | else: 321 | msg = f"{role} {msg}" 322 | role_mention = "" 323 | 324 | await channel.send(f"{role_mention}\n{msg}") 325 | await ctx.send("Done") 326 | 327 | if isinstance(role, discord.Role): 328 | guild: discord.Guild = ctx.guild 329 | grole: discord.Role = guild.get_role(role.id) 330 | if grole.mentionable is True: 331 | await grole.edit(mentionable=False) 332 | 333 | @commands.Cog.listener() 334 | async def on_ready(self): 335 | async with self.bot.session.post( 336 | "https://counter.modmail-plugins.piyush.codes/api/instances/announcement", 337 | json={"id": self.bot.user.id}, 338 | ): 339 | print("Posted to Plugin API") 340 | 341 | @staticmethod 342 | async def generate_embed(description: str): 343 | embed = discord.Embed() 344 | embed.colour = discord.Colour.blurple() 345 | embed.description = description 346 | 347 | return embed 348 | 349 | 350 | def setup(bot): 351 | bot.add_cog(AnnoucementPlugin(bot)) 352 | -------------------------------------------------------------------------------- /anti-steal-close/anti-steal-close.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from datetime import datetime 3 | from discord.ext import commands 4 | 5 | from core.time import UserFriendlyTime, human_timedelta 6 | from core.models import PermissionLevel 7 | from core import checks 8 | 9 | 10 | class AntiStealClosePlugin(commands.Cog): 11 | """ 12 | An initiative to stop people stealing thread closes kthx. 13 | """ 14 | 15 | def __init__(self, bot): 16 | self.bot = bot 17 | 18 | @commands.command(aliases=["asc", "notclosedbyme", "antisteal", "anti-steal"]) 19 | @checks.has_permissions(PermissionLevel.SUPPORTER) 20 | @checks.thread_only() 21 | async def anti_steal_close( 22 | self, ctx, user: discord.User, *, after: UserFriendlyTime = None 23 | ): 24 | """ 25 | Close the thread on the behalf of another user. 26 | 27 | **Usage:** 28 | [p]asc 29 | 30 | **Examples:** 31 | [p]asc 365644930556755969 Closed Due to Inactivity 32 | [p]asc @Piyush#4332 in 24 hours Cya 33 | """ 34 | thread = ctx.thread 35 | 36 | now = datetime.utcnow() 37 | 38 | close_after = (after.dt - now).total_seconds() if after else 0 39 | message = after.arg if after else None 40 | silent = str(message).lower() in {"silent", "silently"} 41 | cancel = str(message).lower() == "cancel" 42 | 43 | if cancel: 44 | 45 | if thread.close_task is not None or thread.auto_close_task is not None: 46 | await thread.cancel_closure(all=True) 47 | embed = discord.Embed( 48 | color=self.bot.error_color, 49 | description="Scheduled close has been cancelled.", 50 | ) 51 | else: 52 | embed = discord.Embed( 53 | color=self.bot.error_color, 54 | description="This thread has not already been scheduled to close.", 55 | ) 56 | 57 | return await ctx.send(embed=embed) 58 | 59 | if after and after.dt > now: 60 | await self.send_scheduled_close_message(ctx, after, silent) 61 | 62 | dupe_message = ctx.message 63 | dupe_message.content = f"[Anti Close Steal] The thread close command was invoked by {ctx.author.name}#{ctx.author.discriminator}" 64 | 65 | await thread.note(dupe_message) 66 | 67 | await thread.close( 68 | closer=user, after=close_after, message=message, silent=silent 69 | ) 70 | 71 | async def send_scheduled_close_message(self, ctx, after, silent=False): 72 | human_delta = human_timedelta(after.dt) 73 | 74 | silent = "*silently* " if silent else "" 75 | 76 | embed = discord.Embed( 77 | title="Scheduled close", 78 | description=f"This thread will close {silent}in {human_delta}.", 79 | color=self.bot.error_color, 80 | ) 81 | 82 | if after.arg and not silent: 83 | embed.add_field(name="Message", value=after.arg) 84 | 85 | embed.set_footer( 86 | text="Closing will be cancelled " "if a thread message is sent." 87 | ) 88 | embed.timestamp = after.dt 89 | 90 | await ctx.send(embed=embed) 91 | 92 | async def handle_log(self, guild: discord.Guild, ctx, user): 93 | channel = discord.utils.find(lambda c: "asc-logs" in c.topic, guild.channels) 94 | if channel is None: 95 | return 96 | else: 97 | embed = discord.Embed(color=self.bot.main_color) 98 | embed.description = f"Thread closed by {ctx.author.name}#{ctx.author.discriminator} on the behalf of {user.username}#{user.discriminator} " 99 | 100 | await channel.send(embed) 101 | 102 | 103 | def setup(bot): 104 | bot.add_cog(AntiStealClosePlugin(bot)) 105 | -------------------------------------------------------------------------------- /backupdb/README.md: -------------------------------------------------------------------------------- 1 |
2 | BD Image 3 |
4 | A plugin to backup modmail database. 5 |
6 |
7 | 8 | 9 | Support 10 | 11 |
12 | 13 | --- -------------------------------------------------------------------------------- /backupdb/backupdb.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import datetime 4 | import discord 5 | from discord.ext import commands 6 | from motor.motor_asyncio import AsyncIOMotorClient 7 | 8 | from core import checks 9 | from core.models import PermissionLevel 10 | 11 | 12 | class BackupDB(commands.Cog): 13 | """ 14 | Take Backup of your mongodb database with a single command! 15 | 16 | **Requires `BACKUP_MONGO_URI` in environment variables or config.json** (different from your original db) 17 | """ 18 | 19 | def __init__(self, bot): 20 | self.bot = bot 21 | self.db = bot.plugin_db.get_partition(self) 22 | self.running = False 23 | 24 | @commands.group() 25 | @checks.has_permissions(PermissionLevel.OWNER) 26 | async def backup(self, ctx: commands.Context): 27 | """ 28 | Backup Your Mongodb database using this command. 29 | 30 | **Deletes Existing data from the backup db** 31 | """ 32 | if ctx.invoked_subcommand is None: 33 | if self.running is True: 34 | await ctx.send( 35 | "A backup/restore process is already running, please wait until it finishes" 36 | ) 37 | return 38 | if os.path.exists("./config.json"): 39 | with open("./config.json") as f: 40 | 41 | jd = json.load(f) 42 | try: 43 | backup_url = jd["BACKUP_MONGO_URI"] 44 | except KeyError: 45 | backup_url = os.getenv("BACKUP_MONGO_URI") 46 | if backup_url is None: 47 | await ctx.send( 48 | ":x: | No `BACKUP_MONGO_URI` found in `config.json` or environment variables, please add one.\nNote: Backup db is different from original db!" 49 | ) 50 | return 51 | else: 52 | backup_url = os.getenv("BACKUP_MONGO_URI") 53 | if backup_url is None: 54 | await ctx.send( 55 | ":x: | No `BACKUP_MONGO_URI` found in `config.json` or environment variables, please add one.\nNote: Backup db is different from original db!" 56 | ) 57 | return 58 | self.running = True 59 | db_name = (backup_url.split("/"))[-1] 60 | backup_client = AsyncIOMotorClient(backup_url) 61 | if "mlab.com" in backup_url: 62 | bdb = backup_client[db_name] 63 | else: 64 | bdb = backup_client["backup_modmail_bot"] 65 | await ctx.send( 66 | embed=await self.generate_embed( 67 | "Connected to backup DB. Removing all documents" 68 | ) 69 | ) 70 | collections = await bdb.list_collection_names() 71 | 72 | if len(collections) > 0: 73 | for collection in collections: 74 | if collection == "system.indexes": 75 | continue 76 | 77 | await bdb[collection].drop() 78 | await ctx.send( 79 | embed=await self.generate_embed( 80 | "Deleted all documents from backup db" 81 | ) 82 | ) 83 | else: 84 | await ctx.send( 85 | embed=await self.generate_embed( 86 | "No Existing collections found! Nothing was deleted!" 87 | ) 88 | ) 89 | du = await self.bot.db.list_collection_names() 90 | for collection in du: 91 | if collection == "system.indexes": 92 | continue 93 | 94 | le = await self.bot.db[str(collection)].find().to_list(None) 95 | for item in le: 96 | await bdb[str(collection)].insert_one(item) 97 | del item 98 | del le 99 | await ctx.send( 100 | embed=await self.generate_embed(f"Backed up `{str(collection)}`") 101 | ) 102 | await self.db.find_one_and_update( 103 | {"_id": "config"}, 104 | {"$set": {"backedupAt": str(datetime.datetime.utcnow())}}, 105 | upsert=True, 106 | ) 107 | await ctx.send( 108 | embed=await self.generate_embed( 109 | f":tada: Backed Up Everything!\nTo restore your backup at any time, type `{self.bot.prefix}backup restore`." 110 | ) 111 | ) 112 | self.running = False 113 | return 114 | 115 | @backup.command() 116 | @checks.has_permissions(PermissionLevel.OWNER) 117 | async def restore(self, ctx: commands.Context): 118 | """ 119 | Restore Your Mongodb database using this command. 120 | 121 | **Deletes Existing data from the original db and overwrites it with data in backup db** 122 | """ 123 | 124 | def check(msg: discord.Message): 125 | return ctx.author == msg.author and ctx.channel == msg.channel 126 | 127 | if self.running is True: 128 | await ctx.send( 129 | "A backup/restore process is already running, please wait until it finishes" 130 | ) 131 | return 132 | 133 | config = await self.db.find_one({"_id": "config"}) 134 | 135 | if config is None or config["backedupAt"] is None: 136 | await ctx.send("No previous backup found, exiting") 137 | return 138 | 139 | await ctx.send( 140 | embed=await self.generate_embed( 141 | f"Are you sure you wanna restore data from backup db which" 142 | f" was last updated on **{config['backedupAt']} UTC**? `[y/n]`" 143 | ) 144 | ) 145 | msg: discord.Message = await self.bot.wait_for("message", check=check) 146 | if msg.content.lower() == "n": 147 | await ctx.send("Exiting!") 148 | return 149 | self.running = True 150 | if os.path.exists("./config.json"): 151 | with open("./config.json") as f: 152 | 153 | jd = json.load(f) 154 | try: 155 | backup_url = jd["BACKUP_MONGO_URI"] 156 | except KeyError: 157 | backup_url = os.getenv("BACKUP_MONGO_URI") 158 | if backup_url is None: 159 | await ctx.send( 160 | ":x: | No `BACKUP_MONGO_URI` found in `config.json` or environment variables" 161 | ) 162 | return 163 | else: 164 | backup_url = os.getenv("BACKUP_MONGO_URI") 165 | if backup_url is None: 166 | await ctx.send( 167 | ":x: | No `BACKUP_MONGO_URI` found in `config.json` or environment variables" 168 | ) 169 | return 170 | 171 | db_name = (backup_url.split("/"))[-1] 172 | backup_client = AsyncIOMotorClient(backup_url) 173 | if "mlab.com" in backup_url: 174 | bdb = backup_client[db_name] 175 | else: 176 | bdb = backup_client["backup_modmail_bot"] 177 | await ctx.send( 178 | embed=await self.generate_embed( 179 | "Connected to backup DB. Removing all documents from original db." 180 | ) 181 | ) 182 | collections = await self.bot.db.list_collection_names() 183 | 184 | if len(collections) > 0: 185 | for collection in collections: 186 | if collection == "system.indexes": 187 | continue 188 | 189 | await self.bot.db[collection].drop() 190 | await ctx.send( 191 | embed=await self.generate_embed("Deleted all documents from main db") 192 | ) 193 | else: 194 | await ctx.send( 195 | embed=await self.generate_embed( 196 | "No Existing collections found! Nothing was deleted!" 197 | ) 198 | ) 199 | du = await bdb.list_collection_names() 200 | for collection in du: 201 | if collection == "system.indexes": 202 | continue 203 | 204 | le = await bdb[str(collection)].find().to_list(None) 205 | for item in le: 206 | await self.bot.db[str(collection)].insert_one(item) 207 | del item 208 | del le 209 | await ctx.send( 210 | embed=await self.generate_embed(f"Restored `{str(collection)}`") 211 | ) 212 | await self.db.find_one_and_update( 213 | {"_id": "config"}, 214 | {"$set": {"restoredAt": str(datetime.datetime.utcnow())}}, 215 | upsert=True, 216 | ) 217 | await ctx.send(embed=await self.generate_embed(":tada: Restored Everything!")) 218 | self.running = False 219 | return 220 | 221 | async def generate_embed(self, msg: str): 222 | embed = discord.Embed(description=msg, color=discord.Colour.blurple()) 223 | return embed 224 | 225 | 226 | def setup(bot): 227 | bot.add_cog(BackupDB(bot)) 228 | -------------------------------------------------------------------------------- /birthday/birthday.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime 3 | import discord 4 | import logging 5 | import pytz 6 | 7 | from difflib import get_close_matches 8 | from discord.ext import commands 9 | from pytz import timezone 10 | 11 | from core import checks 12 | from core.models import PermissionLevel 13 | 14 | logger = logging.getLogger("Modmail") 15 | 16 | 17 | class BirthdayPlugin(commands.Cog): 18 | """ 19 | A birthday plugin. 20 | """ 21 | 22 | def __init__(self, bot): 23 | self.bot = bot 24 | self.db = bot.plugin_db.get_partition(self) 25 | self.birthdays = dict() 26 | self.roles = dict() 27 | self.channels = dict() 28 | self.timezone = "America/Chicago" 29 | self.messages = dict() 30 | self.enabled = True 31 | self.booted = True 32 | self.bot.loop.create_task(self._set_db()) 33 | 34 | async def _set_db(self): 35 | birthdays = await self.db.find_one({"_id": "birthdays"}) 36 | config = await self.db.find_one({"_id": "config"}) 37 | 38 | if birthdays is None: 39 | await self.db.find_one_and_update( 40 | {"_id": "birthdays"}, {"$set": {"birthdays": dict()}}, upsert=True 41 | ) 42 | 43 | birthdays = await self.db.find_one({"_id": "birthdays"}) 44 | 45 | if config is None: 46 | await self.db.find_one_and_update( 47 | {"_id": "config"}, 48 | { 49 | "$set": { 50 | "roles": dict(), 51 | "channels": dict(), 52 | "enabled": True, 53 | "timezone": "America/Chicago", 54 | "messages": dict(), 55 | } 56 | }, 57 | upsert=True, 58 | ) 59 | 60 | config = await self.db.find_one({"_id": "config"}) 61 | 62 | self.birthdays = birthdays.get("birthdays", dict()) 63 | self.roles = config.get("roles", dict()) 64 | self.channels = config.get("channels", dict()) 65 | self.enabled = config.get("enabled", True) 66 | self.timezone = config.get("timezone", "America/Chicago") 67 | self.messages = config.get("messages", dict()) 68 | self.bot.loop.create_task(self._handle_birthdays()) 69 | 70 | async def _update_birthdays(self): 71 | await self.db.find_one_and_update( 72 | {"_id": "birthdays"}, {"$set": {"birthdays": self.birthdays}}, upsert=True 73 | ) 74 | 75 | async def _update_config(self): 76 | await self.db.find_one_and_update( 77 | {"_id": "config"}, 78 | { 79 | "$set": { 80 | "roles": self.roles, 81 | "channels": self.channels, 82 | "enabled": self.enabled, 83 | "timezone": self.timezone, 84 | "messages": self.messages, 85 | } 86 | }, 87 | upsert=True, 88 | ) 89 | 90 | async def _handle_birthdays(self): 91 | while True: 92 | if not self.enabled: 93 | return 94 | 95 | if self.booted: 96 | custom_timezone = timezone(self.timezone) 97 | now = datetime.datetime.now(custom_timezone) 98 | sleep_time = ( 99 | now.replace(hour=0, minute=15, second=0, microsecond=0) - now 100 | ).seconds 101 | self.booted = False 102 | await asyncio.sleep(sleep_time) 103 | continue 104 | 105 | today = now.strftime("%d/%m/%Y").split("/") 106 | 107 | for user, obj in self.birthdays.items(): 108 | if obj["month"] != today[1] or obj["day"] != today[0]: 109 | continue 110 | guild = self.bot.get_guild(int(obj["guild"])) 111 | if guild is None: 112 | continue 113 | member = guild.get_member(int(user)) 114 | if member is None: 115 | continue 116 | 117 | if self.roles[obj["guild"]]: 118 | role = guild.get_role(int(self.roles[obj["guild"]])) 119 | if role: 120 | await member.add_roles(role, reason="Birthday Boi") 121 | 122 | if self.messages[obj["guild"]] and self.channels[obj["guild"]]: 123 | channel = guild.get_channel(int(self.channels[obj["guild"]])) 124 | if channel is None: 125 | continue 126 | age = today[2] - obj["year"] 127 | await channel.send( 128 | self.messages[obj["guild"]] 129 | .replace("{user.mention}", member.mention) 130 | .replace("{user}", str(member)) 131 | .replcae("{age}", age) 132 | ) 133 | continue 134 | 135 | custom_timezone = timezone(self.timezone) 136 | now = datetime.datetime.now(custom_timezone) 137 | sleep_time = ( 138 | now.replace(hour=0, minute=0, second=0, microsecond=0) - now 139 | ).seconds 140 | await asyncio.sleep(sleep_time) 141 | 142 | @commands.group(invoke_without_command=True) 143 | async def birthday(self, ctx: commands.Context): 144 | """ 145 | Birthday stuff. 146 | """ 147 | 148 | await ctx.send_help(ctx.command) 149 | return 150 | 151 | @birthday.command() 152 | async def set(self, ctx: commands.Context, date: str): 153 | """ 154 | Set your birthdate. 155 | 156 | **Format:** 157 | DD/MM/YYYY 158 | 159 | **Example:** 160 | {p}birthday set 26/12/2002 161 | """ 162 | 163 | try: 164 | birthday = date.split("/") 165 | if int(birthday[1]) > 13: 166 | await ctx.send(":x: | Invalid month provided.") 167 | return 168 | birthday_obj = {} 169 | birthday_obj["day"] = int(birthday[0]) 170 | birthday_obj["month"] = int(birthday[1]) 171 | birthday_obj["year"] = int(birthday[2]) 172 | birthday_obj["guild"] = str(ctx.guild.id) 173 | 174 | self.birthdays[str(ctx.author.id)] = birthday_obj 175 | await self._update_birthdays() 176 | await ctx.send(f"Done! You'r birthday was set to {date}") 177 | return 178 | except KeyError: 179 | logger.info(birthday[0]) 180 | logger.info(birthday[1]) 181 | logger.info(birthday[2]) 182 | 183 | await ctx.send("Please check the format of the date") 184 | return 185 | except Exception as e: 186 | await ctx.send(f":x: | An error occurred\n```{e}```") 187 | return 188 | 189 | @birthday.command() 190 | async def clear(self, ctx: commands.Context): 191 | """ 192 | Clear your birthday from the database. 193 | """ 194 | 195 | self.birthdays.pop(str(ctx.author.id)) 196 | await self._update_birthdays() 197 | await ctx.send(f"Done!") 198 | return 199 | 200 | @birthday.command() 201 | @checks.has_permissions(PermissionLevel.ADMIN) 202 | async def channel(self, ctx: commands.Context, channel: discord.TextChannel): 203 | """ 204 | Configure a channel for sending birthday announcements 205 | """ 206 | 207 | self.channels[str(ctx.guild.id)] = str(channel.id) 208 | await self._update_config() 209 | await ctx.send("Done!") 210 | return 211 | 212 | @birthday.command() 213 | @checks.has_permissions(PermissionLevel.ADMIN) 214 | async def role(self, ctx: commands.Context, role: discord.Role): 215 | """ 216 | Configure a role which will be added to the birthay boizzzz 217 | """ 218 | 219 | self.roles[str(ctx.guild.id)] = str(role.id) 220 | await self._update_config() 221 | await ctx.send("Done!") 222 | return 223 | 224 | @birthday.command() 225 | @checks.has_permissions(PermissionLevel.ADMIN) 226 | async def message(self, ctx: commands.Context, *, msg: str): 227 | """ 228 | Set a message to announce when wishing someone's birthday 229 | 230 | **Formatting:** 231 | • {user} - Name of he birthday boi 232 | • {user.mention} - Mention the birthday boi 233 | • {age} - Age of the birthday boiiii 234 | """ 235 | 236 | self.messages[str(ctx.guild.id)] = msg 237 | await self._update_config() 238 | await ctx.send("Done!") 239 | return 240 | 241 | @birthday.command() 242 | @checks.has_permissions(PermissionLevel.ADMIN) 243 | async def toggle(self, ctx: commands.Context): 244 | """ 245 | Enable / Disable this plugin 246 | """ 247 | 248 | self.enabled = not self.enabled 249 | await self._update_config() 250 | await ctx.send(f"{'Enabled' if self.enabled else 'Disabled'} the plugin :p") 251 | return 252 | 253 | @birthday.command() 254 | @checks.has_permissions(PermissionLevel.ADMIN) 255 | async def timezone(self, ctx: commands.Context, timezone: str): 256 | """ 257 | Set a timezone 258 | """ 259 | 260 | if timezone not in pytz.all_timezones: 261 | matches = get_close_matches(timezone, pytz.all_timezones) 262 | if len(matches) > 0: 263 | embed = discord.Embed() 264 | embed.color = 0xEB3446 265 | embed.description = f"Did you mean: \n`{'`, `'.join(matches)}`" 266 | await ctx.send(embed=embed) 267 | return 268 | else: 269 | await ctx.send("Couldn't find the timezone.") 270 | return 271 | 272 | self.timezone = timezone 273 | await self._update_config() 274 | await ctx.send("Done") 275 | return 276 | 277 | 278 | def setup(bot): 279 | bot.add_cog(BirthdayPlugin(bot)) 280 | -------------------------------------------------------------------------------- /birthday/requirements.txt: -------------------------------------------------------------------------------- 1 | pytz -------------------------------------------------------------------------------- /code/code.py: -------------------------------------------------------------------------------- 1 | import json 2 | from discord.ext import commands 3 | 4 | 5 | class CodeBlock: 6 | missing_error = "Missing code block. Please use the following markdown\n\\`\\`\\`language\ncode here\n\\`\\`\\`" 7 | 8 | def __init__(self, argument): 9 | try: 10 | block, code = argument.split("\n", 1) 11 | except ValueError: 12 | raise commands.BadArgument(self.missing_error) 13 | 14 | if not block.startswith("```") and not code.endswith("```"): 15 | raise commands.BadArgument(self.missing_error) 16 | 17 | language = block[3:] 18 | self.command = self.get_command_from_language(language.lower()) 19 | self.source = code.rstrip("`").replace("```", "") 20 | 21 | def get_command_from_language(self, language): 22 | cmds = { 23 | "cpp": "g++ -std=c++1z -O2 -Wall -Wextra -pedantic -pthread main.cpp -lstdc++fs && ./a.out", 24 | "c": "mv main.cpp main.c && gcc -std=c11 -O2 -Wall -Wextra -pedantic main.c && ./a.out", 25 | "py": "python3 main.cpp", 26 | "python": "python3 main.cpp", 27 | "haskell": "runhaskell main.cpp", 28 | } 29 | 30 | cpp = cmds["cpp"] 31 | for alias in ("cc", "h", "c++", "h++", "hpp"): 32 | cmds[alias] = cpp 33 | try: 34 | return cmds[language] 35 | except KeyError as e: 36 | if language: 37 | fmt = f"Unknown language to compile for: {language}" 38 | else: 39 | fmt = "Could not find a language to compile with." 40 | raise commands.BadArgument(fmt) from e 41 | 42 | 43 | class CodeCog(commands.Cog): 44 | """Compile & Run cpp,c,py,haskell code using coliru 45 | 46 | Please Dont Abuse 47 | """ 48 | 49 | def __init__(self, bot): 50 | self.bot = bot 51 | 52 | @commands.command(aliases=["code"]) 53 | async def coliru(self, ctx, code: CodeBlock): 54 | """Compiles Code Through coliru API 55 | 56 | You have to pass in a code block with the language syntax 57 | either set to one of these: 58 | - cpp 59 | - c 60 | - python 61 | - py 62 | - haskell 63 | 64 | Anything else isn't supported. The C++ compiler uses g++ -std=c++14. 65 | The python support is now 3.5.2. 66 | 67 | Please don't spam this for Stacked's sake. 68 | """ 69 | payload = {"cmd": code.command, "src": code.source} 70 | 71 | data = json.dumps(payload) 72 | 73 | async with self.bot.session.post( 74 | "http://coliru.stacked-crooked.com/compile", data=data 75 | ) as resp: 76 | if resp.status != 200: 77 | await ctx.send("Coliru did not respond in time.") 78 | return 79 | 80 | output = await resp.text(encoding="utf-8") 81 | 82 | if len(output) < 1992: 83 | await ctx.send(f"```\n{output}\n```") 84 | return 85 | 86 | # output is too big so post it in gist 87 | async with self.bot.session.post( 88 | "http://coliru.stacked-crooked.com/share", data=data 89 | ) as r: 90 | if r.status != 200: 91 | await ctx.send("Could not create coliru shared link") 92 | else: 93 | shared_id = await r.text() 94 | await ctx.send( 95 | f"Output too big. Coliru link: http://coliru.stacked-crooked.com/a/{shared_id}" 96 | ) 97 | 98 | 99 | def setup(bot): 100 | bot.add_cog(CodeCog(bot)) 101 | -------------------------------------------------------------------------------- /code/util/CodeBlock.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | 4 | 5 | class CodeBlock: 6 | missing_error = "Missing code block. Please use the following markdown\n\\`\\`\\`language\ncode here\n\\`\\`\\`" 7 | 8 | def __init__(self, argument): 9 | try: 10 | block, code = argument.split("\n", 1) 11 | except ValueError: 12 | raise commands.BadArgument(self.missing_error) 13 | 14 | if not block.startswith("```") and not code.endswith("```"): 15 | raise commands.BadArgument(self.missing_error) 16 | 17 | language = block[3:] 18 | self.command = self.get_command_from_language(language.lower()) 19 | self.source = code.rstrip("`").replace("```", "") 20 | 21 | def get_command_from_language(self, language): 22 | cmds = { 23 | "cpp": "g++ -std=c++1z -O2 -Wall -Wextra -pedantic -pthread main.cpp -lstdc++fs && ./a.out", 24 | "c": "mv main.cpp main.c && gcc -std=c11 -O2 -Wall -Wextra -pedantic main.c && ./a.out", 25 | "py": "python3 main.cpp", 26 | "python": "python3 main.cpp", 27 | "haskell": "runhaskell main.cpp", 28 | } 29 | 30 | cpp = cmds["cpp"] 31 | for alias in ("cc", "h", "c++", "h++", "hpp"): 32 | cmds[alias] = cpp 33 | try: 34 | return cmds[language] 35 | except KeyError as e: 36 | if language: 37 | fmt = f"Unknown language to compile for: {language}" 38 | else: 39 | fmt = "Could not find a language to compile with." 40 | raise commands.BadArgument(fmt) from e 41 | -------------------------------------------------------------------------------- /dashboard/dashboard.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from discord.ext import commands 4 | 5 | 6 | class Dasboard(commands.Cog): 7 | def __init__(self, bot): 8 | self.bot: discord.Client = bot 9 | self.db = bot.plugin_db.get_partition(self) 10 | asyncio.create_task(self.set_db()) 11 | 12 | async def set_db(self): 13 | await self.db.find_one_and_update( 14 | {"_id": "config"}, 15 | {"$set": {"log_uri": self.bot.config["log_url"].strip("/")}}, 16 | upsert=True, 17 | ) 18 | 19 | 20 | def setup(bot): 21 | bot.add_cog(Dasboard(bot)) 22 | -------------------------------------------------------------------------------- /dm-on-join/dm-on-join.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import discord 4 | from discord.ext import commands 5 | 6 | logger = logging.getLogger("Modmail") 7 | 8 | from core import checks 9 | from core.models import PermissionLevel 10 | 11 | 12 | class DmOnJoinPlugin(commands.Cog): 13 | def __init__(self, bot): 14 | self.bot = bot 15 | self.db = bot.plugin_db.get_partition(self) 16 | 17 | @commands.command(aliases=["sdms"]) 18 | @checks.has_permissions(PermissionLevel.ADMIN) 19 | async def setdmmessage(self, ctx, *, message): 20 | """Set a message to DM a user after they join.""" 21 | if message.startswith("https://") or message.startswith("http://"): 22 | # message is a URL 23 | if message.startswith("https://hasteb.in/"): 24 | message = "https://hasteb.in/raw/" + message.split("/")[-1] 25 | 26 | async with self.bot.session.get(message) as resp: 27 | message = await resp.text() 28 | 29 | await self.db.find_one_and_update( 30 | {"_id": "dm-config"}, 31 | {"$set": {"dm-message": {"message": message}}}, 32 | upsert=True, 33 | ) 34 | 35 | await ctx.send("Successfully set the message.") 36 | 37 | @commands.Cog.listener() 38 | async def on_member_join(self, member): 39 | config = await self.db.find_one({"_id": "dm-config"}) 40 | 41 | if config is None: 42 | logger.info("User joined, but no DM message was set.") 43 | return 44 | 45 | try: 46 | message = config["dm-message"]["message"] 47 | await member.send(message.replace("{user}", str(member))) 48 | except: 49 | return 50 | 51 | @commands.Cog.listener() 52 | async def on_ready(self): 53 | async with self.bot.session.post( 54 | "https://counter.modmail-plugins.piyush.codes/api/instances/dmonjoin", 55 | json={"id": self.bot.user.id}, 56 | ): 57 | print("Posted to plugin API") 58 | 59 | 60 | def setup(bot): 61 | bot.add_cog(DmOnJoinPlugin(bot)) 62 | -------------------------------------------------------------------------------- /fix/fix.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | 4 | from core.models import PermissionLevel 5 | from core import checks 6 | 7 | 8 | class TopicFixPlugin(commands.Cog): 9 | """ 10 | Fix all threads with broken channel topic 11 | """ 12 | 13 | def __init__(self, bot): 14 | self.bot = bot 15 | 16 | @commands.command(aliases=["f"]) 17 | @checks.has_permissions(PermissionLevel.SUPPORTER) 18 | async def fix(self, ctx): 19 | """ 20 | Fix a broken thread 21 | 22 | **Usage:** 23 | {prefix}fix 24 | """ 25 | genesis_message = await ctx.channel.history( 26 | oldest_first=True, limit=1 27 | ).flatten() 28 | if ( 29 | genesis_message[0].embeds 30 | and genesis_message[0].embeds[0] 31 | and genesis_message[0].embeds[0].footer.text 32 | and "User ID:" in genesis_message[0].embeds[0].footer.text 33 | ): 34 | await ctx.channel.edit( 35 | topic=f"User ID: {genesis_message[0].embeds[0].footer.text}", 36 | reason=f"Fix the thread. Command used by {ctx.author.name}#{ctx.author.discriminator}", 37 | ) 38 | await ctx.send("Fixed the thread.") 39 | else: 40 | await ctx.send("This channel doesn't seem like a modmail thread.") 41 | return 42 | 43 | @commands.Cog.listener() 44 | async def on_ready(self): 45 | async with self.bot.session.post( 46 | "https://counter.modmail-plugins.piyush.codes/api/instances/fix", 47 | json={"id": self.bot.user.id}, 48 | ): 49 | print("Posted to Plugin API") 50 | 51 | 52 | def setup(bot): 53 | bot.add_cog(TopicFixPlugin(bot)) 54 | -------------------------------------------------------------------------------- /github/github.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import discord 4 | from discord.ext import commands 5 | 6 | 7 | class GithubPlugin(commands.Cog): 8 | def __init__(self, bot): 9 | self.bot = bot 10 | self.colors = { 11 | "pr": { 12 | "open": 0x2CBE4E, 13 | "closed": discord.Embed.Empty, 14 | "merged": discord.Embed.Empty, 15 | }, 16 | "issues": {"open": 0xE68D60, "closed": discord.Embed.Empty}, 17 | } 18 | self.regex = r"(\S+)#(\d+)" 19 | 20 | @commands.Cog.listener() 21 | async def on_message(self, msg: discord.Message): 22 | match = re.search(self.regex, msg.content) 23 | 24 | if match: 25 | repo = match.group(1) 26 | num = match.group(2) 27 | 28 | if repo == "modmail": 29 | repo = "kyb3r/modmail" 30 | elif repo == "logviewer": 31 | repo = "kyb3r/logviewer" 32 | 33 | async with self.bot.session.get( 34 | f"https://api.github.com/repos/{repo}/pulls/{num}" 35 | ) as prr: 36 | prj = await prr.json() 37 | 38 | if "message" not in prj: 39 | em = await self.handlePR(prj, repo) 40 | return await msg.channel.send(embed=em) 41 | else: 42 | async with self.bot.session.get( 43 | f"https://api.github.com/repos/{repo}/issues/{num}" 44 | ) as err: 45 | erj = await err.json() 46 | 47 | if "message" in erj and erj["message"] == "Not Found": 48 | pass 49 | else: 50 | em = await self.handleIssue(erj, repo) 51 | return await msg.channel.send(embed=em) 52 | 53 | async def handlePR(self, data, repo): 54 | state = ( 55 | "merged" 56 | if (data["state"] == "closed" and data["merged"]) 57 | else data["state"] 58 | ) 59 | embed = self._base(data, repo, issue=False) 60 | embed.colour = self.colors["pr"][state] 61 | embed.add_field(name="Additions", value=data["additions"]) 62 | embed.add_field(name="Deletions", value=data["deletions"]) 63 | embed.add_field(name="Commits", value=data["commits"]) 64 | # embed.set_footer(text=f"Pull Request #{data['number']}") 65 | return embed 66 | 67 | async def handleIssue(self, data, repo): 68 | embed = self._base(data, repo) 69 | embed.colour = self.colors["issues"][data["state"]] 70 | # embed.set_footer(text=f"Issue #{data['number']}") 71 | return embed 72 | 73 | def _base(self, data, repo, issue=True): 74 | description = ( 75 | f"{data['body'].slice(0, 2045)}..." 76 | if len(data["body"]) > 2048 77 | else data["body"] 78 | ) 79 | 80 | _type = "Issue" if issue else "Pull request" 81 | 82 | rtitle = f"[{repo}] {_type}: #{data['number']} {data['title']}" 83 | title = f"{rtitle.slice(0, 253)}..." if len(rtitle) > 256 else rtitle 84 | embed = discord.Embed() 85 | # embed.set_thumbnail(url="https://images.piyush.codes/b/8rs7vC7.png") 86 | embed.set_author( 87 | name=data["user"]["login"], 88 | icon_url=data["user"]["avatar_url"], 89 | url=data["user"]["html_url"], 90 | ) 91 | embed.title = title 92 | embed.url = data["html_url"] 93 | embed.description = description 94 | embed.add_field(name="Status", value=data["state"], inline=True) 95 | if len(data["labels"]) > 0: 96 | embed.add_field( 97 | name="Labels", 98 | value=", ".join(str(label["name"]) for label in data["labels"]), 99 | ) 100 | return embed 101 | 102 | 103 | def setup(bot): 104 | bot.add_cog(GithubPlugin(bot)) 105 | -------------------------------------------------------------------------------- /giveaway/giveaway.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiohttp 3 | import discord 4 | import math 5 | import random 6 | import time 7 | from datetime import datetime 8 | from discord.ext import commands 9 | from discord.ext.commands.errors import BadArgument 10 | 11 | from core import checks 12 | from core.models import PermissionLevel 13 | 14 | 15 | class GiveawayPlugin(commands.Cog): 16 | """ 17 | Host giveaways on your server with this ~~amazing~~ plugin 18 | """ 19 | 20 | def __init__(self, bot): 21 | self.bot: discord.Client = bot 22 | self.db = bot.plugin_db.get_partition(self) 23 | self.active_giveaways = {} 24 | asyncio.create_task(self._set_giveaways_from_db()) 25 | 26 | async def _set_giveaways_from_db(self): 27 | config = await self.db.find_one({"_id": "config"}) 28 | if config is None: 29 | await self.db.find_one_and_update( 30 | {"_id": "config"}, 31 | {"$set": {"giveaways": dict()}}, 32 | upsert=True, 33 | ) 34 | 35 | for key, giveaway in config.get("giveaways", {}).items(): 36 | if key in self.active_giveaways: 37 | continue 38 | self.active_giveaways[str(key)] = giveaway 39 | self.bot.loop.create_task(self._handle_giveaway(giveaway)) 40 | 41 | async def _update_db(self): 42 | await self.db.find_one_and_update( 43 | {"_id": "config"}, 44 | {"$set": {"giveaways": self.active_giveaways}}, 45 | upsert=True, 46 | ) 47 | 48 | async def _handle_giveaway(self, giveaway): 49 | if str(giveaway["message"]) not in self.active_giveaways or giveaway['ended']: 50 | return 51 | 52 | async def get_random_user(users, _guild, _winners): 53 | rnd = random.choice(users) 54 | in_guild = _guild.get_member(rnd) 55 | if rnd in _winners or in_guild is None or in_guild.id == self.bot.user.id: 56 | idk = await get_random_user(users, _guild, _winners) 57 | return idk 58 | win = [] + _winners 59 | win.append(rnd) 60 | return win 61 | 62 | while True: 63 | if str(giveaway["message"]) not in self.active_giveaways or giveaway['ended']: 64 | break 65 | channel: discord.TextChannel = self.bot.get_channel( 66 | int(giveaway["channel"]) 67 | ) 68 | if channel is None: 69 | try: 70 | self.active_giveaways.pop(str(giveaway["message"])) 71 | await self._update_db() 72 | except: 73 | pass 74 | return 75 | message = await channel.fetch_message(giveaway["message"]) 76 | if message is None or not message.embeds or message.embeds[0] is None: 77 | try: 78 | self.active_giveaways.pop(str(giveaway["message"])) 79 | await self._update_db() 80 | except: 81 | pass 82 | return 83 | guild: discord.Guild = self.bot.get_guild(giveaway["guild"]) 84 | g_time = giveaway["time"] - time.time() 85 | 86 | if g_time <= 0 and not giveaway["ended"]: 87 | if len(message.reactions) <= 0: 88 | embed = message.embeds[0] 89 | embed.description = ( 90 | f"Giveaway has ended!\n\nSadly no one participated :(" 91 | ) 92 | embed.set_footer( 93 | text=f"{giveaway['winners']} {'winners' if giveaway['winners'] > 1 else 'winner'} | Ended at" 94 | ) 95 | await message.edit(embed=embed) 96 | giveaway['ended'] = True 97 | self.active_giveaways[str(giveaway["message"])] = giveaway 98 | await self._update_db() 99 | break 100 | 101 | to_break = False 102 | 103 | for r in message.reactions: 104 | if str(giveaway["message"]) not in self.active_giveaways: 105 | break 106 | 107 | if r.emoji == "🎉": 108 | reactions = r 109 | reacted_users = await reactions.users().flatten() 110 | if len(reacted_users) <= 1: 111 | embed = message.embeds[0] 112 | embed.description = ( 113 | f"Giveaway has ended!\n\nSadly no one participated :(" 114 | ) 115 | embed.set_footer( 116 | text=f"{giveaway['winners']} {'winners' if giveaway['winners'] > 1 else 'winner'} | " 117 | f"Ended at" 118 | ) 119 | await message.edit(embed=embed) 120 | giveaway['ended'] = True 121 | self.active_giveaways[str(giveaway["message"])] = giveaway 122 | await self._update_db() 123 | del guild, channel, reacted_users, embed 124 | break 125 | 126 | # -1 cuz 1 for self 127 | if giveaway["winners"] > (len(reacted_users) - 1): 128 | giveaway["winners"] = len(reacted_users) - 1 129 | 130 | winners = [] 131 | 132 | for index in range(len(reacted_users)): 133 | reacted_users[index] = reacted_users[index].id 134 | 135 | for _ in range(giveaway["winners"]): 136 | winners = await get_random_user( 137 | reacted_users, guild, winners 138 | ) 139 | 140 | embed = message.embeds[0] 141 | winners_text = "" 142 | for winner in winners: 143 | winners_text += f"<@{winner}> " 144 | 145 | embed.description = f"Giveaway has ended!\n\n**{'Winners' if giveaway['winners'] > 1 else 'Winner'}:** {winners_text} " 146 | embed.set_footer( 147 | text=f"{giveaway['winners']} {'winners' if giveaway['winners'] > 1 else 'winner'} | " 148 | f"Ended at" 149 | ) 150 | await message.edit(embed=embed) 151 | await channel.send( 152 | f"🎉 Congratulations {winners_text}, you have won **{giveaway['item']}**!" 153 | ) 154 | try: 155 | giveaway['ended'] = True 156 | self.active_giveaways[str(giveaway["message"])] = giveaway 157 | await self._update_db() 158 | except: 159 | pass 160 | del winners_text, winners, guild, channel, reacted_users, embed 161 | to_break = True 162 | break 163 | 164 | if to_break: 165 | break 166 | else: 167 | 168 | time_remaining = f"{math.floor(g_time // 86400)} Days, {math.floor(g_time // 3600 % 24)} Hours, {math.floor(g_time // 60 % 60)} Minutes, {math.floor(g_time % 60)} Seconds " 169 | description = f"React with 🎉 to enter the giveaway!\nTime Remaining: **{time_remaining}**" 170 | 171 | if giveaway['role'] is not None: 172 | description = description + f"\nMust have role: <@&{giveaway['role']}>" 173 | 174 | embed = message.embeds[0] 175 | embed.description = description 176 | await message.edit(embed=embed) 177 | del channel, guild 178 | await asyncio.sleep( 179 | 60 if g_time > 60 else (5 if g_time > 5 else g_time) 180 | ) 181 | 182 | return 183 | 184 | @commands.Cog.listener() 185 | async def on_reaction_add(self, reaction: discord.Reaction, user: discord.User): 186 | if user.bot or str(reaction.message.id) not in self.active_giveaways.keys(): 187 | return 188 | 189 | giveaway = self.active_giveaways[str(reaction.message.id)] 190 | member = reaction.message.guild.get_member(user.id) 191 | 192 | if giveaway['role'] is not None: 193 | role: discord.Role = reaction.message.guild.get_role(giveaway['role']) 194 | if role is not None and member not in role.members: 195 | try: 196 | await reaction.remove(user=user) 197 | await user.send(f"You do not have role **{role.name}**. So you can't participate in the giveaway. ") 198 | except: 199 | pass 200 | 201 | @commands.group( 202 | name="giveaway", 203 | aliases=["g", "giveaways", "gaway", "givea"], 204 | invoke_without_command=True, 205 | ) 206 | @commands.guild_only() 207 | @checks.has_permissions(PermissionLevel.ADMIN) 208 | async def giveaway(self, ctx: commands.Context): 209 | """ 210 | Create / Stop Giveaways 211 | """ 212 | await ctx.send_help(ctx.command) 213 | return 214 | 215 | @checks.has_permissions(PermissionLevel.ADMIN) 216 | @giveaway.command(name="start", aliases=["create", "c", "s"]) 217 | async def start(self, ctx: commands.Context, channel: discord.TextChannel): 218 | """ 219 | Start a giveaway in interactive mode 220 | """ 221 | 222 | def check(msg: discord.Message): 223 | return ( 224 | ctx.author == msg.author 225 | and ctx.channel == msg.channel 226 | and (len(msg.content) < 2048) 227 | ) 228 | 229 | def cancel_check(msg: discord.Message): 230 | return msg.content == "cancel" or msg.content == f"{ctx.prefix}cancel" 231 | 232 | embed = discord.Embed(colour=0x00FF00) 233 | 234 | await ctx.send(embed=self.generate_embed("What is the giveaway item?")) 235 | giveaway_item = await self.bot.wait_for("message", check=check) 236 | if cancel_check(giveaway_item) is True: 237 | await ctx.send("Cancelled.") 238 | return 239 | embed.title = giveaway_item.content 240 | await ctx.send( 241 | embed=self.generate_embed("How many winners are to be selected?") 242 | ) 243 | giveaway_winners = await self.bot.wait_for("message", check=check) 244 | if cancel_check(giveaway_winners) is True: 245 | await ctx.send("Cancelled.") 246 | return 247 | try: 248 | giveaway_winners = int(giveaway_winners.content) 249 | except: 250 | await ctx.send( 251 | "Unable to parse giveaway winners to numbers, exiting. Make sure to pass numbers from next " 252 | "time" 253 | ) 254 | return 255 | 256 | if giveaway_winners <= 0: 257 | await ctx.send( 258 | "Giveaway can only be held with 1 or more winners. Cancelling command." 259 | ) 260 | return 261 | 262 | await ctx.send( 263 | embed=self.generate_embed( 264 | "How long will the giveaway last?\n\n2d / 2days / 2day -> 2 days\n" 265 | "2m -> 2 minutes\n2 months -> 2 months" 266 | "\ntomorrow / in 10 minutes / 2h 10minutes work too\n" 267 | ) 268 | ) 269 | time_cancel = False 270 | while True: 271 | giveaway_time = await self.bot.wait_for("message", check=check) 272 | if cancel_check(giveaway_time) is True: 273 | time_cancel = True 274 | await ctx.send("Cancelled.") 275 | break 276 | resp = await self.bot.session.get( 277 | "https://dateparser.hastebin.cc", 278 | params={"date": f"in {giveaway_time.content}"}, 279 | ) 280 | if resp.status == 400: 281 | await ctx.send( 282 | "I was not able to parse the time properly, please try again." 283 | ) 284 | continue 285 | elif resp.status == 500: 286 | await ctx.send("The dateparser API seems to have some problems.") 287 | time_cancel = True 288 | break 289 | else: 290 | json = await resp.json() 291 | giveaway_time = json["message"] 292 | break 293 | await ctx.send(embed=self.generate_embed("Roles member must have to participate in the giveaway.\n\nIf no requirements then type `No`")) 294 | while True: 295 | giveaway_role = await self.bot.wait_for("message", check=check) 296 | 297 | if giveaway_role.content.lower() == 'no': 298 | giveaway_role = None 299 | break 300 | 301 | if cancel_check(giveaway_role) is True: 302 | time_cancel = True 303 | await ctx.send("Cancelled.") 304 | break 305 | 306 | try: 307 | giveaway_role = await commands.RoleConverter().convert(ctx, giveaway_role.content) 308 | break 309 | except BadArgument: 310 | await ctx.send(embed=self.generate_embed(f"Not able to find any role with argument '{giveaway_role.content}'. Try again!")) 311 | 312 | if time_cancel is True: 313 | return 314 | 315 | description = f"React with 🎉 to enter the giveaway!\n\n" 316 | description = description+ f"Time Remaining: **{datetime.fromtimestamp(giveaway_time).strftime('%d %H:%M:%S')}**" 317 | if giveaway_role is not None: 318 | description = description + f"\nMust have role: <@&{giveaway_role.id}>" 319 | 320 | embed.description = (description) 321 | embed.set_footer( 322 | text=f"{giveaway_winners} {'winners' if giveaway_winners > 1 else 'winner'} | Ends at" 323 | ) 324 | embed.timestamp = datetime.fromtimestamp(giveaway_time) 325 | msg: discord.Message = await channel.send(embed=embed) 326 | await msg.add_reaction("🎉") 327 | giveaway_obj = { 328 | "ended": False, 329 | "item": giveaway_item.content, 330 | "winners": giveaway_winners, 331 | "time": giveaway_time, 332 | "guild": ctx.guild.id, 333 | "channel": channel.id, 334 | "message": msg.id 335 | } 336 | if giveaway_role is not None: 337 | giveaway_obj["role"] = giveaway_role.id 338 | else: 339 | giveaway_obj['role'] = None 340 | 341 | self.active_giveaways[str(msg.id)] = giveaway_obj 342 | await ctx.send(f"Done! Giveaway started [here](<{msg.jump_url}>)") 343 | await self._update_db() 344 | await self._start_new_giveaway_thread(giveaway_obj) 345 | 346 | @checks.has_permissions(PermissionLevel.ADMIN) 347 | @giveaway.command(name="reroll", aliases=["rroll"]) 348 | async def reroll(self, ctx: commands.Context, _id: str, winners_count: int): 349 | """ 350 | Reroll the giveaway 351 | 352 | **Usage:** 353 | {prefix}giveaway reroll 354 | """ 355 | 356 | # Don't roll if giveaway is active 357 | if _id in self.active_giveaways: 358 | await ctx.send("Sorry, but you can't reroll an active giveaway.") 359 | return 360 | 361 | async def get_random_user(users, _guild, _winners): 362 | rnd = random.choice(users) 363 | in_guild = _guild.get_member(rnd) 364 | if rnd in _winners or in_guild is None or in_guild.id == self.bot.user.id: 365 | idk = await get_random_user(users, _guild, _winners) 366 | return idk 367 | win = [] + _winners 368 | win.append(rnd) 369 | return win 370 | 371 | try: 372 | message = await ctx.channel.fetch_message(int(_id)) 373 | except discord.Forbidden: 374 | await ctx.send("No permission to read the history.") 375 | return 376 | except discord.NotFound: 377 | await ctx.send("Message not found.") 378 | return 379 | 380 | if not message.embeds or message.embeds[0] is None: 381 | await ctx.send( 382 | "The given message doesn't have an embed, so it isn't related to a giveaway." 383 | ) 384 | return 385 | 386 | if len(message.reactions) <= 0: 387 | embed = message.embeds[0] 388 | embed.description = f"Giveaway has ended!\n\nSadly no one participated :(" 389 | embed.set_footer( 390 | text=f"{winners_count} {'winners' if winners_count > 1 else 'winner'} | Ended at" 391 | ) 392 | await message.edit(embed=embed) 393 | return 394 | 395 | for r in message.reactions: 396 | if r.emoji == "🎉": 397 | reactions = r 398 | reacted_users = await reactions.users().flatten() 399 | if len(reacted_users) <= 1: 400 | embed = message.embeds[0] 401 | embed.description = ( 402 | f"Giveaway has ended!\n\nSadly no one participated :(" 403 | ) 404 | await message.edit(embed=embed) 405 | del reacted_users, embed 406 | break 407 | 408 | # -1 cuz 1 for self 409 | if winners_count > (len(reacted_users) - 1): 410 | winners_count = len(reacted_users) - 1 411 | 412 | winners = [] 413 | 414 | for index in range(len(reacted_users)): 415 | reacted_users[index] = reacted_users[index].id 416 | 417 | for _ in range(winners_count): 418 | winners = await get_random_user(reacted_users, ctx.guild, winners) 419 | 420 | embed = message.embeds[0] 421 | winners_text = "" 422 | for winner in winners: 423 | winners_text += f"<@{winner}> " 424 | 425 | embed.description = f"Giveaway has ended!\n\n**{'Winners' if winners_count > 1 else 'Winner'}:** {winners_text}" 426 | embed.set_footer( 427 | text=f"{winners_count} {'winners' if winners_count > 1 else 'winner'} | Ended at" 428 | ) 429 | await message.edit(embed=embed) 430 | await ctx.channel.send( 431 | f"🎉 Congratulations {winners_text}, you have won **{embed.title}**!" 432 | ) 433 | del winners_text, winners, winners_count, reacted_users, embed 434 | break 435 | 436 | @giveaway.command(name="cancel", aliases=["stop"]) 437 | @checks.has_permissions(PermissionLevel.ADMIN) 438 | async def cancel(self, ctx: commands.Context, _id: str): 439 | """ 440 | Stop an active giveaway 441 | 442 | **Usage:** 443 | {prefix}giveaway stop 444 | """ 445 | 446 | if _id not in self.active_giveaways: 447 | await ctx.send("Couldn't find an active giveaway with that ID!") 448 | return 449 | 450 | giveaway = self.active_giveaways[_id] 451 | channel: discord.TextChannel = self.bot.get_channel(int(giveaway["channel"])) 452 | try: 453 | message = await channel.fetch_message(int(_id)) 454 | except discord.Forbidden: 455 | await ctx.send("No permission to read the history.") 456 | return 457 | except discord.NotFound: 458 | await ctx.send("Message not found.") 459 | return 460 | 461 | if not message.embeds or message.embeds[0] is None: 462 | await ctx.send( 463 | "The given message doesn't have an embed, so it isn't related to a giveaway." 464 | ) 465 | return 466 | 467 | embed = message.embeds[0] 468 | embed.description = "The giveaway has been cancelled." 469 | await message.edit(embed=embed) 470 | self.active_giveaways.pop(_id) 471 | await self._update_db() 472 | await ctx.send("Cancelled!") 473 | return 474 | 475 | async def _start_new_giveaway_thread(self, obj): 476 | await self.bot.loop.create_task(self._handle_giveaway(obj)) 477 | 478 | def generate_embed(self, description: str): 479 | embed = discord.Embed() 480 | embed.colour = self.bot.main_color 481 | embed.description = description 482 | 483 | return embed 484 | 485 | 486 | def setup(bot): 487 | bot.add_cog(GiveawayPlugin(bot)) 488 | -------------------------------------------------------------------------------- /hastebin/README.md: -------------------------------------------------------------------------------- 1 |
2 | BD Image 3 |
4 | A plugin to upload text to hastebin. 5 |
6 |
7 | 8 | 9 | Support 10 | 11 |
12 | 13 | --- -------------------------------------------------------------------------------- /hastebin/hastebin.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import os 3 | from discord import Embed 4 | from discord.ext import commands 5 | 6 | from json import JSONDecodeError 7 | from aiohttp import ClientResponseError 8 | 9 | 10 | class HastebinCog(commands.Cog): 11 | def __init__(self, bot): 12 | self.bot = bot 13 | 14 | @commands.command() 15 | async def hastebin(self, ctx, *, message): 16 | """Upload text to hastebin""" 17 | haste_url = os.environ.get("HASTE_URL", "https://hastebin.cc") 18 | 19 | try: 20 | async with self.bot.session.post( 21 | haste_url + "/documents", data=message 22 | ) as resp: 23 | key = (await resp.json())["key"] 24 | embed = Embed( 25 | title="Your uploaded file", 26 | color=self.bot.main_color, 27 | description=f"{haste_url}/" + key, 28 | ) 29 | except (JSONDecodeError, ClientResponseError, IndexError): 30 | embed = Embed( 31 | color=self.bot.main_color, 32 | description="Something went wrong. " 33 | "We're unable to upload your text to hastebin.", 34 | ) 35 | embed.set_footer(text="Hastebin Plugin") 36 | await ctx.send(embed=embed) 37 | 38 | @commands.Cog.listener() 39 | async def on_ready(self): 40 | async with self.bot.session.post( 41 | "https://counter.modmail-plugins.piyush.codes/api/instances/hastebin", 42 | json={"id": self.bot.user.id}, 43 | ): 44 | print("Posted to Plugin API") 45 | 46 | 47 | def setup(bot): 48 | bot.add_cog(HastebinCog(bot)) 49 | -------------------------------------------------------------------------------- /jishaku/jishaku.py: -------------------------------------------------------------------------------- 1 | def setup(bot): 2 | bot.load_extension("jishaku") 3 | -------------------------------------------------------------------------------- /jishaku/requirements.txt: -------------------------------------------------------------------------------- 1 | jishaku -------------------------------------------------------------------------------- /leave-server/README.md: -------------------------------------------------------------------------------- 1 |
2 | LS Image 3 |
4 | A plugin For ModMail Bot to force it to leave a specified server. 5 |
6 |
7 | 8 | 9 | Support 10 | 11 |
12 | 13 | --- 14 | -------------------------------------------------------------------------------- /leave-server/leave-server.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord.ext import commands 3 | 4 | 5 | class LeaveGuildPlugin(commands.Cog): 6 | def __init__(self, bot): 7 | self.bot = bot 8 | 9 | @commands.command() 10 | @commands.is_owner() 11 | async def leaveguild(self, ctx, guild_id: int): 12 | """ 13 | Force your bot to leave a specified server 14 | """ 15 | try: 16 | await self.bot.get_guild(guild_id).leave() 17 | await ctx.send("Left!") 18 | return 19 | except: 20 | await ctx.send("Error!") 21 | return 22 | 23 | @commands.Cog.listener() 24 | async def on_ready(self): 25 | async with self.bot.session.post( 26 | "https://counter.modmail-plugins.piyush.codes/api/instances/leaveserver", 27 | json={"id": self.bot.user.id}, 28 | ): 29 | print("Posted to Plugin API") 30 | 31 | 32 | def setup(bot): 33 | bot.add_cog(LeaveGuildPlugin(bot)) 34 | -------------------------------------------------------------------------------- /moderation/moderation.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | 4 | logger = logging.getLogger("Modmail") 5 | 6 | import discord 7 | import typing 8 | from discord.ext import commands 9 | 10 | from core import checks 11 | from core.models import PermissionLevel 12 | 13 | 14 | class ModerationPlugin(commands.Cog): 15 | """ 16 | Moderate ya server using modmail pog 17 | """ 18 | 19 | def __init__(self, bot): 20 | self.bot = bot 21 | self.db = bot.plugin_db.get_partition(self) 22 | 23 | @commands.group(invoke_without_command=True) 24 | @commands.guild_only() 25 | @checks.has_permissions(PermissionLevel.ADMIN) 26 | async def moderation(self, ctx: commands.Context): 27 | """ 28 | Settings and stuff 29 | """ 30 | await ctx.send_help(ctx.command) 31 | return 32 | 33 | @moderation.command() 34 | @checks.has_permissions(PermissionLevel.ADMIN) 35 | async def channel(self, ctx: commands.Context, channel: discord.TextChannel): 36 | """ 37 | Set the log channel for moderation actions. 38 | """ 39 | 40 | await self.db.find_one_and_update( 41 | {"_id": "config"}, {"$set": {"channel": channel.id}}, upsert=True 42 | ) 43 | 44 | await ctx.send("Done!") 45 | return 46 | 47 | @commands.command(aliases=["banhammer"]) 48 | @checks.has_permissions(PermissionLevel.MODERATOR) 49 | async def ban( 50 | self, 51 | ctx: commands.Context, 52 | members: commands.Greedy[discord.Member], 53 | days: typing.Optional[int] = 0, 54 | *, 55 | reason: str = None, 56 | ): 57 | """Ban one or more users. 58 | Usage: 59 | {prefix}ban @member 10 Advertising their own products 60 | {prefix}ban @member1 @member2 @member3 Spamming 61 | """ 62 | 63 | config = await self.db.find_one({"_id": "config"}) 64 | 65 | if config is None: 66 | return await ctx.send("There's no configured log channel.") 67 | else: 68 | channel = ctx.guild.get_channel(int(config["channel"])) 69 | 70 | if channel is None: 71 | await ctx.send("There is no configured log channel.") 72 | return 73 | 74 | try: 75 | for member in members: 76 | await member.ban( 77 | delete_message_days=days, reason=f"{reason if reason else None}" 78 | ) 79 | 80 | embed = discord.Embed( 81 | color=discord.Color.red(), 82 | title=f"{member} was banned!", 83 | timestamp=datetime.datetime.utcnow(), 84 | ) 85 | 86 | embed.add_field( 87 | name="Moderator", 88 | value=f"{ctx.author}", 89 | inline=False, 90 | ) 91 | 92 | if reason: 93 | embed.add_field(name="Reason", value=reason, inline=False) 94 | 95 | await ctx.send(f"🚫 | {member} is banned!") 96 | await channel.send(embed=embed) 97 | 98 | except discord.Forbidden: 99 | await ctx.send("I don't have the proper permissions to ban people.") 100 | 101 | except Exception as e: 102 | await ctx.send( 103 | "An unexpected error occurred, please check the logs for more details." 104 | ) 105 | logger.error(e) 106 | return 107 | 108 | @commands.command(aliases=["getout"]) 109 | @checks.has_permissions(PermissionLevel.MODERATOR) 110 | async def kick( 111 | self, ctx, members: commands.Greedy[discord.Member], *, reason: str = None 112 | ): 113 | """Kick one or more users. 114 | Usage: 115 | {prefix}kick @member Being rude 116 | {prefix}kick @member1 @member2 @member3 Advertising 117 | """ 118 | 119 | config = await self.db.find_one({"_id": "config"}) 120 | 121 | if config is None: 122 | return await ctx.send("There's no configured log channel.") 123 | else: 124 | channel = ctx.guild.get_channel(int(config["channel"])) 125 | 126 | if channel is None: 127 | await ctx.send("There is no configured log channel.") 128 | return 129 | 130 | try: 131 | for member in members: 132 | await member.kick(reason=f"{reason if reason else None}") 133 | embed = discord.Embed( 134 | color=discord.Color.red(), 135 | title=f"{member} was kicked!", 136 | timestamp=datetime.datetime.utcnow(), 137 | ) 138 | 139 | embed.add_field( 140 | name="Moderator", 141 | value=f"{ctx.author}", 142 | inline=False, 143 | ) 144 | 145 | if reason is not None: 146 | embed.add_field(name="Reason", value=reason, inline=False) 147 | 148 | await ctx.send(f"🦶 | {member} is kicked!") 149 | await channel.send(embed=embed) 150 | 151 | except discord.Forbidden: 152 | await ctx.send("I don't have the proper permissions to kick people.") 153 | 154 | except Exception as e: 155 | await ctx.send( 156 | "An unexpected error occurred, please check the Heroku logs for more details." 157 | ) 158 | logger.error(e) 159 | return 160 | 161 | @commands.command() 162 | @checks.has_permissions(PermissionLevel.MODERATOR) 163 | async def warn(self, ctx, member: discord.Member, *, reason: str): 164 | """Warn a member. 165 | Usage: 166 | {prefix}warn @member Spoilers 167 | """ 168 | 169 | if member.bot: 170 | return await ctx.send("Bots can't be warned.") 171 | 172 | channel_config = await self.db.find_one({"_id": "config"}) 173 | 174 | if channel_config is None: 175 | return await ctx.send("There's no configured log channel.") 176 | else: 177 | channel = ctx.guild.get_channel(int(channel_config["channel"])) 178 | 179 | if channel is None: 180 | return 181 | 182 | config = await self.db.find_one({"_id": "warns"}) 183 | 184 | if config is None: 185 | config = await self.db.insert_one({"_id": "warns"}) 186 | 187 | try: 188 | userwarns = config[str(member.id)] 189 | except KeyError: 190 | userwarns = config[str(member.id)] = [] 191 | 192 | if userwarns is None: 193 | userw = [] 194 | else: 195 | userw = userwarns.copy() 196 | 197 | userw.append({"reason": reason, "mod": ctx.author.id}) 198 | 199 | await self.db.find_one_and_update( 200 | {"_id": "warns"}, {"$set": {str(member.id): userw}}, upsert=True 201 | ) 202 | 203 | await ctx.send(f"Successfully warned **{member}**\n`{reason}`") 204 | 205 | await channel.send( 206 | embed=await self.generateWarnEmbed( 207 | str(member.id), str(ctx.author.id), len(userw), reason 208 | ) 209 | ) 210 | del userw 211 | return 212 | 213 | @commands.command() 214 | @checks.has_permissions(PermissionLevel.MODERATOR) 215 | async def pardon(self, ctx, member: discord.Member, *, reason: str): 216 | """Remove all warnings of a member. 217 | Usage: 218 | {prefix}pardon @member Nice guy 219 | """ 220 | 221 | if member.bot: 222 | return await ctx.send("Bots can't be warned, so they can't be pardoned.") 223 | 224 | channel_config = await self.db.find_one({"_id": "config"}) 225 | 226 | if channel_config is None: 227 | return await ctx.send("There's no configured log channel.") 228 | else: 229 | channel = ctx.guild.get_channel(int(channel_config["channel"])) 230 | 231 | if channel is None: 232 | return 233 | 234 | config = await self.db.find_one({"_id": "warns"}) 235 | 236 | if config is None: 237 | return 238 | 239 | try: 240 | userwarns = config[str(member.id)] 241 | except KeyError: 242 | return await ctx.send(f"{member} doesn't have any warnings.") 243 | 244 | if userwarns is None: 245 | await ctx.send(f"{member} doesn't have any warnings.") 246 | 247 | await self.db.find_one_and_update( 248 | {"_id": "warns"}, {"$set": {str(member.id): []}} 249 | ) 250 | 251 | await ctx.send(f"Successfully pardoned **{member}**\n`{reason}`") 252 | 253 | embed = discord.Embed(color=discord.Color.blue()) 254 | 255 | embed.set_author( 256 | name=f"Pardon | {member}", 257 | icon_url=member.avatar_url, 258 | ) 259 | embed.add_field(name="User", value=f"{member}") 260 | embed.add_field( 261 | name="Moderator", 262 | value=f"<@{ctx.author.id}> - `{ctx.author}`", 263 | ) 264 | embed.add_field(name="Reason", value=reason) 265 | embed.add_field(name="Total Warnings", value="0") 266 | 267 | return await channel.send(embed=embed) 268 | 269 | async def generateWarnEmbed(self, memberid, modid, warning, reason): 270 | member: discord.User = await self.bot.fetch_user(int(memberid)) 271 | mod: discord.User = await self.bot.fetch_user(int(modid)) 272 | 273 | embed = discord.Embed(color=discord.Color.red()) 274 | 275 | embed.set_author( 276 | name=f"Warn | {member}", 277 | icon_url=member.avatar_url, 278 | ) 279 | embed.add_field(name="User", value=f"{member}") 280 | embed.add_field(name="Moderator", value=f"<@{modid}>` - ({mod})`") 281 | embed.add_field(name="Reason", value=reason) 282 | embed.add_field(name="Total Warnings", value=warning) 283 | return embed 284 | 285 | 286 | def setup(bot): 287 | bot.add_cog(ModerationPlugin(bot)) 288 | -------------------------------------------------------------------------------- /moderation/utils/Log.py: -------------------------------------------------------------------------------- 1 | import discord 2 | 3 | 4 | class Log: 5 | def __init__(self, guild: discord.Guild, db): 6 | self.guild: discord.Guild = guild 7 | self.db = db 8 | self.channel = None 9 | 10 | async def _set_channel(self): 11 | config = await self.db.find_one({"_id": "config"}) 12 | if config is None or config["channel"] is None: 13 | return 14 | self.channel: discord.TextChannel = await self.guild.get_channel( 15 | int(config["channel"]) 16 | ) 17 | 18 | async def log( 19 | self, type: str, user: discord.User, mod: discord.User, *, reason: str 20 | ): 21 | if self.channel is None: 22 | return f"No Log Channel has been setup for {self.guild.name}" 23 | else: 24 | embed = discord.Embed() 25 | embed.set_author(name=f"{type} | {user.name}#{user.discriminator}") 26 | embed.add_field( 27 | name="User", value=f"<@{user.id}> `({user.name}#{user.discriminator})`" 28 | ) 29 | -------------------------------------------------------------------------------- /music/music.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import lavalink 4 | import discord 5 | import re 6 | import math 7 | from discord.ext import commands 8 | 9 | 10 | url_rx = re.compile("https?:\\/\\/(?:www\\.)?.+") # noqa: W605 11 | 12 | 13 | class MusicPlugin(commands.Cog): 14 | def __init__(self, bot): 15 | self.bot = bot 16 | self.db = bot.plugin_db.get_partition(self) 17 | self.lavalink = {"host": "", "password": "", "port": 2333} 18 | asyncio.create_task(self.update()) 19 | 20 | def update(self): 21 | self.lavalink["host"] = os.getenv("ll_host") 22 | self.lavalink["port"] = os.getenv("ll_port") 23 | self.lavalink["password"] = os.getenv("ll_password") 24 | if not hasattr( 25 | self.bot, "lavalink" 26 | ): # This ensures the client isn't overwritten during cog reloads. 27 | self.bot.lavalink = lavalink.Client(self.bot.user.id) 28 | self.bot.lavalink.add_node( 29 | self.lavalink["host"], 30 | self.lavalink["port"], 31 | self.lavalink["password"], 32 | os.getenv("ll_region", "eu"), 33 | "default-node", 34 | ) # Host, Port, Password, Region, Name 35 | self.bot.add_listener( 36 | self.bot.lavalink.voice_update_handler, "on_socket_response" 37 | ) 38 | 39 | @commands.command() 40 | async def join(self, ctx: commands.Context, channel: str): 41 | ws = self.bot._connection._get_websocket(ctx.guild.id) 42 | await ws.voice_state(str(ctx.guild.id), channel) 43 | await ctx.send("Done!") 44 | 45 | @commands.command(aliases=["p"]) 46 | async def play(self, ctx, *, query: str): 47 | """ Searches and plays a song from a given query. """ 48 | player = self.bot.lavalink.players.get(ctx.guild.id) 49 | 50 | query = query.strip("<>") 51 | 52 | if not url_rx.match(query): 53 | query = f"ytsearch:{query}" 54 | 55 | results = await player.node.get_tracks(query) 56 | 57 | if not results or not results["tracks"]: 58 | return await ctx.send("Nothing found!") 59 | 60 | embed = discord.Embed(color=discord.Color.blurple()) 61 | 62 | if results["loadType"] == "PLAYLIST_LOADED": 63 | tracks = results["tracks"] 64 | 65 | for track in tracks: 66 | player.add(requester=ctx.author.id, track=track) 67 | 68 | embed.title = "Playlist Enqueued!" 69 | embed.description = ( 70 | f'{results["playlistInfo"]["name"]} - {len(tracks)} tracks' 71 | ) 72 | else: 73 | track = results["tracks"][0] 74 | embed.title = "Track Enqueued" 75 | embed.description = f'[{track["info"]["title"]}]({track["info"]["uri"]})' 76 | player.add(requester=ctx.author.id, track=track) 77 | 78 | await ctx.send(embed=embed) 79 | 80 | if not player.is_playing: 81 | await player.play() 82 | 83 | @commands.command() 84 | async def seek(self, ctx, *, seconds: int): 85 | """ Seeks to a given position in a track. """ 86 | player = self.bot.lavalink.players.get(ctx.guild.id) 87 | 88 | track_time = player.position + (seconds * 1000) 89 | await player.seek(track_time) 90 | 91 | await ctx.send(f"Moved track to **{lavalink.utils.format_time(track_time)}**") 92 | 93 | @commands.command(aliases=["forceskip"]) 94 | async def skip(self, ctx): 95 | """ Skips the current track. """ 96 | player = self.bot.lavalink.players.get(ctx.guild.id) 97 | 98 | if not player.is_playing: 99 | return await ctx.send("Not playing.") 100 | 101 | await player.skip() 102 | await ctx.send("⏭ | Skipped.") 103 | 104 | @commands.command() 105 | async def stop(self, ctx): 106 | """ Stops the player and clears its queue. """ 107 | player = self.bot.lavalink.players.get(ctx.guild.id) 108 | 109 | if not player.is_playing: 110 | return await ctx.send("Not playing.") 111 | 112 | player.queue.clear() 113 | await player.stop() 114 | await ctx.send("⏹ | Stopped.") 115 | 116 | @commands.command(aliases=["np", "n", "playing"]) 117 | async def now(self, ctx): 118 | """ Shows some stats about the currently playing song. """ 119 | player = self.bot.lavalink.players.get(ctx.guild.id) 120 | 121 | if not player.current: 122 | return await ctx.send("Nothing playing.") 123 | 124 | position = lavalink.utils.format_time(player.position) 125 | if player.current.stream: 126 | duration = "🔴 LIVE" 127 | else: 128 | duration = lavalink.utils.format_time(player.current.duration) 129 | song = f"**[{player.current.title}]({player.current.uri})**\n({position}/{duration})" 130 | 131 | embed = discord.Embed( 132 | color=discord.Color.blurple(), title="Now Playing", description=song 133 | ) 134 | await ctx.send(embed=embed) 135 | 136 | @commands.command(aliases=["q"]) 137 | async def queue(self, ctx, page: int = 1): 138 | """ Shows the player's queue. """ 139 | player = self.bot.lavalink.players.get(ctx.guild.id) 140 | 141 | if not player.queue: 142 | return await ctx.send("Nothing queued.") 143 | 144 | items_per_page = 10 145 | pages = math.ceil(len(player.queue) / items_per_page) 146 | 147 | start = (page - 1) * items_per_page 148 | end = start + items_per_page 149 | 150 | queue_list = "" 151 | for index, track in enumerate(player.queue[start:end], start=start): 152 | queue_list += f"`{index + 1}.` [**{track.title}**]({track.uri})\n" 153 | 154 | embed = discord.Embed( 155 | colour=discord.Color.blurple(), 156 | description=f"**{len(player.queue)} tracks**\n\n{queue_list}", 157 | ) 158 | embed.set_footer(text=f"Viewing page {page}/{pages}") 159 | await ctx.send(embed=embed) 160 | 161 | @commands.command(aliases=["resume"]) 162 | async def pause(self, ctx): 163 | """ Pauses/Resumes the current track. """ 164 | player = self.bot.lavalink.players.get(ctx.guild.id) 165 | 166 | if not player.is_playing: 167 | return await ctx.send("Not playing.") 168 | 169 | if player.paused: 170 | await player.set_pause(False) 171 | await ctx.send("⏯ | Resumed") 172 | else: 173 | await player.set_pause(True) 174 | await ctx.send("⏯ | Paused") 175 | 176 | @commands.command(aliases=["vol"]) 177 | async def volume(self, ctx, volume: int = None): 178 | """ Changes the player's volume (0-1000). """ 179 | player = self.bot.lavalink.players.get(ctx.guild.id) 180 | 181 | if not volume: 182 | return await ctx.send(f"🔈 | {player.volume}%") 183 | 184 | await player.set_volume( 185 | volume 186 | ) # Lavalink will automatically cap values between, or equal to 0-1000. 187 | await ctx.send(f"🔈 | Set to {player.volume}%") 188 | 189 | @commands.command() 190 | async def shuffle(self, ctx): 191 | """ Shuffles the player's queue. """ 192 | player = self.bot.lavalink.players.get(ctx.guild.id) 193 | if not player.is_playing: 194 | return await ctx.send("Nothing playing.") 195 | 196 | player.shuffle = not player.shuffle 197 | await ctx.send("🔀 | Shuffle " + ("enabled" if player.shuffle else "disabled")) 198 | 199 | @commands.command(aliases=["loop"]) 200 | async def repeat(self, ctx): 201 | """ Repeats the current song until the command is invoked again. """ 202 | player = self.bot.lavalink.players.get(ctx.guild.id) 203 | 204 | if not player.is_playing: 205 | return await ctx.send("Nothing playing.") 206 | 207 | player.repeat = not player.repeat 208 | await ctx.send("🔁 | Repeat " + ("enabled" if player.repeat else "disabled")) 209 | 210 | @commands.command() 211 | async def remove(self, ctx, index: int): 212 | """ Removes an item from the player's queue with the given index. """ 213 | player = self.bot.lavalink.players.get(ctx.guild.id) 214 | 215 | if not player.queue: 216 | return await ctx.send("Nothing queued.") 217 | 218 | if index > len(player.queue) or index < 1: 219 | return await ctx.send( 220 | f"Index has to be **between** 1 and {len(player.queue)}" 221 | ) 222 | 223 | removed = player.queue.pop(index - 1) # Account for 0-index. 224 | 225 | await ctx.send(f"Removed **{removed.title}** from the queue.") 226 | 227 | @commands.command() 228 | async def find(self, ctx, *, query): 229 | """ Lists the first 10 search results from a given query. """ 230 | player = self.bot.lavalink.players.get(ctx.guild.id) 231 | 232 | if not query.startswith("ytsearch:") and not query.startswith("scsearch:"): 233 | query = "ytsearch:" + query 234 | 235 | results = await player.node.get_tracks(query) 236 | 237 | if not results or not results["tracks"]: 238 | return await ctx.send("Nothing found.") 239 | 240 | tracks = results["tracks"][:10] # First 10 results 241 | 242 | o = "" 243 | for index, track in enumerate(tracks, start=1): 244 | track_title = track["info"]["title"] 245 | track_uri = track["info"]["uri"] 246 | o += f"`{index}.` [{track_title}]({track_uri})\n" 247 | 248 | embed = discord.Embed(color=discord.Color.blurple(), description=o) 249 | await ctx.send(embed=embed) 250 | 251 | @commands.command(aliases=["dc"]) 252 | async def disconnect(self, ctx): 253 | """ Disconnects the player from the voice channel and clears its queue. """ 254 | player = self.bot.lavalink.players.get(ctx.guild.id) 255 | 256 | if not player.is_connected: 257 | return await ctx.send("Not connected.") 258 | 259 | if not ctx.author.voice or ( 260 | player.is_connected 261 | and ctx.author.voice.channel.id != int(player.channel_id) 262 | ): 263 | return await ctx.send("You're not in my voicechannel!") 264 | 265 | player.queue.clear() 266 | await player.stop() 267 | await self.join(ctx.guild.id, None) 268 | await ctx.send("*⃣ | Disconnected.") 269 | 270 | async def ensure_voice(self, ctx): 271 | """ This check ensures that the bot and command author are in the same voicechannel. """ 272 | player = self.bot.lavalink.players.create( 273 | ctx.guild.id, endpoint=str(ctx.guild.region) 274 | ) 275 | # Create returns a player if one exists, otherwise creates. 276 | 277 | should_connect = ctx.command.name in ( 278 | "play" 279 | ) # Add commands that require joining voice to work. 280 | 281 | if not ctx.author.voice or not ctx.author.voice.channel: 282 | raise commands.CommandInvokeError("Join a voicechannel first.") 283 | 284 | if not player.is_connected: 285 | if not should_connect: 286 | raise commands.CommandInvokeError("Not connected.") 287 | 288 | permissions = ctx.author.voice.channel.permissions_for(ctx.me) 289 | 290 | if ( 291 | not permissions.connect or not permissions.speak 292 | ): # Check user limit too? 293 | raise commands.CommandInvokeError( 294 | "I need the `CONNECT` and `SPEAK` permissions." 295 | ) 296 | 297 | player.store("channel", ctx.channel.id) 298 | await self.join(ctx=ctx, channel=str(ctx.author.voice.channel.id)) 299 | else: 300 | if int(player.channel_id) != ctx.author.voice.channel.id: 301 | raise commands.CommandInvokeError("You need to be in my voicechannel.") 302 | 303 | 304 | def setup(bot): 305 | bot.add_cog(MusicPlugin(bot)) 306 | -------------------------------------------------------------------------------- /music/requirements.txt: -------------------------------------------------------------------------------- 1 | lavalink -------------------------------------------------------------------------------- /plugins.json: -------------------------------------------------------------------------------- 1 | { 2 | "allowed":[ 3 | "tags", 4 | "announcement", 5 | "dmonjoin", 6 | "hastebin", 7 | "leaveserver", 8 | "translator", 9 | "reacttocontact", 10 | "moderation", 11 | "backupdb", 12 | "fix", 13 | "giveaway" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /poll/poll.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015 Rapptz 2 | from discord.ext import commands 3 | import discord 4 | import asyncio 5 | import datetime 6 | 7 | from core import checks 8 | from core.models import PermissionLevel 9 | 10 | 11 | def to_emoji(c): 12 | base = 0x1F1E6 13 | return chr(base + c) 14 | 15 | 16 | class Polls(commands.Cog): 17 | """Poll voting system.""" 18 | 19 | def __init__(self, bot): 20 | self.bot = bot 21 | 22 | @commands.group(name="poll", invoke_without_command=True) 23 | @checks.has_permissions(PermissionLevel.MODERATOR) 24 | async def poll(self, ctx: commands.Context): 25 | """Easily create Polls.""" 26 | await ctx.send_help(ctx.command) 27 | 28 | @poll.command() 29 | @commands.guild_only() 30 | @checks.has_permissions(PermissionLevel.MODERATOR) 31 | async def start(self, ctx, *, question): 32 | """Interactively creates a poll with the following question. 33 | 34 | To vote, use reactions! 35 | """ 36 | perms = ctx.channel.permissions_for(ctx.me) 37 | if not perms.add_reactions: 38 | return await ctx.send("Need Add Reactions permissions.") 39 | 40 | # a list of messages to delete when we're all done 41 | messages = [ctx.message] 42 | answers = [] 43 | 44 | def check(m): 45 | return ( 46 | m.author == ctx.author 47 | and m.channel == ctx.channel 48 | and len(m.content) <= 100 49 | ) 50 | 51 | for i in range(20): 52 | messages.append( 53 | await ctx.send( 54 | f"Say a Poll option or {ctx.prefix}done to publish the Poll." 55 | ) 56 | ) 57 | 58 | try: 59 | entry = await self.bot.wait_for("message", check=check, timeout=60.0) 60 | except asyncio.TimeoutError: 61 | break 62 | 63 | messages.append(entry) 64 | 65 | if entry.clean_content.startswith(f"{ctx.prefix}done"): 66 | break 67 | 68 | answers.append((to_emoji(i), entry.clean_content)) 69 | 70 | try: 71 | await ctx.channel.delete_messages(messages) 72 | except: 73 | pass # oh well 74 | 75 | answer = "\n".join(f"{keycap}: {content}" for keycap, content in answers) 76 | embed = discord.Embed( 77 | color=self.bot.main_color, 78 | timestamp=datetime.datetime.utcnow(), 79 | description=f"**{question}**\n{answer}", 80 | ) 81 | embed.set_author(name=ctx.author, icon_url=ctx.author.avatar_url) 82 | poll = await ctx.send(embed=embed) 83 | for emoji, _ in answers: 84 | await poll.add_reaction(emoji) 85 | 86 | @start.error 87 | async def poll_error(self, ctx, error): 88 | if isinstance(error, commands.MissingRequiredArgument): 89 | return await ctx.send("Missing the question.") 90 | 91 | @poll.command() 92 | @commands.guild_only() 93 | @checks.has_permissions(PermissionLevel.MODERATOR) 94 | async def quick(self, ctx, *questions_and_choices: str): 95 | """Makes a poll quickly. 96 | The first argument is the question and the rest are the choices. 97 | for example: `?poll quick "Green or Light Green?" Green "Light Green"` 98 | 99 | or it can be a simple yes or no poll, like: 100 | `?poll quick "Do you watch Anime?"` 101 | """ 102 | 103 | if len(questions_and_choices) == 0: 104 | return await ctx.send("You need to specify a question.") 105 | elif len(questions_and_choices) == 2: 106 | return await ctx.send("You need at least 2 choices.") 107 | elif len(questions_and_choices) > 21: 108 | return await ctx.send("You can only have up to 20 choices.") 109 | 110 | perms = ctx.channel.permissions_for(ctx.me) 111 | if not perms.add_reactions: 112 | return await ctx.send("Need Add Reactions permissions.") 113 | try: 114 | await ctx.message.delete() 115 | except: 116 | pass 117 | question = questions_and_choices[0] 118 | 119 | if len(questions_and_choices) == 1: 120 | embed = discord.Embed( 121 | color=self.bot.main_color, description=f"**{question}**" 122 | ) 123 | embed.set_author(name=ctx.author, icon_url=ctx.author.avatar_url) 124 | poll = await ctx.send(embed=embed) 125 | reactions = ["👍", "👎"] 126 | for emoji in reactions: 127 | await poll.add_reaction(emoji) 128 | 129 | else: 130 | choices = [ 131 | (to_emoji(e), v) for e, v in enumerate(questions_and_choices[1:]) 132 | ] 133 | 134 | body = "\n".join(f"{key}: {c}" for key, c in choices) 135 | embed = discord.Embed( 136 | color=self.bot.main_color, 137 | timestamp=datetime.datetime.utcnow(), 138 | description=f"**{question}**\n{body}", 139 | ) 140 | embed.set_author(name=ctx.author, icon_url=ctx.author.avatar_url) 141 | poll = await ctx.send(embed=embed) 142 | for emoji, _ in choices: 143 | await poll.add_reaction(emoji) 144 | 145 | 146 | def setup(bot): 147 | bot.add_cog(Polls(bot)) 148 | -------------------------------------------------------------------------------- /private/private.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import io 3 | import json 4 | import os 5 | import shutil 6 | import sys 7 | import typing 8 | import zipfile 9 | from importlib import invalidate_caches 10 | from difflib import get_close_matches 11 | from pathlib import Path, PurePath 12 | from re import match 13 | from site import USER_SITE 14 | from subprocess import PIPE 15 | 16 | import discord 17 | from discord.ext import commands 18 | 19 | from pkg_resources import parse_version 20 | 21 | from core import checks 22 | from core.models import PermissionLevel, getLogger 23 | from core.paginator import EmbedPaginatorSession 24 | from core.utils import truncate, trigger_typing 25 | 26 | logger = getLogger(__name__) 27 | 28 | 29 | class InvalidPluginError(commands.BadArgument): 30 | pass 31 | 32 | 33 | class Plugin: 34 | def __init__(self, user, repo, name, branch=None): 35 | self.user = user 36 | self.repo = repo 37 | self.name = name 38 | self.branch = branch if branch is not None else "master" 39 | self.url = f"https://github.com/{user}/{repo}/archive/{self.branch}.zip" 40 | self.link = f"https://github.com/{user}/{repo}/tree/{self.branch}/{name}" 41 | 42 | @property 43 | def path(self): 44 | return ( 45 | PurePath("plugins") / self.user / self.repo / f"{self.name}-{self.branch}" 46 | ) 47 | 48 | @property 49 | def abs_path(self): 50 | return Path(__file__).absolute().parent.parent / self.path 51 | 52 | @property 53 | def cache_path(self): 54 | return ( 55 | Path(__file__).absolute().parent.parent 56 | / "temp" 57 | / "plugins-cache" 58 | / f"{self.user}-{self.repo}-{self.branch}.zip" 59 | ) 60 | 61 | @property 62 | def ext_string(self): 63 | return f"plugins.{self.user}.{self.repo}.{self.name}-{self.branch}.{self.name}" 64 | 65 | def __str__(self): 66 | return f"{self.user}/{self.repo}/{self.name}@{self.branch}" 67 | 68 | def __lt__(self, other): 69 | return self.name.lower() < other.name.lower() 70 | 71 | @classmethod 72 | def from_string(cls, s, strict=False): 73 | if not strict: 74 | m = match(r"^(.+?)/(.+?)/(.+?)(?:@(.+?))?$", s) 75 | else: 76 | m = match(r"^(.+?)/(.+?)/(.+?)@(.+?)$", s) 77 | if m is not None: 78 | return Plugin(*m.groups()) 79 | raise InvalidPluginError( 80 | "Cannot decipher %s.", s 81 | ) # pylint: disable=raising-format-tuple 82 | 83 | def __hash__(self): 84 | return hash((self.user, self.repo, self.name, self.branch)) 85 | 86 | def __repr__(self): 87 | return f"" 88 | 89 | def __eq__(self, other): 90 | return isinstance(other, Plugin) and self.__str__() == other.__str__() 91 | 92 | 93 | class PrivatePlugins(commands.Cog): 94 | """ 95 | Plugins expand Modmail functionality by allowing third-party addons. 96 | 97 | These addons could have a range of features from moderation to simply 98 | making your life as a moderator easier! 99 | Learn how to create a plugin yourself here: 100 | https://github.com/kyb3r/modmail/wiki/Plugins 101 | """ 102 | 103 | def __init__(self, bot): 104 | self.bot = bot 105 | self.registry = {} 106 | self.loaded_plugins = set() 107 | self._ready_event = asyncio.Event() 108 | 109 | self.bot.loop.create_task(self.populate_registry()) 110 | 111 | if self.bot.config.get("enable_plugins"): 112 | self.bot.loop.create_task(self.initial_load_plugins()) 113 | else: 114 | logger.info("Plugins not loaded since ENABLE_PLUGINS=false.") 115 | 116 | async def populate_registry(self): 117 | url = "https://raw.githubusercontent.com/kyb3r/modmail/master/plugins/registry.json" 118 | async with self.bot.session.get(url) as resp: 119 | self.registry = json.loads(await resp.text()) 120 | 121 | async def initial_load_plugins(self): 122 | await self.bot.wait_for_connected() 123 | 124 | for plugin_name in list(self.bot.config["plugins"]): 125 | try: 126 | plugin = Plugin.from_string(plugin_name, strict=True) 127 | except InvalidPluginError: 128 | self.bot.config["plugins"].remove(plugin_name) 129 | try: 130 | # For backwards compat 131 | plugin = Plugin.from_string(plugin_name) 132 | except InvalidPluginError: 133 | logger.error( 134 | "Failed to parse plugin name: %s.", plugin_name, exc_info=True 135 | ) 136 | continue 137 | 138 | logger.info( 139 | "Migrated legacy plugin name: %s, now %s.", plugin_name, str(plugin) 140 | ) 141 | self.bot.config["plugins"].append(str(plugin)) 142 | 143 | try: 144 | await self.download_plugin(plugin) 145 | await self.load_plugin(f"../../../../{plugin}") 146 | except Exception as e: 147 | if isinstance(e, commands.errors.ExtensionAlreadyLoaded): 148 | continue 149 | # logger.error("Error when loading plugin %s.", plugin, exc_info=True) 150 | continue 151 | 152 | logger.debug("Finished loading all plugins.") 153 | self._ready_event.set() 154 | await self.bot.config.update() 155 | 156 | async def download_plugin(self, plugin, force=False): 157 | if plugin.abs_path.exists() and not force: 158 | return 159 | 160 | plugin.abs_path.mkdir(parents=True, exist_ok=True) 161 | 162 | if plugin.cache_path.exists() and not force: 163 | plugin_io = plugin.cache_path.open("rb") 164 | logger.debug("Loading cached %s.", plugin.cache_path) 165 | 166 | else: 167 | headers = {} 168 | if os.path.exists("./config.json"): 169 | with open("./config.json") as f: 170 | jd = json.load(f) 171 | try: 172 | GITHUB_TOKEN = jd["GITHUB_TOKEN"] 173 | if GITHUB_TOKEN is not None: 174 | headers["Authorization"] = f"token {GITHUB_TOKEN}" 175 | except KeyError: 176 | GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") 177 | if GITHUB_TOKEN is not None: 178 | headers["Authorization"] = f"token {GITHUB_TOKEN}" 179 | else: 180 | GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") 181 | if GITHUB_TOKEN is not None: 182 | headers["Authorization"] = f"token {GITHUB_TOKEN}" 183 | async with self.bot.session.get(plugin.url, headers=headers) as resp: 184 | logger.debug("Downloading %s.", plugin.url) 185 | raw = await resp.read() 186 | plugin_io = io.BytesIO(raw) 187 | if not plugin.cache_path.parent.exists(): 188 | plugin.cache_path.parent.mkdir(parents=True) 189 | 190 | with plugin.cache_path.open("wb") as f: 191 | f.write(raw) 192 | 193 | with zipfile.ZipFile(plugin_io) as zipf: 194 | for info in zipf.infolist(): 195 | path = PurePath(info.filename) 196 | print(path) 197 | if len(path.parts) >= 3 and path.parts[1] == plugin.name: 198 | plugin_path = plugin.abs_path / Path(*path.parts[2:]) 199 | if info.is_dir(): 200 | plugin_path.mkdir(parents=True, exist_ok=True) 201 | else: 202 | plugin_path.parent.mkdir(parents=True, exist_ok=True) 203 | with zipf.open(info) as src, plugin_path.open("wb") as dst: 204 | shutil.copyfileobj(src, dst) 205 | 206 | plugin_io.close() 207 | 208 | async def load_plugin(self, plugin): 209 | print(plugin.abs_path) 210 | if not (plugin.abs_path / f"{plugin.name}.py").exists(): 211 | raise InvalidPluginError(f"{plugin.name}.py not found.") 212 | 213 | req_txt = plugin.abs_path / "requirements.txt" 214 | 215 | if req_txt.exists(): 216 | # Install PIP requirements 217 | 218 | venv = hasattr(sys, "real_prefix") # in a virtual env 219 | user_install = " --user" if not venv else "" 220 | proc = await asyncio.create_subprocess_shell( 221 | f"{sys.executable} -m pip install --upgrade{user_install} -r {req_txt} -q -q", 222 | stderr=PIPE, 223 | stdout=PIPE, 224 | ) 225 | 226 | logger.debug("Downloading requirements for %s.", plugin.ext_string) 227 | 228 | stdout, stderr = await proc.communicate() 229 | 230 | if stdout: 231 | logger.debug("[stdout]\n%s.", stdout.decode()) 232 | 233 | if stderr: 234 | logger.debug("[stderr]\n%s.", stderr.decode()) 235 | logger.error( 236 | "Failed to download requirements for %s.", 237 | plugin.ext_string, 238 | exc_info=True, 239 | ) 240 | raise InvalidPluginError( 241 | f"Unable to download requirements: ```\n{stderr.decode()}\n```" 242 | ) 243 | 244 | if os.path.exists(USER_SITE): 245 | sys.path.insert(0, USER_SITE) 246 | 247 | try: 248 | print(plugin.ext_string) 249 | self.bot.load_extension(plugin.ext_string) 250 | logger.info("Loaded plugin: %s", plugin.ext_string.split(".")[-1]) 251 | self.loaded_plugins.add(plugin) 252 | 253 | except commands.errors.ExtensionAlreadyLoaded: 254 | pass 255 | except commands.ExtensionError as exc: 256 | logger.error("Plugin load failure: %s", plugin.ext_string, exc_info=True) 257 | raise InvalidPluginError("Cannot load extension, plugin invalid.") from exc 258 | 259 | async def parse_user_input(self, ctx, plugin_name, check_version=False): 260 | 261 | if not self._ready_event.is_set(): 262 | embed = discord.Embed( 263 | description="Plugins are still loading, please try again later.", 264 | color=self.bot.main_color, 265 | ) 266 | await ctx.send(embed=embed) 267 | return 268 | 269 | if plugin_name in self.registry: 270 | details = self.registry[plugin_name] 271 | user, repo = details["repository"].split("/", maxsplit=1) 272 | branch = details.get("branch") 273 | 274 | if check_version: 275 | required_version = details.get("bot_version", False) 276 | 277 | if required_version and self.bot.version < parse_version( 278 | required_version 279 | ): 280 | embed = discord.Embed( 281 | description="Your bot's version is too low. " 282 | f"This plugin requires version `{required_version}`.", 283 | color=self.bot.error_color, 284 | ) 285 | await ctx.send(embed=embed) 286 | return 287 | 288 | plugin = Plugin(user, repo, plugin_name, branch) 289 | 290 | else: 291 | try: 292 | plugin = Plugin.from_string(plugin_name) 293 | except InvalidPluginError: 294 | embed = discord.Embed( 295 | description="Invalid plugin name, double check the plugin name " 296 | "or use one of the following formats: " 297 | "username/repo/plugin, username/repo/plugin@branch.", 298 | color=self.bot.error_color, 299 | ) 300 | await ctx.send(embed=embed) 301 | return 302 | return plugin 303 | 304 | @commands.group(aliases=["plugin"], invoke_without_command=True) 305 | @checks.has_permissions(PermissionLevel.OWNER) 306 | async def plugins(self, ctx): 307 | """ 308 | Manage plugins for Modmail. 309 | """ 310 | 311 | await ctx.send_help(ctx.command) 312 | 313 | @plugins.command(name="add", aliases=["install", "load"]) 314 | @checks.has_permissions(PermissionLevel.OWNER) 315 | @trigger_typing 316 | async def plugins_add(self, ctx, *, plugin_name: str): 317 | """ 318 | Install a new plugin for the bot. 319 | 320 | `plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, 321 | or a direct reference to a GitHub hosted plugin (in the format `user/repo/name[@branch]`). 322 | """ 323 | 324 | plugin = await self.parse_user_input(ctx, plugin_name, check_version=True) 325 | if plugin is None: 326 | return 327 | 328 | if str(plugin) in self.bot.config["plugins"]: 329 | embed = discord.Embed( 330 | description="This plugin is already installed.", 331 | color=self.bot.error_color, 332 | ) 333 | return await ctx.send(embed=embed) 334 | 335 | if plugin.name in self.bot.cogs: 336 | # another class with the same name 337 | embed = discord.Embed( 338 | description="Cannot install this plugin (dupe cog name).", 339 | color=self.bot.error_color, 340 | ) 341 | return await ctx.send(embed=embed) 342 | 343 | embed = discord.Embed( 344 | description=f"Starting to download plugin from {plugin.link}...", 345 | color=self.bot.main_color, 346 | ) 347 | msg = await ctx.send(embed=embed) 348 | 349 | try: 350 | await self.download_plugin(plugin, force=True) 351 | except Exception: 352 | logger.warning("Unable to download plugin %s.", plugin, exc_info=True) 353 | 354 | embed = discord.Embed( 355 | description="Failed to download plugin, check logs for error.", 356 | color=self.bot.error_color, 357 | ) 358 | 359 | return await msg.edit(embed=embed) 360 | 361 | self.bot.config["plugins"].append(str(plugin)) 362 | await self.bot.config.update() 363 | 364 | if self.bot.config.get("enable_plugins"): 365 | 366 | invalidate_caches() 367 | 368 | try: 369 | await self.load_plugin(plugin) 370 | except Exception: 371 | logger.warning("Unable to load plugin %s.", plugin, exc_info=True) 372 | 373 | embed = discord.Embed( 374 | description="Failed to download plugin, check logs for error.", 375 | color=self.bot.error_color, 376 | ) 377 | 378 | else: 379 | embed = discord.Embed( 380 | description="Successfully installed plugin.\n" 381 | "*Friendly reminder, plugins have absolute control over your bot. " 382 | "Please only install plugins from developers you trust.*", 383 | color=self.bot.main_color, 384 | ) 385 | else: 386 | embed = discord.Embed( 387 | description="Successfully installed plugin.\n" 388 | "*Friendly reminder, plugins have absolute control over your bot. " 389 | "Please only install plugins from developers you trust.*\n\n" 390 | "This plugin is currently not enabled due to `ENABLE_PLUGINS=false`, " 391 | "to re-enable plugins, remove or change `ENABLE_PLUGINS=true` and restart your bot.", 392 | color=self.bot.main_color, 393 | ) 394 | return await msg.edit(embed=embed) 395 | 396 | @plugins.command(name="remove", aliases=["del", "delete"]) 397 | @checks.has_permissions(PermissionLevel.OWNER) 398 | async def plugins_remove(self, ctx, *, plugin_name: str): 399 | """ 400 | Remove an installed plugin of the bot. 401 | 402 | `plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference 403 | to a GitHub hosted plugin (in the format `user/repo/name[@branch]`). 404 | """ 405 | plugin = await self.parse_user_input(ctx, plugin_name) 406 | if plugin is None: 407 | return 408 | 409 | if str(plugin) not in self.bot.config["plugins"]: 410 | embed = discord.Embed( 411 | description="Plugin is not installed.", color=self.bot.error_color 412 | ) 413 | return await ctx.send(embed=embed) 414 | 415 | if self.bot.config.get("enable_plugins"): 416 | try: 417 | self.bot.unload_extension(plugin.ext_string) 418 | self.loaded_plugins.remove(plugin) 419 | except (commands.ExtensionNotLoaded, KeyError): 420 | logger.warning("Plugin was never loaded.") 421 | 422 | self.bot.config["plugins"].remove(str(plugin)) 423 | await self.bot.config.update() 424 | shutil.rmtree( 425 | plugin.abs_path, 426 | onerror=lambda *args: logger.warning( 427 | "Failed to remove plugin files %s: %s", plugin, str(args[2]) 428 | ), 429 | ) 430 | try: 431 | plugin.abs_path.parent.rmdir() 432 | plugin.abs_path.parent.parent.rmdir() 433 | except OSError: 434 | pass # dir not empty 435 | 436 | embed = discord.Embed( 437 | description="The plugin is successfully uninstalled.", 438 | color=self.bot.main_color, 439 | ) 440 | await ctx.send(embed=embed) 441 | 442 | async def update_plugin(self, ctx, plugin_name): 443 | logger.debug("Updating %s.", plugin_name) 444 | plugin = await self.parse_user_input(ctx, plugin_name, check_version=True) 445 | if plugin is None: 446 | return 447 | 448 | if str(plugin) not in self.bot.config["plugins"]: 449 | embed = discord.Embed( 450 | description="Plugin is not installed.", color=self.bot.error_color 451 | ) 452 | return await ctx.send(embed=embed) 453 | 454 | async with ctx.typing(): 455 | await self.download_plugin(plugin, force=True) 456 | if self.bot.config.get("enable_plugins"): 457 | try: 458 | self.bot.unload_extension(plugin.ext_string) 459 | except commands.ExtensionError: 460 | logger.warning("Plugin unload fail.", exc_info=True) 461 | await self.load_plugin(plugin) 462 | logger.debug("Updated %s.", plugin_name) 463 | embed = discord.Embed( 464 | description=f"Successfully updated {plugin.name}.", 465 | color=self.bot.main_color, 466 | ) 467 | return await ctx.send(embed=embed) 468 | 469 | @plugins.command(name="update") 470 | @checks.has_permissions(PermissionLevel.OWNER) 471 | async def plugins_update(self, ctx, *, plugin_name: str = None): 472 | """ 473 | Update a plugin for the bot. 474 | 475 | `plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference 476 | to a GitHub hosted plugin (in the format `user/repo/name[@branch]`). 477 | 478 | To update all plugins, do `{prefix}plugins update`. 479 | """ 480 | 481 | if plugin_name is None: 482 | # pylint: disable=redefined-argument-from-local 483 | for plugin_name in self.bot.config["plugins"]: 484 | await self.update_plugin(ctx, plugin_name) 485 | else: 486 | await self.update_plugin(ctx, plugin_name) 487 | 488 | @plugins.command(name="loaded", aliases=["enabled", "installed"]) 489 | @checks.has_permissions(PermissionLevel.OWNER) 490 | async def plugins_loaded(self, ctx): 491 | """ 492 | Show a list of currently loaded plugins. 493 | """ 494 | 495 | if not self.bot.config.get("enable_plugins"): 496 | embed = discord.Embed( 497 | description="No plugins are loaded due to `ENABLE_PLUGINS=false`, " 498 | "to re-enable plugins, remove or set `ENABLE_PLUGINS=true` and restart your bot.", 499 | color=self.bot.error_color, 500 | ) 501 | return await ctx.send(embed=embed) 502 | 503 | if not self._ready_event.is_set(): 504 | embed = discord.Embed( 505 | description="Plugins are still loading, please try again later.", 506 | color=self.bot.main_color, 507 | ) 508 | return await ctx.send(embed=embed) 509 | 510 | if not self.loaded_plugins: 511 | embed = discord.Embed( 512 | description="There are no plugins currently loaded.", 513 | color=self.bot.error_color, 514 | ) 515 | return await ctx.send(embed=embed) 516 | 517 | loaded_plugins = map(str, sorted(self.loaded_plugins)) 518 | pages = ["```\n"] 519 | for plugin in loaded_plugins: 520 | msg = str(plugin) + "\n" 521 | if len(msg) + len(pages[-1]) + 3 <= 2048: 522 | pages[-1] += msg 523 | else: 524 | pages[-1] += "```" 525 | pages.append(f"```\n{msg}") 526 | 527 | if pages[-1][-3:] != "```": 528 | pages[-1] += "```" 529 | 530 | embeds = [] 531 | for page in pages: 532 | embed = discord.Embed( 533 | title="Loaded plugins:", description=page, color=self.bot.main_color 534 | ) 535 | embeds.append(embed) 536 | paginator = EmbedPaginatorSession(ctx, *embeds) 537 | await paginator.run() 538 | 539 | @plugins.group( 540 | invoke_without_command=True, name="registry", aliases=["list", "info"] 541 | ) 542 | @checks.has_permissions(PermissionLevel.OWNER) 543 | async def plugins_registry( 544 | self, ctx, *, plugin_name: typing.Union[int, str] = None 545 | ): 546 | """ 547 | Shows a list of all approved plugins. 548 | 549 | Usage: 550 | `{prefix}plugin registry` Details about all plugins. 551 | `{prefix}plugin registry plugin-name` Details about the indicated plugin. 552 | `{prefix}plugin registry page-number` Jump to a page in the registry. 553 | """ 554 | 555 | await self.populate_registry() 556 | 557 | embeds = [] 558 | 559 | registry = sorted(self.registry.items(), key=lambda elem: elem[0]) 560 | 561 | if isinstance(plugin_name, int): 562 | index = plugin_name - 1 563 | if index < 0: 564 | index = 0 565 | if index >= len(registry): 566 | index = len(registry) - 1 567 | else: 568 | index = next( 569 | (i for i, (n, _) in enumerate(registry) if plugin_name == n), 0 570 | ) 571 | 572 | if not index and plugin_name is not None: 573 | embed = discord.Embed( 574 | color=self.bot.error_color, 575 | description=f'Could not find a plugin with name "{plugin_name}" within the registry.', 576 | ) 577 | 578 | matches = get_close_matches(plugin_name, self.registry.keys()) 579 | 580 | if matches: 581 | embed.add_field( 582 | name="Perhaps you meant:", 583 | value="\n".join(f"`{m}`" for m in matches), 584 | ) 585 | 586 | return await ctx.send(embed=embed) 587 | 588 | for name, details in registry: 589 | details = self.registry[name] 590 | user, repo = details["repository"].split("/", maxsplit=1) 591 | branch = details.get("branch") 592 | 593 | plugin = Plugin(user, repo, name, branch) 594 | 595 | embed = discord.Embed( 596 | color=self.bot.main_color, 597 | description=details["description"], 598 | url=plugin.link, 599 | title=details["repository"], 600 | ) 601 | 602 | embed.add_field( 603 | name="Installation", value=f"```{self.bot.prefix}plugins add {name}```" 604 | ) 605 | 606 | embed.set_author( 607 | name=details["title"], icon_url=details.get("icon_url"), url=plugin.link 608 | ) 609 | 610 | if details.get("thumbnail_url"): 611 | embed.set_thumbnail(url=details.get("thumbnail_url")) 612 | 613 | if details.get("image_url"): 614 | embed.set_image(url=details.get("image_url")) 615 | 616 | if plugin in self.loaded_plugins: 617 | embed.set_footer(text="This plugin is currently loaded.") 618 | else: 619 | required_version = details.get("bot_version", False) 620 | if required_version and self.bot.version < parse_version( 621 | required_version 622 | ): 623 | embed.set_footer( 624 | text="Your bot is unable to install this plugin, " 625 | f"minimum required version is v{required_version}." 626 | ) 627 | else: 628 | embed.set_footer(text="Your bot is able to install this plugin.") 629 | 630 | embeds.append(embed) 631 | 632 | paginator = EmbedPaginatorSession(ctx, *embeds) 633 | paginator.current = index 634 | await paginator.run() 635 | 636 | @plugins_registry.command(name="compact", aliases=["slim"]) 637 | @checks.has_permissions(PermissionLevel.OWNER) 638 | async def plugins_registry_compact(self, ctx): 639 | """ 640 | Shows a compact view of all plugins within the registry. 641 | """ 642 | 643 | await self.populate_registry() 644 | 645 | registry = sorted(self.registry.items(), key=lambda elem: elem[0]) 646 | 647 | pages = [""] 648 | 649 | for plugin_name, details in registry: 650 | details = self.registry[plugin_name] 651 | user, repo = details["repository"].split("/", maxsplit=1) 652 | branch = details.get("branch") 653 | 654 | plugin = Plugin(user, repo, plugin_name, branch) 655 | 656 | desc = discord.utils.escape_markdown( 657 | details["description"].replace("\n", "") 658 | ) 659 | 660 | name = f"[`{plugin.name}`]({plugin.link})" 661 | fmt = f"{name} - {desc}" 662 | 663 | if plugin_name in self.loaded_plugins: 664 | limit = 75 - len(plugin_name) - 4 - 8 + len(name) 665 | if limit < 0: 666 | fmt = plugin.name 667 | limit = 75 668 | fmt = truncate(fmt, limit) + "[loaded]\n" 669 | else: 670 | limit = 75 - len(plugin_name) - 4 + len(name) 671 | if limit < 0: 672 | fmt = plugin.name 673 | limit = 75 674 | fmt = truncate(fmt, limit) + "\n" 675 | 676 | if len(fmt) + len(pages[-1]) <= 2048: 677 | pages[-1] += fmt 678 | else: 679 | pages.append(fmt) 680 | 681 | embeds = [] 682 | 683 | for page in pages: 684 | embed = discord.Embed(color=self.bot.main_color, description=page) 685 | embed.set_author(name="Plugin Registry", icon_url=self.bot.user.avatar_url) 686 | embeds.append(embed) 687 | 688 | paginator = EmbedPaginatorSession(ctx, *embeds) 689 | await paginator.run() 690 | 691 | 692 | def setup(bot): 693 | if "Plugins" in bot.cogs: 694 | bot.remove_cog("Plugins") 695 | bot.add_cog(PrivatePlugins(bot)) 696 | -------------------------------------------------------------------------------- /quote/quote.py: -------------------------------------------------------------------------------- 1 | import typing 2 | import discord 3 | from discord.ext import commands 4 | from modmailtranslation import Translator, KeyNotFoundError 5 | 6 | 7 | class QuotePlugin(commands.Cog): 8 | def __init__(self, bot): 9 | self.bot: discord.Client = bot 10 | self.db = bot.plugin_db.get_partition(self) 11 | self.i18n = Translator("") 12 | 13 | @commands.command(aliases=["q"]) 14 | async def quote( 15 | self, 16 | ctx: commands.Context, 17 | channel: typing.Optional[discord.TextChannel], 18 | message_id: str, 19 | ): 20 | if not channel: 21 | channel = ctx.channel 22 | try: 23 | try: 24 | message = await channel.fetch_message(int(message_id)) 25 | except discord.NotFound: 26 | await ctx.send(self.i18n.get("MESSAGE_NOT_FOUND")) 27 | return 28 | except discord.Forbidden: 29 | await ctx.send(self.i18n.get("FORBIDDEN")) 30 | return 31 | except: 32 | await ctx.send(self.i18n.get("ERROR")) 33 | return 34 | except KeyNotFoundError: 35 | await ctx.send( 36 | "Seems Like the command isn't localised for your language yet." 37 | ) 38 | return 39 | -------------------------------------------------------------------------------- /react-to-contact/react-to-contact.py: -------------------------------------------------------------------------------- 1 | import re 2 | import asyncio 3 | import datetime 4 | import discord 5 | from discord.ext import commands 6 | 7 | from core import checks 8 | from core.models import PermissionLevel 9 | 10 | 11 | class ReactToContact(commands.Cog): 12 | """ 13 | Make users start modmail thread by clicking an emoji 14 | """ 15 | 16 | def __init__(self, bot): 17 | self.bot = bot 18 | self.db = bot.plugin_db.get_partition(self) 19 | self.reaction = None 20 | self.channel = None 21 | self.message = None 22 | 23 | @commands.command(aliases=["sr"]) 24 | @commands.guild_only() 25 | @checks.has_permissions(PermissionLevel.ADMIN) 26 | async def setreaction(self, ctx: commands.Context, link: str): 27 | """ 28 | Set the message on which the bot will look reactions on. 29 | Creates an __interactive session__ to use emoji **(Supports Unicode Emoji Too)** 30 | Before using this command, make sure there is a reaction on the message you want the plugin to look at. 31 | 32 | **Usage:** 33 | {prefix}setreaction 34 | """ 35 | 36 | def check(reaction, user): 37 | return user == ctx.message.author 38 | 39 | regex = r"discordapp\.com" 40 | 41 | if bool(re.search(regex, link)) is True: 42 | sl = link.split("/") 43 | msg = sl[-1] 44 | channel = sl[-2] 45 | 46 | await ctx.send( 47 | "React to this message with the emoji." 48 | " `(The reaction should also be on the message or it won't work.)`" 49 | ) 50 | reaction, user = await self.bot.wait_for("reaction_add", check=check) 51 | 52 | await self.db.find_one_and_update( 53 | {"_id": "config"}, 54 | { 55 | "$set": { 56 | "channel": channel, 57 | "message": msg, 58 | "reaction": f"{reaction.emoji.name if isinstance(reaction.emoji, discord.Emoji) else reaction.emoji}", 59 | } 60 | }, 61 | upsert=True, 62 | ) 63 | await ctx.send("Done!") 64 | 65 | else: 66 | await ctx.send("Please give a valid message link") 67 | return 68 | 69 | @commands.Cog.listener() 70 | async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): 71 | if payload.user_id == self.bot.user.id: 72 | return 73 | 74 | user = self.bot.get_user(payload.user_id) 75 | 76 | if user is None or user.bot: 77 | return 78 | 79 | config = await self.db.find_one({"_id": "config"}) 80 | 81 | if config is None: 82 | # print("No Config") 83 | return 84 | 85 | if config["reaction"] is None or (payload.emoji.name != config["reaction"]): 86 | # print("No Reaction") 87 | return 88 | 89 | if config["channel"] is None or (payload.channel_id != int(config["channel"])): 90 | # print("No Channel") 91 | return 92 | 93 | if config["message"] is None or (payload.message_id != int(config["message"])): 94 | # print("No Message") 95 | return 96 | 97 | guild: discord.Guild = discord.utils.find( 98 | lambda g: g.id == payload.guild_id, self.bot.guilds 99 | ) 100 | 101 | member: discord.Member = guild.get_member(payload.user_id) 102 | 103 | channel = guild.get_channel(int(config["channel"])) 104 | 105 | msg: discord.Message = await channel.fetch_message(int(config["message"])) 106 | 107 | await msg.remove_reaction(payload.emoji, member) 108 | 109 | try: 110 | exists = await self.bot.threads.find(recipient=user) 111 | if exists: 112 | return 113 | 114 | thread = await self.bot.threads.create(user) 115 | 116 | if self.bot.config["dm_disabled"] >= 1: 117 | logger.info("Contacting user %s when Modmail DM is disabled.", user) 118 | 119 | embed = discord.Embed( 120 | title="Created Thread", 121 | description=f"Thread started by {user.mention}.", 122 | color=self.bot.main_color, 123 | ) 124 | await thread.wait_until_ready() 125 | await thread.channel.send(embed=embed) 126 | sent_emoji, _ = await self.bot.retrieve_emoji() 127 | await asyncio.sleep(3) 128 | 129 | except (discord.HTTPException, discord.Forbidden): 130 | ch = self.bot.get_channel(int(self.bot.config.get("log_channel_id"))) 131 | 132 | await ch.send( 133 | embed=discord.Embed( 134 | title="User Contact failed", 135 | description=f"**{member.name}#{member.discriminator}** tried contacting, but the bot couldnt dm him/her.", 136 | color=self.bot.main_color, 137 | timestamp=datetime.datetime.utcnow(), 138 | ) 139 | ) 140 | 141 | 142 | def setup(bot): 143 | bot.add_cog(ReactToContact(bot)) 144 | -------------------------------------------------------------------------------- /reboot/reboot.py.txt: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import discord 4 | import logging 5 | from discord.ext import commands 6 | 7 | from core import checks 8 | from core.models import PermissionLevel 9 | 10 | logger = logging.getLogger('Modmail') 11 | 12 | 13 | class RebootCog(commands.Cog): 14 | def __init__(self, bot): 15 | self.bot = bot 16 | 17 | @commands.command() 18 | @checks.has_permissions(PermissionLevel.OWNER) 19 | async def reboot(self, ctx): 20 | """Clears Cached Logs & Reboots The Bot""" 21 | msg = await ctx.send(embed=discord.Embed( 22 | color=discord.Color.blurple(), 23 | description="Processing..." 24 | )) 25 | 26 | # Clear The cached logs 27 | #with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 28 | # '../../../temp/logs.log'), 'w'): 29 | # pass 30 | await ctx.invoke(self.bot.get_command('debug clear')) 31 | emsg = await msg.edit(embed=discord.Embed( 32 | color=discord.Color.blurple(), 33 | description="✅ Cleared Cached Logs" 34 | )) 35 | logger.info("==== Rebooting Bot ====") 36 | await msg.edit(embed=discord.Embed( 37 | color=discord.Color.blurple(), 38 | description="`✅ | Cleared Cached Logs`\n\n`✅ | Rebooting....`" 39 | )) 40 | os.execl(sys.executable, sys.executable, * sys.argv) 41 | 42 | 43 | def setup(bot): 44 | bot.add_cog(RebootCog(bot)) 45 | -------------------------------------------------------------------------------- /reminder/reminder.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import math 3 | import time 4 | from discord.ext import commands 5 | 6 | from core import checks 7 | from core.models import PermissionLevel, getLogger 8 | 9 | logger = getLogger(__name__) 10 | 11 | 12 | class ReminderPlugin(commands.Cog): 13 | """ 14 | Create Reminders. 15 | """ 16 | 17 | def __init__(self, bot): 18 | self.bot = bot 19 | self.db = bot.plugin_db.get_partition(self) 20 | self.active_reminders = {} 21 | 22 | async def _update_db(self): 23 | await self.db.find_one_and_update( 24 | {"_id": "reminders"}, 25 | {"$set": {"active": self.active_reminders}}, 26 | upsert=True, 27 | ) 28 | 29 | async def _set_from_db(self): 30 | config = await self.db.find_one({"_id": "reminders"}) 31 | if config is None: 32 | await self.db.find_one_and_update( 33 | {"_id": "reminders"}, 34 | {"$set": {"reminders": dict()}}, 35 | upsert=True, 36 | ) 37 | 38 | for key, reminder in config.get("reminders", {}).items(): 39 | if key in self.active_reminders: 40 | continue 41 | self.active_reminders[str(key)] = reminder 42 | self.bot.loop.create_task(self._handle_reminder(reminder)) 43 | 44 | async def _handle_reminder(self, reminder_obj): 45 | logger.info("In Handle Reminder") 46 | _time = reminder_obj["time"] - time.time() 47 | logger.info(_time) 48 | await asycio.sleep(_time if _time >= 0 else 0) 49 | logger.info("Timeout finished") 50 | 51 | if str(reminder_obj["message"]) not in self.active_reminders: 52 | logger.info("No Reminder in cache") 53 | return 54 | 55 | channel = self.bot.get_channel(reminder_obj["channel"]) 56 | if channel is None: 57 | logger.info("Channel Not Found") 58 | try: 59 | self.active_reminders.pop(str(reminder_obj["message"])) 60 | except KeyError: 61 | pass 62 | return 63 | 64 | days = math.floor(g_time // 86400) 65 | hours = math.floor(g_time // 3600 % 24) 66 | minutes = math.floor(g_time // 60 % 60) 67 | seconds = math.floor(g_time % 60) 68 | 69 | to_send = f"{f'{days} Days ' if days > 0 else ''}{f'{hours} Hours ' if hours > 0 else ''}{f'{minutes} Minutes ' if minutes > 0 else ''}{f'{seconds} Seconds ' if seconds > 0 else ''} ago: {reminder_obj['reminder']}\n\n{reminder_obj['jump_url']}" 70 | try: 71 | await channel.send(to_send) 72 | self.active_reminders.pop(str(reminder_obj["message"])) 73 | except: 74 | logger.info("Cant POP") 75 | pass 76 | await self._update_db() 77 | 78 | @commands.command(name="reminder", aliases=["remindme", "remind", "rme"]) 79 | @checks.has_permissions(PermissionLevel.REGULAR) 80 | async def reminder(self, ctx: commands.Context, *, message: str): 81 | """ 82 | Create a reminder 83 | 84 | **Example:** 85 | {prefix}remind in 2 hours Test This 86 | """ 87 | resp = await self.bot.session.get( 88 | "https://dateparser.piyush.codes/fromstr", 89 | params={ 90 | "message": message[: len(message) // 2] 91 | if len(message) > 20 92 | else message 93 | }, 94 | ) 95 | try: 96 | json = await resp.json() 97 | except: 98 | await ctx.send("API appears to be down, please try sometime later") 99 | if resp.status == 400: 100 | await ctx.send(json["message"]) 101 | return 102 | elif resp.status == 500: 103 | await ctx.send(json["message"]) 104 | return 105 | else: 106 | time = json["message"] 107 | message = message.replace(json["readable_time"], "") 108 | 109 | await ctx.send( 110 | f"Alright <@{ctx.author.id}>, {json['readable_time']}: {message}" 111 | ) 112 | reminder_obj = { 113 | "message": ctx.message.id, 114 | "channel": ctx.channel.id, 115 | "guild": ctx.guild.id, 116 | "reminder": message, 117 | "time": time, 118 | "url": ctx.message.jump_url, 119 | } 120 | self.active_reminders[str(ctx.message.id)] = reminder_obj 121 | self.bot.loop.create_task(self._handle_reminder(reminder_obj)) 122 | await self._update_db() 123 | 124 | 125 | def setup(bot): 126 | bot.add_cog(ReminderPlugin(bot)) 127 | -------------------------------------------------------------------------------- /report-user/report-user.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import asyncio 3 | from datetime import datetime 4 | from discord.ext import commands 5 | 6 | from core import checks 7 | from core.models import PermissionLevel 8 | 9 | 10 | class ReportUser(commands.Cog): 11 | """ 12 | Report a user to staff 13 | """ 14 | 15 | def __init__(self, bot): 16 | self.bot: discord.Client = bot 17 | self.db = bot.plugin_db.get_partition(self) 18 | self.blacklist = [] 19 | self.channel = None 20 | self.message = "Thanks for reporting, our Staff will look into it soon." 21 | self.current_case = 1 22 | asyncio.create_task(self._set_config()) 23 | 24 | async def _set_config(self): 25 | config = await self.db.find_one({"_id": "config"}) 26 | if config is None: 27 | return 28 | else: 29 | self.blacklist = config.get("blacklist", []) 30 | self.channel = config.get("channel", None) 31 | self.current_case = config.get("case", 1) 32 | self.message = config.get( 33 | "message", "Thanks for reporting, our Staff will look into it soon." 34 | ) 35 | 36 | async def update(self): 37 | await self.db.find_one_and_update( 38 | {"_id": "config"}, 39 | { 40 | "$set": { 41 | "blacklist": self.blacklist, 42 | "chanel": self.channel, 43 | "message": self.message, 44 | "case": self.current_case, 45 | } 46 | }, 47 | upsert=True, 48 | ) 49 | 50 | @commands.group() 51 | async def ru(self, ctx: commands.Context): 52 | """ 53 | Report User Staff Commands 54 | """ 55 | return 56 | 57 | @ru.command() 58 | @checks.has_permissions(PermissionLevel.ADMIN) 59 | async def blacklist(self, ctx, member: discord.Member): 60 | """ 61 | Blacklist or blacklist a user 62 | """ 63 | if member.id not in self.blacklist: 64 | self.blacklist.append(member.id) 65 | updated = False 66 | else: 67 | self.blacklist.pop(member.id) 68 | updated = True 69 | await self.update() 70 | 71 | await ctx.send(f"{'Un' if updated else ''}Blacklisted!") 72 | 73 | @ru.command() 74 | @checks.has_permissions(PermissionLevel.ADMIN) 75 | async def channel(self, ctx: commands.Context, channel: discord.TextChannel): 76 | """ 77 | Set A reports Channel 78 | """ 79 | await self.db.find_one_and_update( 80 | {"_id": "config"}, {"$set": {"channel": str(channel.id)}}, upsert=True 81 | ) 82 | self.channel = str(channel.id) 83 | await ctx.send("Done!") 84 | 85 | @ru.command() 86 | @checks.has_permissions(PermissionLevel.ADMIN) 87 | async def message(self, ctx, *, msg: str): 88 | """ 89 | Customise the message that will be sent to user 90 | """ 91 | await self.db.find_one_and_update( 92 | {"_id": "config"}, {"$set": {"message": msg}}, upsert=True 93 | ) 94 | self.message = msg 95 | await ctx.send("Done!") 96 | 97 | @commands.command() 98 | async def report( 99 | self, ctx: commands.Context, member: discord.Member, *, reason: str 100 | ): 101 | """ 102 | Report a user 103 | """ 104 | if ctx.author.id in self.blacklist: 105 | await ctx.message.delete() 106 | return 107 | 108 | if self.channel is None: 109 | await ctx.message.delete() 110 | await ctx.author.send("Reports Channel for the guild has not been set.") 111 | return 112 | else: 113 | channel: discord.TextChannel = self.bot.get_channel(int(self.channel)) 114 | embed = discord.Embed( 115 | color=discord.Colour.red(), timestamp=datetime.utcnow() 116 | ) 117 | embed.set_author( 118 | name=f"{ctx.author.name}#{ctx.author.discriminator}", 119 | icon_url=ctx.author.avatar_url, 120 | ) 121 | embed.title = "User Report" 122 | embed.add_field( 123 | name="Against", 124 | value=f"{member.name}#{member.discriminator}", 125 | inline=False, 126 | ) 127 | embed.add_field(name="Reason", value=reason, inline=False) 128 | embed.set_footer(text=f"Case {self.current_case}") 129 | m: discord.Message = await channel.send(embed=embed) 130 | await ctx.author.send(self.message) 131 | await ctx.message.delete() 132 | await m.add_reaction("\U00002705") 133 | await self.db.insert_one( 134 | { 135 | "case": self.current_case, 136 | "author": str(ctx.author.id), 137 | "against": str(member.id), 138 | "reason": reason, 139 | "resolved": False, 140 | } 141 | ) 142 | self.current_case = self.current_case + 1 143 | await self.update() 144 | return 145 | 146 | @ru.command() 147 | @checks.has_permissions(PermissionLevel.MOD) 148 | async def info(self, ctx: commands.Context, casen: int): 149 | case = await self.db.find_one({"case": casen}) 150 | 151 | if case is None: 152 | await ctx.send(f"Case `#{casen}` dose'nt exist") 153 | return 154 | else: 155 | user1: discord.User = await self.bot.fetch_user(int(case["author"])) 156 | user2: discord.User = await self.bot.fetch_user(int(case["against"])) 157 | embed = discord.Embed(color=discord.Colour.red()) 158 | embed.add_field( 159 | name="By", value=f"{user1.name}#{user1.discriminator}", inline=False 160 | ) 161 | embed.add_field( 162 | name="Against", 163 | value=f"{user2.name}#{user2.discriminator}", 164 | inline=False, 165 | ) 166 | embed.add_field(name="Reason", value=case["reason"], inline=False) 167 | embed.add_field(name="Resolved", value=case["resolved"], inline=False) 168 | embed.title = "Report Log" 169 | await ctx.send(embed=embed) 170 | 171 | @commands.Cog.listener() 172 | async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): 173 | if payload.user_id == self.bot.user.id: 174 | return 175 | 176 | if ( 177 | str(payload.channel_id) != str(self.channel) 178 | or str(payload.emoji.name) != "✅" 179 | ): 180 | return 181 | 182 | channel: discord.TextChannel = self.bot.get_channel(payload.channel_id) 183 | msg: discord.Message = await channel.fetch_message(payload.message_id) 184 | 185 | if not msg.embeds or msg.embeds[0] is None: 186 | return 187 | 188 | if msg.embeds[0].footer.text is None: 189 | return 190 | 191 | case = int(msg.embeds[0].footer.text[5:]) 192 | 193 | casedb = await self.db.find_one({"case": case}) 194 | 195 | if casedb is None: 196 | return 197 | 198 | if casedb["resolved"] is True: 199 | await channel.send(f"Case `#{case}`Already resolved.") 200 | return 201 | 202 | def check(messge: discord.Message): 203 | return ( 204 | payload.user_id == messge.author.id 205 | and payload.channel_id == messge.channel.id 206 | ) 207 | 208 | await channel.send("Enter Your Report which will be sent to the reporter") 209 | reportr = await self.bot.wait_for("message", check=check) 210 | user1 = self.bot.get_user(int(casedb["author"])) 211 | await user1.send(f"**Reply From Staff Team:**\n{reportr.content}") 212 | await channel.send("DM'd") 213 | await self.db.find_one_and_update({"case": case}, {"$set": {"resolved": True}}) 214 | return 215 | 216 | 217 | def setup(bot): 218 | bot.add_cog(ReportUser(bot)) 219 | -------------------------------------------------------------------------------- /role-assignment/role-assignment.py: -------------------------------------------------------------------------------- 1 | # This file contains edited code from https://github.com/papiersnipper/modmail-plugins/blob/master/role-assignment/role-assignment.py . Copyright reserved with respective owners 2 | import logging 3 | 4 | import asyncio 5 | import discord 6 | from discord.ext import commands 7 | 8 | from core import checks 9 | from core.models import PermissionLevel 10 | 11 | Cog = getattr(commands, "Cog", object) 12 | 13 | logger = logging.getLogger("Modmail") 14 | 15 | 16 | class RoleAssignment(Cog): 17 | """Assign roles using reactions. 18 | More info: [click here](https://github.com/officialpiyush/modmail-plugins/tree/master/role-assignment) 19 | """ 20 | 21 | def __init__(self, bot): 22 | self.bot = bot 23 | self.db = bot.plugin_db.get_partition(self) 24 | self.ids = [] 25 | asyncio.create_task(self.sync()) 26 | 27 | async def update_db(self): 28 | 29 | await self.db.find_one_and_update( 30 | {"_id": "role-config"}, {"$set": {"ids": self.ids}} 31 | ) 32 | 33 | async def _set_db(self): 34 | 35 | config = await self.db.find_one({"_id": "role-config"}) 36 | 37 | if config is None: 38 | return 39 | 40 | self.ids = config["ids"] 41 | 42 | async def sync(self): 43 | 44 | await self._set_db() 45 | 46 | category_id = int(self.bot.config["main_category_id"]) 47 | 48 | if category_id is None: 49 | print("No main_category_id found!") 50 | return 51 | 52 | guild = self.bot.get_guild(int(self.bot.config["guild_id"])) 53 | 54 | if guild is None: 55 | print("No guild_id found!") 56 | return 57 | 58 | for c in guild.categories: 59 | if c.id != category_id: 60 | continue 61 | else: 62 | channel_genesis_ids = [] 63 | for channel in c.channels: 64 | if not isinstance(channel, discord.TextChannel): 65 | continue 66 | 67 | if channel.topic is None: 68 | continue 69 | 70 | if channel.topic[:9] != "User ID: ": 71 | continue 72 | 73 | messages = await channel.history(oldest_first=True).flatten() 74 | genesis_message = str(messages[0].id) 75 | channel_genesis_ids.append(genesis_message) 76 | 77 | if genesis_message not in self.ids: 78 | self.ids.append(genesis_message) 79 | else: 80 | continue 81 | 82 | for id in self.ids: 83 | if id not in channel_genesis_ids: 84 | self.ids.remove(id) 85 | else: 86 | continue 87 | 88 | await self.update_db() 89 | logger.info("Synced role with the database") 90 | 91 | @commands.group(name="role", aliases=["roles"], invoke_without_command=True) 92 | @checks.has_permissions(PermissionLevel.ADMINISTRATOR) 93 | async def role(self, ctx): 94 | """Automaticly assign roles when you click on the emoji.""" 95 | 96 | await ctx.send_help(ctx.command) 97 | 98 | @role.command(name="add") 99 | @checks.has_permissions(PermissionLevel.ADMINISTRATOR) 100 | async def add(self, ctx, emoji: discord.Emoji, *, role: discord.Role): 101 | """Add a clickable emoji to each new message.""" 102 | 103 | config = await self.db.find_one({"_id": "role-config"}) 104 | 105 | if config is None: 106 | await self.db.insert_one({"_id": "role-config", "emoji": {}}) 107 | 108 | config = await self.db.find_one({"_id": "role-config"}) 109 | 110 | emoji_dict = config["emoji"] 111 | 112 | try: 113 | emoji_dict[str(emoji.id)] 114 | failed = True 115 | except KeyError: 116 | failed = False 117 | 118 | if failed: 119 | return await ctx.send("That emoji already assigns a role.") 120 | 121 | emoji_dict[f"<:{emoji.name}:{emoji.id}>"] = role.name 122 | 123 | await self.db.update_one( 124 | {"_id": "role-config"}, {"$set": {"emoji": emoji_dict}} 125 | ) 126 | 127 | await ctx.send( 128 | f'I successfully pointed <:{emoji.name}:{emoji.id}> to "{role.name}"' 129 | ) 130 | 131 | @role.command(name="remove") 132 | @checks.has_permissions(PermissionLevel.ADMINISTRATOR) 133 | async def remove(self, ctx, emoji: discord.Emoji): 134 | """Remove a clickable emoji from each new message.""" 135 | 136 | config = await self.db.find_one({"_id": "role-config"}) 137 | 138 | if config is None: 139 | return await ctx.send("There are no emoji set for this server.") 140 | 141 | emoji_dict = config["emoji"] 142 | 143 | try: 144 | del emoji_dict[f"<:{emoji.name}:{emoji.id}>"] 145 | except KeyError: 146 | return await ctx.send("That emoji is not configured") 147 | 148 | await self.db.update_one( 149 | {"_id": "role-config"}, {"$set": {"emoji": emoji_dict}} 150 | ) 151 | 152 | await ctx.send(f"I successfully deleted <:{emoji.name}:{emoji.id}>.") 153 | 154 | @Cog.listener() 155 | async def on_thread_ready(self, thread): 156 | message = thread.genesis_message 157 | 158 | try: 159 | for k, v in (await self.db.find_one({"_id": "role-config"}))[ 160 | "emoji" 161 | ].items(): 162 | await message.add_reaction(k) 163 | except TypeError: 164 | return 165 | 166 | self.ids.append(str(message.id)) 167 | await self.update_db() 168 | 169 | @Cog.listener() 170 | async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): 171 | 172 | await asyncio.sleep(1) 173 | 174 | if str(payload.message_id) not in self.ids: 175 | return 176 | 177 | guild: discord.Guild = self.bot.main_guild 178 | 179 | if payload.user_id == self.bot.user.id: 180 | return 181 | 182 | member_id = int(guild.get_channel(payload.channel_id).topic[9:]) 183 | 184 | role = (await self.db.find_one({"_id": "role-config"}))["emoji"][ 185 | f"<:{payload.emoji.name}:{payload.emoji.id}>" 186 | ] 187 | 188 | role = discord.utils.get(guild.roles, name=role) 189 | 190 | if role is None: 191 | return await guild.get_channel(payload.channel_id).send( 192 | "I couldn't find that role..." 193 | ) 194 | 195 | for m in guild.members: 196 | if m.id == member_id: 197 | member = m 198 | else: 199 | continue 200 | 201 | await member.add_roles(role) 202 | await guild.get_channel(payload.channel_id).send( 203 | f"Successfully added {role} to {member.name}" 204 | ) 205 | 206 | @Cog.listener() 207 | async def on_raw_reaction_remove(self, payload): 208 | 209 | await asyncio.sleep(1) 210 | 211 | if str(payload.message_id) not in self.ids: 212 | return 213 | 214 | guild = self.bot.main_guild 215 | 216 | member_id = int(guild.get_channel(payload.channel_id).topic[9:]) 217 | 218 | role = (await self.db.find_one({"_id": "role-config"}))["emoji"][ 219 | f"<:{payload.emoji.name}:{payload.emoji.id}>" 220 | ] 221 | 222 | role = discord.utils.get(guild.roles, name=role) 223 | 224 | if role is None: 225 | return await guild.get_channel(payload.channel_id).send( 226 | "Configured role not found." 227 | ) 228 | 229 | for m in guild.members: 230 | if m.id == member_id: 231 | member = m 232 | else: 233 | continue 234 | 235 | await member.remove_roles(role) 236 | await guild.get_channel(payload.channel_id).send( 237 | f"Successfully removed {role} from {member.name}" 238 | ) 239 | 240 | 241 | def setup(bot): 242 | bot.add_cog(RoleAssignment(bot)) 243 | -------------------------------------------------------------------------------- /rolereaction/rolereaction.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import discord 3 | from discord.ext import commands 4 | 5 | from core import checks 6 | from core.models import PermissionLevel 7 | 8 | 9 | class ReactionRole(commands.Cog): 10 | def __init__(self, bot): 11 | self.bot = bot 12 | self.db = bot.plugin_db.get_partition(self) 13 | self.roles = dict() 14 | asyncio.create_task(self._set_config()) 15 | 16 | async def _set_config(self): 17 | config = await self.db.find_one({"_id": "config"}) 18 | if config is None: 19 | return 20 | self.roles = dict(config.get("roles", {})) 21 | 22 | @commands.group(aliases=["rr"]) 23 | async def rolereaction(self, ctx): 24 | if ctx.invoked_subcommand is None: 25 | return 26 | 27 | @rolereaction.command() 28 | @checks.has_permissions(PermissionLevel.MODERATOR) 29 | async def add(self, ctx, emoji: discord.Emoji, role: discord.Role): 30 | emote = emoji.name if emoji.id is None else emoji.id 31 | 32 | if emote in self.roles: 33 | updated = True 34 | else: 35 | updated = False 36 | self.roles[emote] = role.id 37 | 38 | await self.db.find_one_and_update( 39 | {"_id": "config"}, {"$set": {"roles": self.roles}}, upsert=True 40 | ) 41 | 42 | await ctx.send( 43 | f"Successfully {'updated'if updated else 'pointed'} {emoji} towards {role.name}" 44 | ) 45 | 46 | @rolereaction.command() 47 | @checks.has_permissions(PermissionLevel.MODERATOR) 48 | async def remove(self, ctx, emoji: discord.Emoji): 49 | """Remove a role from the role reaction list""" 50 | emote = emoji.name if emoji.id is None else emoji.id 51 | 52 | if emote not in self.roles: 53 | await ctx.send("The Given Emote Was Not Configured") 54 | return 55 | 56 | self.roles.pop(emote) 57 | 58 | await self.db.find_one_and_update( 59 | {"_id": "config"}, {"$set": {"roles": self.roles}}, upsert=True 60 | ) 61 | 62 | await ctx.send(f"Removed {emoji} from rolereaction list") 63 | return 64 | 65 | @rolereaction.command(aliases=["sc"]) 66 | @checks.has_permissions(PermissionLevel.MODERATOR) 67 | async def set_channel(self, ctx, channel=discord.TextChannel): 68 | await self.db.find_one_and_update( 69 | {"_id": "config"}, {"$set": {"channel": str(channel.id)}}, upsert=True 70 | ) 71 | 72 | await ctx.send(f"{channel.mention} has been set!") 73 | 74 | @rolereaction.command() 75 | @checks.has_permissions(PermissionLevel.MODERATOR) 76 | async def react(self, ctx, id: discord.Message.id): 77 | """React On The Message""" 78 | config = await self.db.find_one({"_id": "config"}) 79 | if config is None: 80 | return 81 | 82 | dbchannel = config["channel"] 83 | 84 | channel: discord.TextChannel = await ctx.guild.get_channel(int(dbchannel)) 85 | 86 | if channel: 87 | msg: discord.Message = await channel.fetch_message(int(id)) 88 | for x in self.roles: 89 | await msg.add_reaction(x) 90 | 91 | @commands.Cog.listener() 92 | async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): 93 | user: discord.User = self.bot.get_user(int(payload.user_id)) 94 | guild: discord.Guild = self.bot.config.get("GUILD_ID") 95 | 96 | if user.bot: 97 | return 98 | 99 | member: discord.Member = await guild.fetch_member(payload.user_id) 100 | 101 | if member is None: 102 | return 103 | 104 | if payload.emoji.name in self.roles or payload.emoji.id in self.roles: 105 | role = await guild.get_role( 106 | self.roles[payload.emoji.name or payload.emoji.id] 107 | ) 108 | await member.add_roles(role) 109 | 110 | 111 | def setup(bot): 112 | bot.add_cog(ReactionRole(bot)) 113 | -------------------------------------------------------------------------------- /staff-stats/staff-stats.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from discord.ext import commands 4 | 5 | from core import checks 6 | from core.models import PermissionLevel 7 | 8 | 9 | class StaffStatsPlugin(commands.Cog): 10 | """ 11 | Just a plugin which saves staff IDs in the database for frontend stuff. 12 | """ 13 | 14 | def __init__(self, bot): 15 | self.bot = bot 16 | self.db = bot.plugin_db.get_partition(self) 17 | bot.loop.create_task(self._update_stats()) 18 | 19 | async def _update_stats(self): 20 | while True: 21 | category = self.bot.get_channel( 22 | int(self.bot.config.get("main_category_id")) 23 | ) 24 | 25 | staff_members = list() 26 | 27 | for member in self.bot.modmail_guild.members: 28 | if member.permissions_in(category).read_messages: 29 | if not member.bot: 30 | staff_members.append(str(member.id)) 31 | 32 | await self.db.find_one_and_update( 33 | {"_id": "list"}, {"$set": {"staff": staff_members}}, upsert=True 34 | ) 35 | 36 | await asyncio.sleep(86400) 37 | 38 | @commands.command() 39 | @checks.has_permissions(PermissionLevel.ADMIN) 40 | async def syncstaff(self, ctx): 41 | """ 42 | Sync Staff 43 | """ 44 | category = self.bot.get_channel(int(self.bot.config.get("main_category_id"))) 45 | 46 | staff_members = list() 47 | 48 | for member in self.bot.modmail_guild.members: 49 | if member.permissions_in(category).read_messages: 50 | if not member.bot: 51 | staff_members.append(str(member.id)) 52 | 53 | await self.db.find_one_and_update( 54 | {"_id": "list"}, {"$set": {"staff": staff_members}}, upsert=True 55 | ) 56 | 57 | await ctx.send("Done.") 58 | return 59 | 60 | 61 | def setup(bot): 62 | bot.add_cog(StaffStatsPlugin(bot)) 63 | -------------------------------------------------------------------------------- /starboard/starboard.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from datetime import datetime 3 | 4 | import discord 5 | from discord import Client 6 | from discord.ext import commands 7 | 8 | from core import checks 9 | from core.models import PermissionLevel, getLogger 10 | 11 | logger = getLogger(__name__) 12 | 13 | 14 | class Starboard(commands.Cog): 15 | """ 16 | Basically a starboard . Leave a ⭐ if you like this plugin https://github.com/officialpiyush/modmail-plugins 17 | """ 18 | def __init__(self, bot): 19 | self.bot: Client = bot 20 | self.db = bot.plugin_db.get_partition(self) 21 | self.channel = None 22 | self.stars = 2 23 | self.user_blacklist: list = list() 24 | self.channel_blacklist: list = list() 25 | self.bot.loop.create_task(self._set_val()) 26 | 27 | async def _update_db(self): 28 | await self.db.find_one_and_update( 29 | {"_id": "config"}, 30 | { 31 | "$set": { 32 | "channel": self.channel, 33 | "stars": self.stars, 34 | "blacklist": { 35 | "user": self.user_blacklist, 36 | "channel": self.channel_blacklist, 37 | }, 38 | } 39 | }, 40 | upsert=True, 41 | ) 42 | 43 | async def _set_val(self): 44 | config = await self.db.find_one({"_id": "config"}) 45 | 46 | if config is None: 47 | await self._update_db() 48 | return 49 | 50 | self.channel = config.get("channel", None) 51 | self.stars = config.get("stars", 2) 52 | self.user_blacklist = config["blacklist"]["user"] 53 | self.channel_blacklist = config["blacklist"]["channel"] 54 | 55 | @commands.group(aliases=["st", "sb"], invoke_without_command=True) 56 | @checks.has_permissions(PermissionLevel.ADMIN) 57 | async def starboard(self, ctx: commands.Context): 58 | await ctx.send_help(ctx.command) 59 | 60 | @starboard.command(aliases=["setchannel", "setch", "sc"]) 61 | @checks.has_permissions(PermissionLevel.ADMIN) 62 | async def channel(self, ctx: commands.Context, channel: discord.TextChannel): 63 | """ 64 | Set the starboard channel where the messages will go 65 | **Usage:** 66 | starboard channel **#this-is-a-channel** 67 | """ 68 | self.channel = str(channel.id) 69 | await self._update_db() 70 | 71 | await ctx.send(f"Done! {channel.mention} is the Starboard Channel now!") 72 | 73 | @starboard.command(aliases=["setstars", "ss"]) 74 | @checks.has_permissions(PermissionLevel.ADMIN) 75 | async def stars(self, ctx: commands.Context, stars: int): 76 | """ 77 | Set the number of stars the message needs to appear on the starboard channel 78 | **Usage:** 79 | starboard stars 2 80 | """ 81 | self.stars = stars 82 | await self._update_db() 83 | 84 | await ctx.send( 85 | f"Done.Now this server needs `{stars}` :star: to appear on the starboard channel." 86 | ) 87 | 88 | @starboard.group() 89 | @checks.has_permissions(PermissionLevel.ADMIN) 90 | async def blacklist(self, ctx: commands.Context): 91 | """ 92 | Blacklist users and channels 93 | """ 94 | if ctx.invoked_subcommand is None: 95 | await ctx.send_help() 96 | 97 | @blacklist.command(aliases=["user"]) 98 | @checks.has_permissions(PermissionLevel.ADMIN) 99 | async def member(self, ctx: commands.Context, member: discord.Member): 100 | """ 101 | Blacklist a user so that the user's reaction dosen't get counted 102 | **Usage:** 103 | starboard blacklist member @user 104 | """ 105 | 106 | if str(member.id) in self.user_blacklist: 107 | self.user_blacklist.remove(str(member.id)) 108 | removed = True 109 | else: 110 | self.user_blacklist.append(str(member.id)) 111 | removed = False 112 | 113 | await ctx.send( 114 | f"{'Un' if removed else None}Blacklisted **{member.name}#{member.discriminator}**" 115 | ) 116 | return 117 | 118 | @blacklist.command(name="channel") 119 | @checks.has_permissions(PermissionLevel.ADMIN) 120 | async def blacklist_channel( 121 | self, ctx: commands.Context, channel: discord.TextChannel 122 | ): 123 | """ 124 | Blacklist Channels so that messages sent in those channels dont appear on starboard 125 | **Usage:** 126 | starboard blacklist channel **#channel** 127 | """ 128 | if str(channel.id) in self.channel_blacklist: 129 | self.channel_blacklist.remove(str(channel.id)) 130 | await self._update_db() 131 | removed = True 132 | else: 133 | self.channel_blacklist.append(str(channel.id)) 134 | await self._update_db() 135 | removed = False 136 | 137 | await ctx.send(f"{'Un' if removed else None}Blacklisted {channel.mention}") 138 | return 139 | 140 | @commands.Cog.listener() 141 | async def on_raw_reaction_remove(self, payload: discord.RawReactionActionEvent): 142 | await self.handle_reaction(payload=payload) 143 | 144 | @commands.Cog.listener() 145 | async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): 146 | await self.handle_reaction(payload=payload) 147 | 148 | async def handle_reaction(self, payload: discord.RawReactionActionEvent): 149 | config = await self.db.find_one({"_id": "config"}) 150 | 151 | if not config or not self.channel: 152 | logger.info("No config or channel") 153 | return 154 | 155 | # check for blacklist 156 | if self.channel_blacklist.__contains__(str(payload.channel_id)) or self.user_blacklist.__contains__( 157 | str(payload.user_id)): 158 | logger.info("Blacklisted") 159 | return 160 | 161 | guild: discord.Guild = self.bot.get_guild(int(self.bot.config["guild_id"])) 162 | starboard_channel: discord.TextChannel = guild.get_channel(int(self.channel)) 163 | channel: discord.TextChannel = guild.get_channel(payload.channel_id) 164 | user: discord.User = await self.bot.fetch_user(payload.user_id) 165 | 166 | if not channel or not starboard_channel: 167 | logger.info("No channel found") 168 | return 169 | 170 | message: discord.Message = await channel.fetch_message(payload.message_id) 171 | 172 | if message.author.id == payload.user_id: 173 | logger.info("Author added the reaction") 174 | return 175 | 176 | found_emote = False 177 | for emote in message.reactions: 178 | if emote.emoji == "⭐": 179 | found_emote = True 180 | reaction: discord.Reaction = emote 181 | count = reaction.count 182 | reacted_users: typing.List[discord.User] = await reaction.users().flatten() 183 | has_author_reacted = discord.utils.find(lambda u: u.id == message.author.id, reacted_users) 184 | if has_author_reacted: 185 | count = count - 1 186 | 187 | should_delete = False 188 | 189 | if count < self.stars: 190 | should_delete = True 191 | 192 | messages = await starboard_channel.history( 193 | limit=70, 194 | around=message.created_at 195 | ).flatten() 196 | found = False 197 | 198 | for msg in messages: 199 | if len(msg.embeds) <= 0: 200 | logger.info("No embeds") 201 | continue 202 | 203 | if not msg.embeds[0].footer or not msg.embeds[0].footer.text or "⭐" not in msg.embeds[ 204 | 0].footer.text: 205 | print(msg.embeds) 206 | logger.info("No stars") 207 | continue 208 | 209 | if msg.embeds[0].footer.text.endswith(str(payload.message_id)): 210 | logger.info("got one") 211 | found = True 212 | if should_delete: 213 | logger.info("delete message") 214 | await msg.delete() 215 | break 216 | e = msg.embeds[0] 217 | e.set_footer(text=f"⭐ {count} | {payload.message_id}") 218 | await msg.edit(content=f"<#{payload.channel_id}>", embed=e) 219 | break 220 | 221 | if not found: 222 | if should_delete: 223 | logger.info("Should Delete") 224 | return 225 | 226 | embed = discord.Embed( 227 | color=discord.Colour.gold(), 228 | description=message.content, 229 | timestamp=datetime.utcnow(), 230 | title="Jump to message ►", 231 | url=message.jump_url 232 | ) 233 | embed.set_author( 234 | name=str(message.author), 235 | icon_url=message.author.avatar_url, 236 | ) 237 | embed.set_footer(text=f"⭐ {count} | {payload.message_id}") 238 | if len(message.attachments) > 1: 239 | try: 240 | embed.set_image(url=message.attachments[0].url) 241 | except: 242 | pass 243 | 244 | await starboard_channel.send( 245 | f"{channel.mention}", embed=embed 246 | ) 247 | 248 | if not found_emote: 249 | messages = await starboard_channel.history( 250 | limit=70, 251 | around=message.created_at 252 | ).flatten() 253 | found = False 254 | 255 | for msg in messages: 256 | if len(msg.embeds) <= 0: 257 | logger.info("No embeds") 258 | continue 259 | 260 | if not msg.embeds[0].footer or not msg.embeds[0].footer.text or "⭐" not in msg.embeds[0].footer.text: 261 | print(msg.embeds) 262 | logger.info("No stars") 263 | continue 264 | 265 | if msg.embeds[0].footer.text.endswith(str(payload.message_id)): 266 | logger.info("got one") 267 | found = True 268 | await msg.delete() 269 | 270 | 271 | def setup(bot): 272 | bot.add_cog(Starboard(bot)) 273 | -------------------------------------------------------------------------------- /tags/README.md: -------------------------------------------------------------------------------- 1 |
2 | BD Image 3 |
4 | A plugin to manage tags, etc. 5 |
6 |
7 | 8 | 9 | Support 10 | 11 |
12 | 13 | --- -------------------------------------------------------------------------------- /tags/tags.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from datetime import datetime 3 | from discord.ext import commands 4 | 5 | from core import checks 6 | from core.models import PermissionLevel 7 | 8 | 9 | class TagsPlugin(commands.Cog): 10 | def __init__(self, bot): 11 | self.bot: discord.Client = bot 12 | self.db = bot.plugin_db.get_partition(self) 13 | 14 | @commands.group(invoke_without_command=True) 15 | @commands.guild_only() 16 | @checks.has_permissions(PermissionLevel.REGULAR) 17 | async def tags(self, ctx: commands.Context): 18 | """ 19 | Create Edit & Manage Tags 20 | """ 21 | await ctx.send_help(ctx.command) 22 | 23 | @tags.command() 24 | async def add(self, ctx: commands.Context, name: str, *, content: str): 25 | """ 26 | Make a new tag 27 | """ 28 | if (await self.find_db(name=name)) is not None: 29 | await ctx.send(f":x: | Tag with name `{name}` already exists!") 30 | return 31 | else: 32 | ctx.message.content = content 33 | await self.db.insert_one( 34 | { 35 | "name": name, 36 | "content": ctx.message.clean_content, 37 | "createdAt": datetime.utcnow(), 38 | "updatedAt": datetime.utcnow(), 39 | "author": ctx.author.id, 40 | "uses": 0, 41 | } 42 | ) 43 | 44 | await ctx.send( 45 | f":white_check_mark: | Tag with name `{name}` has been successfully created!" 46 | ) 47 | return 48 | 49 | @tags.command() 50 | async def edit(self, ctx: commands.Context, name: str, *, content: str): 51 | """ 52 | Edit an existing tag 53 | 54 | Only owner of tag or user with Manage Server permissions can use this command 55 | """ 56 | tag = await self.find_db(name=name) 57 | 58 | if tag is None: 59 | await ctx.send(f":x: | Tag with name `{name}` dose'nt exist") 60 | return 61 | else: 62 | member: discord.Member = ctx.author 63 | if ctx.author.id == tag["author"] or member.guild_permissions.manage_guild: 64 | await self.db.find_one_and_update( 65 | {"name": name}, 66 | {"$set": {"content": content, "updatedAt": datetime.utcnow()}}, 67 | ) 68 | 69 | await ctx.send( 70 | f":white_check_mark: | Tag `{name}` is updated successfully!" 71 | ) 72 | else: 73 | await ctx.send("You don't have enough permissions to edit that tag") 74 | 75 | @tags.command() 76 | async def delete(self, ctx: commands.Context, name: str): 77 | """ 78 | Delete a tag. 79 | 80 | Only owner of tag or user with Manage Server permissions can use this command 81 | """ 82 | tag = await self.find_db(name=name) 83 | if tag is None: 84 | await ctx.send(":x: | Tag `{name}` not found in the database.") 85 | else: 86 | if ( 87 | ctx.author.id == tag["author"] 88 | or ctx.author.guild_permissions.manage_guild 89 | ): 90 | await self.db.delete_one({"name": name}) 91 | 92 | await ctx.send( 93 | f":white_check_mark: | Tag `{name}` has been deleted successfully!" 94 | ) 95 | else: 96 | await ctx.send("You don't have enough permissions to delete that tag") 97 | 98 | @tags.command() 99 | async def claim(self, ctx: commands.Context, name: str): 100 | """ 101 | Claim a tag if the user has left the server 102 | """ 103 | tag = await self.find_db(name=name) 104 | 105 | if tag is None: 106 | await ctx.send(":x: | Tag `{name}` not found.") 107 | else: 108 | member = await ctx.guild.get_member(tag["author"]) 109 | if member is not None: 110 | await ctx.send( 111 | f":x: | The owner of the tag is still in the server `{member.name}#{member.discriminator}`" 112 | ) 113 | return 114 | else: 115 | await self.db.find_one_and_update( 116 | {"name": name}, 117 | {"$set": {"author": ctx.author.id, "updatedAt": datetime.utcnow()}}, 118 | ) 119 | 120 | await ctx.send( 121 | f":white_check_mark: | Tag `{name}` is now owned by `{ctx.author.name}#{ctx.author.discriminator}`" 122 | ) 123 | 124 | @tags.command() 125 | async def info(self, ctx: commands.Context, name: str): 126 | """ 127 | Get info on a tag 128 | """ 129 | tag = await self.find_db(name=name) 130 | 131 | if tag is None: 132 | await ctx.send(":x: | Tag `{name}` not found.") 133 | else: 134 | user: discord.User = await self.bot.fetch_user(tag["author"]) 135 | embed = discord.Embed() 136 | embed.colour = discord.Colour.green() 137 | embed.title = f"{name}'s Info" 138 | embed.add_field( 139 | name="Created By", value=f"{user.name}#{user.discriminator}" 140 | ) 141 | embed.add_field(name="Created At", value=tag["createdAt"]) 142 | embed.add_field( 143 | name="Last Modified At", value=tag["updatedAt"], inline=False 144 | ) 145 | embed.add_field(name="Uses", value=tag["uses"], inline=False) 146 | await ctx.send(embed=embed) 147 | return 148 | 149 | @commands.command() 150 | async def tag(self, ctx: commands.Context, name: str): 151 | """ 152 | Use a tag! 153 | """ 154 | tag = await self.find_db(name=name) 155 | if tag is None: 156 | await ctx.send(f":x: | Tag {name} not found.") 157 | return 158 | else: 159 | await ctx.send(tag["content"]) 160 | await self.db.find_one_and_update( 161 | {"name": name}, {"$set": {"uses": tag["uses"] + 1}} 162 | ) 163 | return 164 | 165 | @commands.Cog.listener() 166 | async def on_message(self, msg: discord.Message): 167 | if not msg.content.startswith(self.bot.prefix) or msg.author.bot: 168 | return 169 | content = msg.content.replace(self.bot.prefix, "") 170 | names = content.split(" ") 171 | 172 | tag = await self.db.find_one({"name": names[0]}) 173 | 174 | if tag is None: 175 | return 176 | else: 177 | await msg.channel.send(tag["content"]) 178 | await self.db.find_one_and_update( 179 | {"name": names[0]}, {"$set": {"uses": tag["uses"] + 1}} 180 | ) 181 | return 182 | 183 | async def find_db(self, name: str): 184 | return await self.db.find_one({"name": name}) 185 | 186 | 187 | def setup(bot): 188 | bot.add_cog(TagsPlugin(bot)) 189 | -------------------------------------------------------------------------------- /translator/README.md: -------------------------------------------------------------------------------- 1 |
2 | Translator Image 3 |
4 | A Modmail plugin that (auto) translates messages 5 |
6 |
7 | 8 | 9 | Support 10 | 11 |
12 | 13 | --- 14 | 15 | # How To Install? 16 | 17 | * To Install this plugin just run the following command - `plugins add officialpiyush/modmail-plugins/translator` and you are good to go! 18 | 19 | # Support 20 | 21 | * Support inquiries are handled in my server - [Discord Invite Link](https://discord.gg/hzD72GE) 22 | 23 | # Commands 24 | 25 | | Command | Aliases | Description | Example | Permission Required | Source Code | 26 | |:------------------------: |:-------: |:-----------------------------------------------: |:-------------------------------------------------: |:-------------------: |:--------------------------------------------------------------------------------------------------------------------------------------: | 27 | | translate | - | Translate a message inside a modmail thread | translate | None | [Source](https://github.com/officialpiyush/modmail-plugins/blob/d7ad5b46dbe7f4023d435f113d57363057aa850d/translator/translator.py#L23) | 28 | | translatetext | tt | Translate a message | translatetext | None | [Source](https://github.com/officialpiyush/modmail-plugins/blob/d7ad5b46dbe7f4023d435f113d57363057aa850d/translator/translator.py#L44) | 29 | | auto_translate_thread | att | Add/Remove a channel from auto translation list | att | Manage Messages | [Source](https://github.com/officialpiyush/modmail-plugins/blob/d7ad5b46dbe7f4023d435f113d57363057aa850d/translator/translator.py#L54) | 30 | | toggle_auto_translations | tat | Turn The Auto Translation Service On/Off | tat | Manage Server | [Source](https://github.com/officialpiyush/modmail-plugins/blob/d7ad5b46dbe7f4023d435f113d57363057aa850d/translator/translator.py#L75) | 31 | 32 | # Authors 33 | 34 | > GitHub [@officialpiyush](https://github.com/officialpiyush) 35 | -------------------------------------------------------------------------------- /translator/requirements.txt: -------------------------------------------------------------------------------- 1 | googletrans == 2.4.0 -------------------------------------------------------------------------------- /translator/translator.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import asyncio 3 | import datetime 4 | from discord.ext import commands 5 | from discord import NotFound, HTTPException, User 6 | 7 | from core import checks 8 | from core.models import PermissionLevel 9 | 10 | from googletrans import Translator 11 | 12 | 13 | class TranslatePlugin(commands.Cog): 14 | def __init__(self, bot): 15 | self.bot = bot 16 | self.db = bot.plugin_db.get_partition(self) 17 | self.translator = Translator() 18 | self.tt = set() 19 | self.enabled = True 20 | asyncio.create_task(self._set_config()) 21 | 22 | async def _set_config(self): 23 | config = await self.db.find_one({"_id": "config"}) 24 | if config is None: 25 | await self.db.find_one_and_update( 26 | {"_id": "config"}, 27 | {"$set": {"enabled": True, "translateSet": list([])}}, 28 | upsert=True, 29 | ) 30 | self.enabled = config.get("enabled", True) 31 | self.tt = set(config.get("translateSet", [])) 32 | 33 | @commands.command() 34 | async def translate(self, ctx, msgid: int): 35 | """Translate a sent message or a modmail thread message into english.""" 36 | try: 37 | msg = await ctx.channel.fetch_message(msgid) 38 | if not msg.embeds: 39 | ms = msg.content 40 | elif msg.embeds and msg.embeds[0] is not None: 41 | ms = msg.embeds[0].description 42 | else: 43 | await ctx.send("Something wrong!") 44 | return 45 | tmsg = self.translator.translate(ms) 46 | embed = discord.Embed() 47 | embed.color = 4388013 48 | embed.description = tmsg.text 49 | await ctx.channel.send(embed=embed) 50 | except NotFound: 51 | await ctx.send("The provided message Was not found.") 52 | except HTTPException: 53 | await ctx.send("Failed to retrieve the message.") 54 | 55 | @commands.command(aliases=["tt"]) 56 | async def translatetext(self, ctx, *, message): 57 | """Translates a provided message into english""" 58 | tmsg = self.translator.translate(message) 59 | embed = discord.Embed() 60 | embed.color = 4388013 61 | embed.description = tmsg.text 62 | await ctx.channel.send(embed=embed) 63 | 64 | @commands.command(aliases=["att"]) 65 | @checks.has_permissions(PermissionLevel.SUPPORTER) 66 | async def auto_translate_thread(self, ctx): 67 | """Turn on auto translations for the ongoing thread.""" 68 | if "User ID:" not in ctx.channel.topic: 69 | await ctx.send("The channel is not a modmail thread") 70 | return 71 | if ctx.channel.id in self.tt: 72 | self.tt.remove(ctx.channel.id) 73 | removed = True 74 | else: 75 | self.tt.add(ctx.channel.id) 76 | removed = False 77 | 78 | await self.db.update_one( 79 | {"_id": "config"}, {"$set": {"translateSet": list(self.tt)}}, upsert=True 80 | ) 81 | 82 | await ctx.send( 83 | f"{'Removed' if removed else 'Added'} Channel {'from' if removed else 'to'} Auto Translations List." 84 | ) 85 | 86 | @commands.command(aliases=["tat"]) 87 | @checks.has_permissions(PermissionLevel.MODERATOR) 88 | async def toggle_auto_translations(self, ctx, enabled: bool): 89 | """Enable/Disable automatic translations""" 90 | self.enabled = enabled 91 | await self.db.update_one( 92 | {"_id": "config"}, {"$set": {"enabled": self.enabled}}, upsert=True 93 | ) 94 | await ctx.send(f"{'Enabled' if enabled else 'Disabled'} Auto Translations") 95 | 96 | @commands.Cog.listener() 97 | async def on_message(self, message): 98 | if not self.enabled: 99 | return 100 | 101 | channel = message.channel 102 | 103 | if channel.id not in self.tt: 104 | return 105 | 106 | if isinstance(message.author, User): 107 | return 108 | 109 | if "User ID:" not in channel.topic: 110 | return 111 | 112 | if not message.embeds: 113 | return 114 | 115 | if ( 116 | message.embeds[0].footer.text 117 | and "Message ID" not in message.embeds[0].footer.text 118 | ): 119 | return 120 | 121 | embed = message.embeds[0] 122 | 123 | tmsg = await self.bot.loop.run_in_executor( 124 | None, self.translator.translate, message.embeds[0].description 125 | ) 126 | 127 | if tmsg.src == "en": 128 | return 129 | 130 | field = { 131 | "inline": False, 132 | "name": f"Translation [{(tmsg.src).upper()}]", 133 | "value": tmsg.text, 134 | } 135 | 136 | try: 137 | embed._fields.insert(0, field) 138 | except AttributeError: 139 | embed._fields = [field] 140 | 141 | await message.edit(embed=embed) 142 | 143 | @commands.Cog.listener() 144 | async def on_ready(self): 145 | async with self.bot.session.post( 146 | "https://counter.modmail-plugins.piyush.codes/api/instances/translator", 147 | json={"id": self.bot.user.id}, 148 | ): 149 | print("Posted to Plugin API") 150 | 151 | 152 | def setup(bot): 153 | bot.add_cog(TranslatePlugin(bot)) 154 | -------------------------------------------------------------------------------- /warn/warn.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | 4 | logger = logging.getLogger("Modmail") 5 | 6 | import discord 7 | import typing 8 | from discord.ext import commands 9 | 10 | from core import checks 11 | from core.models import PermissionLevel 12 | 13 | 14 | class WarnPlugin(commands.Cog): 15 | """ 16 | Moderate ya server using modmail pog 17 | """ 18 | 19 | def __init__(self, bot): 20 | self.bot = bot 21 | self.db = bot.plugin_db.get_partition(self) 22 | 23 | @commands.group(invoke_without_command=True) 24 | @commands.guild_only() 25 | @checks.has_permissions(PermissionLevel.ADMIN) 26 | async def moderation(self, ctx: commands.Context): 27 | """ 28 | Settings and stuff 29 | """ 30 | await ctx.send_help(ctx.command) 31 | return 32 | 33 | @moderation.command() 34 | @checks.has_permissions(PermissionLevel.ADMIN) 35 | async def channel(self, ctx: commands.Context, channel: discord.TextChannel): 36 | """ 37 | Set the log channel for moderation actions. 38 | """ 39 | 40 | await self.db.find_one_and_update( 41 | {"_id": "config"}, {"$set": {"channel": channel.id}}, upsert=True 42 | ) 43 | 44 | await ctx.send("Done!") 45 | return 46 | 47 | @commands.command() 48 | @checks.has_permissions(PermissionLevel.MODERATOR) 49 | async def warn(self, ctx, member: discord.Member, *, reason: str): 50 | """Warn a member. 51 | Usage: 52 | {prefix}warn @member Spoilers 53 | """ 54 | 55 | if member.bot: 56 | return await ctx.send("Bots can't be warned.") 57 | 58 | channel_config = await self.db.find_one({"_id": "config"}) 59 | 60 | if channel_config is None: 61 | return await ctx.send("There's no configured log channel.") 62 | else: 63 | channel = ctx.guild.get_channel(int(channel_config["channel"])) 64 | 65 | if channel is None: 66 | return 67 | 68 | config = await self.db.find_one({"_id": "warns"}) 69 | 70 | if config is None: 71 | config = await self.db.insert_one({"_id": "warns"}) 72 | 73 | try: 74 | userwarns = config[str(member.id)] 75 | except KeyError: 76 | userwarns = config[str(member.id)] = [] 77 | 78 | if userwarns is None: 79 | userw = [] 80 | else: 81 | userw = userwarns.copy() 82 | 83 | userw.append({"reason": reason, "mod": ctx.author.id}) 84 | 85 | await self.db.find_one_and_update( 86 | {"_id": "warns"}, {"$set": {str(member.id): userw}}, upsert=True 87 | ) 88 | 89 | await ctx.send(f"Successfully warned **{member}**\n`{reason}`") 90 | 91 | await channel.send( 92 | embed=await self.generateWarnEmbed( 93 | str(member.id), str(ctx.author.id), len(userw), reason 94 | ) 95 | ) 96 | del userw 97 | return 98 | 99 | @commands.command() 100 | @checks.has_permissions(PermissionLevel.MODERATOR) 101 | async def pardon(self, ctx, member: discord.Member, *, reason: str): 102 | """Remove all warnings of a member. 103 | Usage: 104 | {prefix}pardon @member Nice guy 105 | """ 106 | 107 | if member.bot: 108 | return await ctx.send("Bots can't be warned, so they can't be pardoned.") 109 | 110 | channel_config = await self.db.find_one({"_id": "config"}) 111 | 112 | if channel_config is None: 113 | return await ctx.send("There's no configured log channel.") 114 | else: 115 | channel = ctx.guild.get_channel(int(channel_config["channel"])) 116 | 117 | if channel is None: 118 | return 119 | 120 | config = await self.db.find_one({"_id": "warns"}) 121 | 122 | if config is None: 123 | return 124 | 125 | try: 126 | userwarns = config[str(member.id)] 127 | except KeyError: 128 | return await ctx.send(f"{member} doesn't have any warnings.") 129 | 130 | if userwarns is None: 131 | await ctx.send(f"{member} doesn't have any warnings.") 132 | 133 | await self.db.find_one_and_update( 134 | {"_id": "warns"}, {"$set": {str(member.id): []}} 135 | ) 136 | 137 | await ctx.send(f"Successfully pardoned **{member}**\n`{reason}`") 138 | 139 | embed = discord.Embed(color=discord.Color.blue()) 140 | 141 | embed.set_author( 142 | name=f"Pardon | {member}", 143 | icon_url=member.avatar_url, 144 | ) 145 | embed.add_field(name="User", value=f"{member}") 146 | embed.add_field( 147 | name="Moderator", 148 | value=f"<@{ctx.author.id}> - `{ctx.author}`", 149 | ) 150 | embed.add_field(name="Reason", value=reason) 151 | embed.add_field(name="Total Warnings", value="0") 152 | 153 | return await channel.send(embed=embed) 154 | 155 | async def generateWarnEmbed(self, memberid, modid, warning, reason): 156 | member: discord.User = await self.bot.fetch_user(int(memberid)) 157 | mod: discord.User = await self.bot.fetch_user(int(modid)) 158 | 159 | embed = discord.Embed(color=discord.Color.red()) 160 | 161 | embed.set_author( 162 | name=f"Warn | {member}", 163 | icon_url=member.avatar_url, 164 | ) 165 | embed.add_field(name="User", value=f"{member}") 166 | embed.add_field(name="Moderator", value=f"<@{modid}>` - ({mod})`") 167 | embed.add_field(name="Reason", value=reason) 168 | embed.add_field(name="Total Warnings", value=warning) 169 | return embed 170 | 171 | 172 | def setup(bot): 173 | bot.add_cog(WarnPlugin(bot)) 174 | --------------------------------------------------------------------------------