├── .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 |

3 |
A repository to store Modmail Plugins
4 |
5 |
6 |
7 |
8 |
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) | [](#) | [](#) |
23 | | Announcement | Easily make announcements | `plugins add officialpiyush/modmail-plugins/announcement` | [Source](https://github.com/officialpiyush/modmail-plugins/tree/master/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) | [](#) | [](#) |
25 | | Hastebin | Upload text to hastebin | `plugins add officialpiyush/modmail-plugins/hastebin` | [Source](https://github.com/officialpiyush/modmail-plugins/tree/master/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) | [](#) | [](#) |
27 | | Translator | Translate messages | `plugins add officialpiyush/modmail-plugins/translator` | [Source](https://github.com/officialpiyush/modmail-plugins/tree/master/translator) | [](#) | [](#) |
28 |
29 |
30 | # Contributors
31 |
32 | [](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 |

3 |
4 |
A plugin to backup modmail database.
5 |
6 |
7 |
8 |
9 |
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 |

3 |
4 |
A plugin to upload text to hastebin.
5 |
6 |
7 |
8 |
9 |
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 |

3 |
4 |
A plugin For ModMail Bot to force it to leave a specified server.
5 |
6 |
7 |
8 |
9 |
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 |

3 |
4 |
A plugin to manage tags, etc.
5 |
6 |
7 |
8 |
9 |
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 |

3 |
4 |
A Modmail plugin that (auto) translates messages
5 |
6 |
7 |
8 |
9 |
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 |
--------------------------------------------------------------------------------