class Account(rubbercog.Rubbercog): """Manage the bot account""" def __init__(self, bot): super().__init__(bot) self.text = CogText("account") @commands.is_owner() @commands.group(name="set") async def set(self, ctx): """Set attributes""" await utils.send_help(ctx) @set.command(name="name", aliases=["username"]) async def set_name(self, ctx, *, name: str): """Set bot name""" await self.bot.user.edit(username=name) await ctx.send(self.text.get("name", "ok", name=name)) @set.command(name="avatar", aliases=["image"]) async def set_avatar(self, ctx, *, path: str): """Set bot avatar path: path to image file, starting in `data/` directory """ if ".." in path: return await self.output.error(ctx, self.text.get("avatar", "invalid")) try: with open("data/" + path, "rb") as image: with ctx.typing(): await self.bot.user.edit(avatar=image.read()) except FileNotFoundError: return await self.output.error( ctx, self.text.get("avatar", "not_found")) await ctx.send(self.text.get("avatar", "ok"))
class Warden(rubbercog.Rubbercog): """A cog for database lookups""" # TODO Implement template matching to prevent false positives def __init__(self, bot): super().__init__(bot) self.config = CogConfig("warden") self.text = CogText("warden") self.limit_full = 3 self.limit_hard = 7 self.limit_soft = 14 def doCheckRepost(self, message: discord.Message): return ( message.channel.id in self.config.get("deduplication channels") and message.attachments is not None and len(message.attachments) > 0 and not message.author.bot ) @commands.Cog.listener() async def on_message(self, message: discord.Message): if message.author.bot: return # repost check - test for duplicates if self.doCheckRepost(message): if len(message.attachments) > 0: await self.checkDuplicate(message) # gif check for link in self.config.get("penalty strings"): if link in message.content: penalty = self.config.get("penalty value") await message.channel.send( self.text.get("gif warning", mention=message.author, value=penalty) ) repo_k.update_karma_get(message.author, penalty) await utils.delete(message) await self.console.debug(message, f"Removed message linking to {link}") break @commands.Cog.listener() async def on_message_delete(self, message: discord.Message): if self.doCheckRepost(message): i = repo_i.deleteByMessage(message.id) await self.console.debug(self, f"Removed {i} dhash(es) from database") # try to detect repost embed messages = await message.channel.history( after=message, limit=10, oldest_first=True ).flatten() for m in messages: if not m.author.bot: continue if len(m.embeds) != 1 or type(m.embeds[0].footer.text) != str: continue if str(message.id) != m.embeds[0].footer.text.split(" | ")[1]: continue try: await m.delete() except Exception as e: await self.console.error(message, "Could not delete report embed.", e) break @commands.Cog.listener() async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): """Handle 'This is a repost' embed""" if payload.channel_id not in self.config.get("deduplication channels"): return if payload.member.bot: return if not hasattr(payload.emoji, "name") or payload.emoji.name not in ("🆗", "❎"): return try: embed_message = ( await self.getGuild() .get_channel(payload.channel_id) .fetch_message(payload.message_id) ) except Exception as e: return await self.console.debug(self, "Reaction's message not found", e) if not embed_message or not embed_message.author.bot: return try: repost_message = embed_message.embeds[0].footer.text.split(" | ")[1] repost_message = await embed_message.channel.fetch_message(int(repost_message)) except Exception: return await self.console.debug(embed_message, "Could not find repost's original.") for r in embed_message.reactions: if r.emoji not in ("🆗", "❎"): continue if ( payload.emoji.name == "❎" and r.emoji == "❎" and r.count > self.config.get("not duplicate limit") ): # remove bot's reactions, it is not a repost try: await repost_message.remove_reaction("♻️", self.bot.user) await repost_message.remove_reaction("🤷🏻", self.bot.user) await repost_message.remove_reaction("🤔", self.bot.user) except Exception as e: await self.console.error(embed_message, "Could not remove bot's reaction", e) return await embed_message.delete() elif payload.emoji.name == "🆗" and r.emoji == "🆗": # get original author's ID repost_author_id = embed_message.embeds[0].footer.text.split(" | ")[0] if str(repost_message.author.id) != repost_author_id: return await embed_message.remove_reaction("🆗", embed_message.author) # contract the embed info_field = embed_message.embeds[0].fields[0] embed = discord.Embed() embed.add_field(name=info_field.name, value=info_field.value) embed.set_footer(text=embed_message.embeds[0].footer.text) try: await embed_message.edit(embed=embed) await embed_message.clear_reactions() except discord.NotFound: pass async def saveMessageHashes(self, message: discord.Message): for f in message.attachments: if f.size > self.config.get("max_size") * 1024: continue fp = BytesIO() await f.save(fp) try: i = Image.open(fp) except OSError: # not an image continue h = dhash.dhash_int(i) # fmt: off repo_i.add_image( channel_id=message.channel.id, message_id=message.id, attachment_id=f.id, dhash=str(hex(h)), ) # fmt: on yield h @commands.group() @commands.check(acl.check) async def scan(self, ctx): """Scan for reposts""" if ctx.invoked_subcommand is None: await ctx.send_help(ctx.invoked_with) @commands.check(acl.check) @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) @commands.bot_has_permissions(read_message_history=True) @scan.command(name="history") async def scan_history(self, ctx, limit): """Scan current channel for images and save them as hashes limit: [all | <int>] """ # parse parameter if limit == "all": limit = None else: try: limit = int(limit) if limit < 1: raise ValueError except ValueError: raise commands.BadArgument("Expected 'all' or positive integer") messages = await ctx.channel.history(limit=limit).flatten() # fmt: off title = "**INITIATING...**\n\nLoaded {} messages" await asyncio.sleep(0.5) template = ( "**SCANNING IN PROGRESS**\n\n" "Processed **{}** of **{}** messages ({:.1f} %)\n" "Computed **{}** hashes" ) # fmt: on msg = await ctx.send(title.format(len(messages))) ctr_nofile = 0 ctr_hashes = 0 i = 0 now = time.time() for i, message in enumerate(messages): # update info on every 10th message if i % 20 == 0: # fmt: off await msg.edit(content=template.format( i, len(messages), (i / len(messages) * 100), ctr_hashes )) # fmt: on if len(message.attachments) == 0: ctr_nofile += 1 continue hashes = [x async for x in self.saveMessageHashes(message)] ctr_hashes += len(hashes) # fmt: off await msg.edit(content= "**SCAN COMPLETE**\n\n" f"Processed **{len(messages)}** messages.\n" f"Computed **{ctr_hashes}** hashes in {(time.time() - now):.1f} seconds." ) # fmt: on @commands.check(acl.check) @scan.command(name="compare", aliases=["messages"]) async def scan_compare(self, ctx, first: int, second: int): """Scan two messages and report comparison result Arguments --------- first: Message ID second: Message ID """ hashes1 = repo_i.get_by_message(first) hashes2 = repo_i.get_by_message(second) if len(hashes1) == 0: return await ctx.send(self.text.get("comparison", "not_found", message_id=str(first))) if len(hashes2) == 0: return await ctx.send(self.text.get("comparison", "not_found", message_id=str(second))) text = [] text.append(self.text.get("comparison", "header", message_id=str(first))) for h in hashes1: text.append(self.text.get("comparison", "line", hash=str(h.dhash)[2:])) text.append("") text.append(self.text.get("comparison", "header", message_id=str(second))) for h in hashes2: text.append(self.text.get("comparison", "line", hash=str(h.dhash))) if len(hashes1) == 1 or len(hashes2) == 1: hash1 = int(hashes1[0].dhash, 16) hash2 = int(hashes2[0].dhash, 16) hamming = dhash.get_num_bits_different(hash1, hash2) prob = "{:.1f}".format((1 - hamming / 128) * 100) text.append("") text.append(self.text.get("comparison", "footer", percent=str(prob), bits=str(hamming))) await ctx.send("\n".join(text)) async def checkDuplicate(self, message: discord.Message): """Check if uploaded files are known""" hashes = [x async for x in self.saveMessageHashes(message)] if len(message.attachments) > 0 and len(hashes) == 0: await message.add_reaction("▶") await asyncio.sleep(2) await message.remove_reaction("▶", self.bot.user) duplicates = {} posts_all = None for image_hash in hashes: # try to look up hash directly posts_full = repo_i.getHash(str(hex(image_hash))) if len(posts_full) > 0: # full match found for post in posts_full: # skip current message if post.message_id == message.id: continue # add to duplicates duplicates[post] = 0 await self.console.debug(message, "Full dhash match") break # move on to the next hash continue # full match not found, iterate over whole database if posts_all is None: posts_all = repo_i.getAll() hamming_min = 128 duplicate = None for post in posts_all: # skip current message if post.message_id == message.id: continue # do the comparison post_hash = int(post.dhash, 16) hamming = dhash.get_num_bits_different(image_hash, post_hash) if hamming < hamming_min: duplicate = post hamming_min = hamming duplicates[duplicate] = hamming_min await self.console.debug(message, f"Closest Hamming distance: {hamming_min}/128 bits") for image_hash, hamming_distance in duplicates.items(): if hamming_distance <= self.limit_soft: await self._announceDuplicate(message, image_hash, hamming_distance) async def _announceDuplicate(self, message: discord.Message, original: object, hamming: int): """Send message that a post is a original original: object hamming: Hamming distance between the image and closest database entry """ if hamming <= self.limit_full: t = "**♻️ To je repost!**" await message.add_reaction("♻️") elif hamming <= self.limit_hard: t = "**♻️ To je asi repost**" await message.add_reaction("🤔") else: t = "To je možná repost" await message.add_reaction("🤷🏻") prob = "{:.1f} %".format((1 - hamming / 128) * 100) timestamp = utils.id_to_datetime(original.attachment_id).strftime("%Y-%m-%d %H:%M:%S") src_chan = self.getGuild().get_channel(original.channel_id) try: src_post = await src_chan.fetch_message(original.message_id) link = src_post.jump_url author = discord.utils.escape_markdown(src_post.author.display_name) except discord.errors.NotFound: link = "404 " + emote.sad author = "_??? (404)_" d = self.text.get( "repost description", name=discord.utils.escape_markdown(message.author.display_name), value=prob, ) embed = discord.Embed(title=t, color=config.color, description=d, url=message.jump_url) embed.add_field(name=f"**{author}**, {timestamp}", value=link, inline=False) embed.add_field( name=self.text.get("repost title"), value="_" + self.text.get("repost content", limit=self.config.get("not duplicate limit")) + "_", ) embed.set_footer(text=f"{message.author.id} | {message.id}") m = await message.channel.send(embed=embed) await m.add_reaction("❎") await m.add_reaction("🆗")
class Librarian(rubbercog.Rubbercog): """Knowledge and information based commands""" # TODO Move czech strings to text.default.json def __init__(self, bot): super().__init__(bot) self.config = CogConfig("librarian") self.text = CogText("librarian") @commands.command(aliases=["svátek"]) async def svatek(self, ctx): url = f"http://svatky.adresa.info/json?date={date.today().strftime('%d%m')}" res = await utils.fetch_json(url) names = [] for i in res: names.append(i["name"]) if len(names): await ctx.send( self.text.get("nameday", "cs", name=", ".join(names))) else: await ctx.send(self.text.get("nameday", "cs0")) @commands.command(aliases=["sviatok"]) async def meniny(self, ctx): url = f"http://svatky.adresa.info/json?lang=sk&date={date.today().strftime('%d%m')}" res = await utils.fetch_json(url) names = [] for i in res: names.append(i["name"]) if len(names): await ctx.send( self.text.get("nameday", "sk", name=", ".join(names))) else: await ctx.send(self.text.get("nameday", "sk0")) @commands.command(aliases=["tyden", "týden", "tyzden", "týždeň"]) async def week(self, ctx: commands.Context): """See if the current week is odd or even""" cal_week = date.today().isocalendar()[1] stud_week = cal_week - self.config.get("starting_week") + 1 even, odd = self.text.get("week", "even"), self.text.get("week", "odd") cal_type = even if cal_week % 2 == 0 else odd stud_type = even if stud_week % 2 == 0 else odd embed = self.embed(ctx=ctx) embed.add_field( name=self.text.get("week", "calendar"), value="{} ({})".format(cal_type, cal_week), ) if 0 < stud_week <= self.config.get("total_weeks"): embed.add_field( name=self.text.get("week", "study"), value=str(stud_week), ) await ctx.send(embed=embed) await utils.delete(ctx) await utils.room_check(ctx) @commands.command(aliases=["počasí", "pocasi", "počasie", "pocasie"]) async def weather(self, ctx, *, place: str = "Brno"): token = self.config.get("weather_token") place = place[:100] if "&" in place: return await ctx.send(self.text.get("weather", "place_not_found")) url = ("https://api.openweathermap.org/data/2.5/weather?q=" + place + "&units=metric&lang=cz&appid=" + token) res = await utils.fetch_json(url) """ Example response { "coord":{ "lon":16.61, "lat":49.2 }, "weather":[ { "id":800, "temp_maixn":"Clear", "description":"jasno", "icon":"01d" } ], "base":"stations", "main":{ "temp":21.98, "feels_like":19.72, "temp_min":20.56, "temp_max":23, "pressure":1013, "humidity":53 }, "visibility":10000, "wind":{ "speed":4.1, "deg":50 }, "clouds":{ "all":0 }, "dt":1595529518, "sys":{ "type":1, "id":6851, "country":"CZ", "sunrise":1595474051, "sunset":1595529934 }, "timezone":7200, "id":3078610, "name":"Brno", "cod":200 } """ if str(res["cod"]) == "404": return await ctx.send(self.text.get("weather", "place_not_found")) elif str(res["cod"]) == "401": return await ctx.send(self.text.get("weather", "token")) elif str(res["cod"]) != "200": return await ctx.send( self.text.get("weather", "place_error", message=res["message"])) title = res["weather"][0]["description"] description = (self.text.get("weather", "description", name=res["name"], country=res["sys"]["country"]) if "country" in res["sys"] else self.text.get( "weather", "description_short", name=res["name"])) if description.endswith("CZ"): description = description[:-4] embed = self.embed(ctx=ctx, title=title[0].upper() + title[1:], description=description) embed.set_thumbnail(url="https://openweathermap.org/img/w/{}.png". format(res["weather"][0]["icon"])) embed.add_field( name=self.text.get("weather", "temperature"), value=self.text.get( "weather", "temperature_value", real=round(res["main"]["temp"], 1), feel=round(res["main"]["feels_like"], 1), ) + "\n" + self.text.get( "weather", "temperature_minmax", min=round(res["main"]["temp_min"], 1), max=round(res["main"]["temp_max"], 1), ), inline=False, ) embed.add_field( name=self.text.get("weather", "humidity"), value=str(res["main"]["humidity"]) + " %", ) embed.add_field( name=self.text.get("weather", "clouds"), value=(str(res["clouds"]["all"]) + " %"), ) if "visibility" in res: if res["visibility"] == 10000: value = self.text.get("weather", "visibility_max") else: value = f"{res['visibility']/1000} km" embed.add_field( name=self.text.get("weather", "visibility"), value=value, ) embed.add_field(name=self.text.get("weather", "wind"), value=f"{res['wind']['speed']} m/s") await utils.send(ctx, embed=embed) await utils.room_check(ctx) @commands.command(aliases=["b64"]) async def base64(self, ctx, direction: str, *, data: str): """Get base64 data direction: [encode, e, -e; decode, d, -d] text: string (under 1000 characters) """ if data is None or not len(data): return await utils.send_help(ctx) data = data[:1000] if direction in ("encode", "e", "-e"): direction = "encode" result = base64.b64encode(data.encode("utf-8")).decode("utf-8") elif direction in ("decode", "d", "-d"): direction = "decode" try: result = base64.b64decode(data.encode("utf-8")).decode("utf-8") except Exception as e: return await ctx.send(f"> {e}") else: return await utils.send_help(ctx) quote = self.sanitise(data[:50]) + ("…" if len(data) > 50 else "") await ctx.send(f"**base64 {direction}** ({quote}):\n> ```{result}```") await utils.room_check(ctx) @commands.command() async def hashlist(self, ctx): """Get list of available hash functions""" result = "**hashlib**\n" result += "> " + " ".join(sorted(hashlib.algorithms_available)) await ctx.send(result) @commands.command() async def hash(self, ctx, fn: str, *, data: str): """Get hash function result Run hashlist command to see available algorithms """ if fn in hashlib.algorithms_available: result = hashlib.new(fn, data.encode("utf-8")).hexdigest() else: return await ctx.send(self.text.get("invalid_hash")) quote = self.sanitise(data[:50]) + ("…" if len(data) > 50 else "") await ctx.send(f"**{fn}** ({quote}):\n> ```{result}```") @commands.command(aliases=["maclookup"]) async def macaddress(self, ctx, mac: str): """Get information about MAC address""" apikey = self.config.get("maclookup_token") if apikey == 0: return await self.output.error( ctx, self.text.get("maclookup", "no_token"), ) if "&" in mac or "?" in mac: return await self.output.error( ctx, self.text.get("maclookup", "bad_mac", mention=ctx.author.mention), ) url = f"https://api.maclookup.app/v2/macs/{mac}?format=json&apiKey={apikey}" res = await utils.fetch_json(url) if res["success"] is False: embed = self.embed( ctx=ctx, title=self.text.get("maclookup", "error", errcode=res["errorCode"]), description=res["error"], footer="maclookup.app", ) return await ctx.send(embed=embed) if res["found"] is False: embed = self.embed( ctx=ctx, title=self.text.get("maclookup", "error", errcode="404"), description=self.text.get("maclookup", "not_found"), footer="maclookup.app", ) return await ctx.send(embed=embed) embed = self.embed(ctx=ctx, title=res["macPrefix"], footer="maclookup.app") embed.add_field( name=self.text.get("maclookup", "company"), value=res["company"], inline=False, ) embed.add_field(name=self.text.get("maclookup", "country"), value=res["country"]) block = f"`{res['blockStart']}`" if res["blockStart"] != res["blockEnd"]: block += f"\n`{res['blockEnd']}`" embed.add_field(name=self.text.get("maclookup", "block"), value=f'`{res["blockStart"]}`') await ctx.send(embed=embed) @commands.cooldown(rate=2, per=20, type=commands.BucketType.user) # The API has limit of 45 requests per minute @commands.cooldown(rate=45, per=55, type=commands.BucketType.default) @commands.command(aliases=["iplookup"]) async def ipaddress(self, ctx, query: str): """Get information about an IP address or a domain name""" if "&" in query or "?" in query or not len(query): return await self.output.error( ctx, self.text.get("iplookup", "bad_query", mention=ctx.author.mention), ) url = ( f"http://ip-api.com/json/{query}" "?fields=query,status,message,country,regionName,city,lat,lon,isp,org" ) res = await utils.fetch_json(url) # TODO The API states that we should be listening for the `X-Rl` header. # If it is `0`, we must stop for `X-ttl` seconds. # https://ip-api.com/docs/api:json if res["status"] == "fail": embed = self.embed( ctx=ctx, title=self.text.get("iplookup", "error"), description="`" + res["message"] + "`", footer="ip-api.com", ) return await ctx.send(embed=embed) embed = self.embed(ctx=ctx, title=res["query"], footer="ip-api.com") embed.add_field( name=res["city"], value=f"{res['regionName']}, {res['country']}", inline=False, ) embed.add_field( name=self.text.get("iplookup", "geo"), value=f"{res['lon']}, {res['lat']}", ) embed.add_field( name=self.text.get("iplookup", "org"), value=res["org"], ) embed.add_field( name=self.text.get("iplookup", "isp"), value=res["isp"], ) await ctx.send(embed=embed)
class Shop(rubbercog.Rubbercog): """Make use of your karma""" def __init__(self, bot): super().__init__(bot) self.config = CogConfig("shop") self.text = CogText("shop") @commands.command() async def shop(self, ctx): """Display prices for various services""" embed = self.embed( ctx=ctx, title=self.text.get("info", "title"), description=self.text.get("info", "description"), ) embed.add_field( name=self.text.get("info", "set"), value=self.config.get("set"), ) embed.add_field( name=self.text.get("info", "reset"), value=self.config.get("reset"), ) await ctx.send(embed=embed) await utils.room_check(ctx) @commands.bot_has_permissions(manage_nicknames=True) @commands.check(acl.check) @commands.group(name="nickname") async def nickname(self, ctx): """Change your nickname""" await utils.send_help(ctx) @commands.cooldown(rate=5, per=60, type=commands.BucketType.member) @commands.check(acl.check) @nickname.command(name="set") async def nickname_set(self, ctx, *, nick: str): """Set the nickname. Use command `shop` to see prices Attributes ---------- nick: Your new nickname """ # stop if user does not have nickname set if ctx.author.nick is None and nick is None or not len(nick): return await ctx.send(self.text.get("no_nick", mention=ctx.author.mention)) # check if user has karma if self.get_user_karma(ctx.author.id) < self.config.get("set"): return await ctx.send( self.text.get( "not_enough_karma", mention=ctx.author.mention, ) ) for char in ("@", "#", "`", "'", '"'): if char in nick: return await ctx.send(self.text.get("bad_character", mention=ctx.author.mention)) # set nickname try: await ctx.author.edit(nick=nick, reason="Nickname purchase") except discord.Forbidden: return await ctx.send(self.text.get("higher_role")) repo_k.updateMemberKarma(ctx.author.id, -1 * self.price_nick) await ctx.send( self.text.get( "new_nick", mention=ctx.author.mention, nick=discord.utils.escape_markdown(nick), value=self.price_nick, ) ) await self.event.user(ctx, f"Nickname changed to {nick}.") @commands.cooldown(rate=1, per=60, type=commands.BucketType.member) @commands.check(acl.check) @nickname.command(name="unset") async def nickname_unset(self, ctx): """Unset the nickname""" if ctx.author.nick is None: return await ctx.send(self.text.get("no_nick", mention=ctx.author.mention)) # check if user has karma if self.get_user_karma(ctx.author.id) < self.config.get("reset"): return await ctx.send( self.text.get( "not_enough_karma", mention=ctx.author.mention, ) ) nick = ctx.author.nick await ctx.author.edit(nick=None, reason="Nickname reset") await ctx.send( self.text.get( "nick_removed", mention=ctx.author.mention, nick=self.sanitise(nick), ) ) await self.event.user(ctx, "Nickname reset.") ## ## Logic ## def get_user_karma(self, user_id: int) -> int: return getattr(repo_k.getMember(user_id), "karma", 0)
class Meme(rubbercog.Rubbercog): """Interact with users""" def __init__(self, bot): super().__init__(bot) self.text = CogText("meme") self.config = CogConfig("meme") self.fishing_pool = self.config.get("_fishing") @commands.guild_only() @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user) @commands.command() async def hug(self, ctx, *, target: Union[discord.Member, discord.Role] = None): """Hug someone! target: Discord user or role. If none, the bot will hug you. """ if target is None: hugger = self.bot.user hugged = ctx.author else: hugger = ctx.author hugged = target if type(hugged) == discord.Role: repo_i.add(ctx.guild.id, "hug", hugger.id, None) else: repo_i.add(ctx.guild.id, "hug", hugger.id, hugged.id) await ctx.send(emote.hug_right + (" **" + hugged.display_name + "**" if type(target) == discord.Member else " ***" + hugged.name + "***")) @commands.guild_only() @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user) @commands.command() async def whip(self, ctx, *, user: discord.Member = None): """Whip someone""" if user is None: whipper = self.bot.user whipped = ctx.author else: whipper = ctx.author whipped = user repo_i.add(ctx.guild.id, "whip", whipper.id, whipped.id) async with ctx.typing(): url = whipped.avatar_url_as(format="jpg") response = requests.get(url) avatar = Image.open(BytesIO(response.content)) frames = self.get_whip_frames(avatar) with BytesIO() as image_binary: frames[0].save( image_binary, format="GIF", save_all=True, append_images=frames[1:], duration=30, loop=0, transparency=0, disposal=2, optimize=False, ) image_binary.seek(0) await ctx.send( file=discord.File(fp=image_binary, filename="whip.gif")) return @commands.guild_only() @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user) @commands.command() async def spank(self, ctx, *, user: discord.Member = None): """Spank someone""" if user is None: spanker = self.bot.user spanked = ctx.author else: spanker = ctx.author spanked = user repo_i.add(ctx.guild.id, "spank", spanker.id, spanked.id) async with ctx.typing(): url = spanked.avatar_url_as(format="jpg") response = requests.get(url) avatar = Image.open(BytesIO(response.content)) frames = self.get_spank_frames(avatar) with BytesIO() as image_binary: frames[0].save( image_binary, format="GIF", save_all=True, append_images=frames[1:], duration=30, loop=0, transparency=0, disposal=2, optimize=False, ) image_binary.seek(0) await ctx.send( file=discord.File(fp=image_binary, filename="spank.gif")) @commands.guild_only() @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user) @commands.command() async def pet(self, ctx, *, member: discord.Member = None): """Pet someone! member: Discord user. If none, the bot will pet you. """ if member is None: petter = self.bot.user petted = ctx.author else: petter = ctx.author petted = member repo_i.add(ctx.guild.id, "pet", petter.id, petted.id) async with ctx.typing(): url = petted.avatar_url_as(format="jpg") response = requests.get(url) avatar = Image.open(BytesIO(response.content)) frames = self.get_pet_frames(avatar) with BytesIO() as image_binary: frames[0].save( image_binary, format="GIF", save_all=True, append_images=frames[1:], duration=40, loop=0, transparency=0, disposal=2, optimize=False, ) image_binary.seek(0) await ctx.send( file=discord.File(fp=image_binary, filename="pet.gif")) return # this is a more intensive solution that creates non-transparent # background without it being glitched with BytesIO() as image_binary: image_utils.save_gif(frames, 30, image_binary) image_binary.seek(0) await ctx.send( file=discord.File(fp=image_binary, filename="pet.gif")) @commands.guild_only() @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user) @commands.command() async def hyperpet(self, ctx, *, member: discord.Member = None): """Pet someone really hard member: Discord user. If none, the bot will hyperpet you. """ if member is None: petter = self.bot.user petted = ctx.author else: petter = ctx.author petted = member repo_i.add(ctx.guild.id, "hyperpet", petter.id, petted.id) async with ctx.typing(): url = petted.avatar_url_as(format="jpg") response = requests.get(url) avatar = Image.open(BytesIO(response.content)) frames = self.get_hyperpet_frames(avatar) with BytesIO() as image_binary: frames[0].save( image_binary, format="GIF", save_all=True, append_images=frames[1:], duration=30, loop=0, transparency=0, disposal=2, optimize=False, ) image_binary.seek(0) await ctx.send( file=discord.File(fp=image_binary, filename="hyperpet.gif") ) @commands.guild_only() @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user) @commands.command() async def bonk(self, ctx, *, member: discord.Member = None): """Bonk someone member: Discord user. If none, the bot will bonk you. """ if member is None: bonker = self.bot.user bonked = ctx.author else: bonker = ctx.author bonked = member repo_i.add(ctx.guild.id, "bonk", bonker.id, bonked.id) async with ctx.typing(): url = bonked.avatar_url_as(format="jpg") response = requests.get(url) avatar = Image.open(BytesIO(response.content)) frames = self.get_bonk_frames(avatar) with BytesIO() as image_binary: frames[0].save( image_binary, format="GIF", save_all=True, append_images=frames[1:], duration=30, loop=0, transparency=0, disposal=2, optimize=False, ) image_binary.seek(0) await ctx.send( file=discord.File(fp=image_binary, filename="bonk.gif")) @commands.guild_only() @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user) @commands.command() async def slap(self, ctx, *, member: discord.Member = None): """Slap someone! member: Discord user. If none, the bot will slap you. """ if member is None: slapper = self.bot.user slapped = ctx.author else: slapper = ctx.author slapped = member options = ["つ", "づ", "ノ"] repo_i.add(ctx.guild.id, "slap", slapper.id, slapped.id) await ctx.send("**{}**{} {}".format( self.sanitise(slapper.display_name), random.choice(options), self.sanitise(slapped.display_name), )) @commands.guild_only() @commands.cooldown(rate=1, per=5, type=commands.BucketType.user) @commands.command() async def relations(self, ctx, *, user: discord.User = None): """Get your information about hugs, pets, ...""" if user is None: user = ctx.author embed = self.embed( ctx=ctx, description=f"**{self.sanitise(user.display_name)}**\n" f"{self.text.get('relations_help')}", ) for action in ("hug", "pet", "hyperpet", "slap", "spank", "whip", "bonk"): lookup = repo_i.get_user_action(user.id, ctx.guild.id, action) if lookup[0] == 0 and lookup[1] == 0: continue value = self.text.get("value", gave=lookup[0], got=lookup[1]) embed.add_field(name=f"{config.prefix}{action}", value=value) await ctx.send(embed=embed) await utils.room_check(ctx) @commands.cooldown(rate=5, per=120, type=commands.BucketType.user) @commands.command(aliases=["owo"]) async def uwu(self, ctx, *, message: str = None): """UWUize message""" if message is None: text = "OwO!" else: text = self.sanitise(self.uwuize(message), limit=1900, markdown=True) await ctx.send(f"**{self.sanitise(ctx.author.display_name)}**\n>>> " + text) await utils.delete(ctx.message) @commands.cooldown(rate=5, per=120, type=commands.BucketType.user) @commands.command(aliases=["rcase", "randomise"]) async def randomcase(self, ctx, *, message: str = None): """raNdOMisE cAsInG""" if message is None: text = "O.o" else: text = "" for letter in message: if letter.isalpha(): text += letter.upper() if random.choice( (True, False)) else letter.lower() else: text += letter text = self.sanitise(text[:1900], markdown=True) await ctx.send(f"**{self.sanitise(ctx.author.display_name)}**\n>>> " + text) await utils.delete(ctx.message) @commands.cooldown(rate=3, per=10, type=commands.BucketType.user) @commands.command() async def fish(self, ctx): """Go fishing!""" roll = random.uniform(0, 1) options = None for probabilty, harvest in self.fishing_pool.items(): if roll >= float(probabilty): options = harvest break else: return await ctx.send( self.text.get("fishing_fail", mention=ctx.author.mention)) await ctx.send(random.choice(options)) ## ## Logic ## @staticmethod def uwuize(string: str) -> str: # Adapted from https://github.com/PhasecoreX/PCXCogs/blob/master/uwu/uwu.py result = [] def uwuize_word(string: str) -> str: try: if string.lower()[0] == "m" and len(string) > 2: w = "W" if string[1].isupper() else "w" string = string[0] + w + string[1:] except Exception: # this is how we handle emojis pass string = string.replace("r", "w").replace("R", "W") string = string.replace("ř", "w").replace("Ř", "W") string = string.replace("l", "w").replace("L", "W") string = string.replace("?", "?" * random.randint(1, 3)) string = string.replace("'", ";" * random.randint(1, 3)) if string[-1] == ",": string = string[:-1] + "." * random.randint(2, 3) return string result = " ".join( [uwuize_word(s) for s in string.split(" ") if len(s)]) if result[-1] == "?": result += " UwU" if result[-1] == "!": result += " OwO" if result[-1] == ".": result = result[:-1] + "," * random.randint(2, 4) return result @staticmethod def round_image(frame_avatar: Image.Image) -> Image.Image: """Convert square avatar to circle""" frame_mask = Image.new("1", frame_avatar.size, 0) draw = ImageDraw.Draw(frame_mask) draw.ellipse((0, 0) + frame_avatar.size, fill=255) frame_avatar.putalpha(frame_mask) return frame_avatar @staticmethod def get_pet_frames(avatar: Image.Image) -> List[Image.Image]: """Get frames for the pet""" frames = [] width, height = 148, 148 vertical_offset = (0, 0, 0, 0, 1, 2, 3, 4, 5, 4, 3, 2, 2, 1, 0) frame_avatar = image_utils.round_image(avatar.resize((100, 100))) for i in range(14): img = "%02d" % (i + 1) frame = Image.new("RGBA", (width, height), (54, 57, 63, 1)) hand = Image.open(f"data/meme/pet/{img}.png") frame.paste(frame_avatar, (35, 25 + vertical_offset[i]), frame_avatar) frame.paste(hand, (10, 5), hand) frames.append(frame) return frames @staticmethod def get_hyperpet_frames(avatar: Image.Image) -> List[Image.Image]: """Get frames for the hyperpet""" frames = [] width, height = 148, 148 vertical_offset = (0, 1, 2, 3, 1, 0) avatar = image_utils.round_image(avatar.resize((100, 100))) avatar_pixels = np.array(avatar) git_hash = int(utils.git_get_hash(), 16) for i in range(6): deform_hue = git_hash % 100**(i + 1) // 100**i / 100 frame_avatar = Image.fromarray( image_utils.shift_hue(avatar_pixels, deform_hue)) img = "%02d" % (i + 1) frame = Image.new("RGBA", (width, height), (54, 57, 63, 1)) hand = Image.open(f"data/meme/hyperpet/{img}.png") frame.paste(frame_avatar, (35, 25 + vertical_offset[i]), frame_avatar) frame.paste(hand, (10, 5), hand) frames.append(frame) return frames @staticmethod def get_bonk_frames(avatar: Image.Image) -> List[Image.Image]: """Get frames for the bonk""" frames = [] width, height = 200, 170 deformation = (0, 0, 0, 5, 10, 20, 15, 5) avatar = image_utils.round_image(avatar.resize((100, 100))) for i in range(8): img = "%02d" % (i + 1) frame = Image.new("RGBA", (width, height), (54, 57, 63, 1)) bat = Image.open(f"data/meme/bonk/{img}.png") frame_avatar = avatar.resize((100, 100 - deformation[i])) frame.paste(frame_avatar, (80, 60 + deformation[i]), frame_avatar) frame.paste(bat, (10, 5), bat) frames.append(frame) return frames @staticmethod def get_whip_frames(avatar: Image.Image) -> List[Image.Image]: """Get frames for the whip""" frames = [] width, height = 250, 150 deformation = (0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 5, 9, 6, 4, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) translation = (0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 3, 3, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0) avatar = image_utils.round_image(avatar.resize((100, 100))) for i in range(26): img = "%02d" % (i + 1) frame = Image.new("RGBA", (width, height), (54, 57, 63, 1)) whip_frame = Image.open(f"data/meme/whip/{img}.png").resize( (150, 150)) frame_avatar = avatar.resize((100 - deformation[i], 100)) frame.paste(frame_avatar, (135 + deformation[i] + translation[i], 25), frame_avatar) frame.paste(whip_frame, (0, 0), whip_frame) frames.append(frame) return frames @staticmethod def get_spank_frames(avatar: Image.Image) -> List[Image.Image]: """Get frames for the spank""" frames = [] width, height = 200, 120 deformation = (4, 2, 1, 0, 0, 0, 0, 3) avatar = image_utils.round_image(avatar.resize((100, 100))) for i in range(8): img = "%02d" % (i + 1) frame = Image.new("RGBA", (width, height), (54, 57, 63, 1)) spoon = Image.open(f"data/meme/spank/{img}.png").resize((100, 100)) frame_avatar = avatar.resize( (100 + 2 * deformation[i], 100 + 2 * deformation[i])) frame.paste(spoon, (10, 15), spoon) frame.paste(frame_avatar, (80 - deformation[i], 10 - deformation[i]), frame_avatar) frames.append(frame) return frames
class Animals(rubbercog.Rubbercog): """Private zone""" def __init__(self, bot): super().__init__(bot) self.config = CogConfig("animals") self.text = CogText("animals") self.channel = None self.role = None # Because the API doesn't return the avatar resource immediately, # sometimes nothing happens, because the client caches the 404 # response (?). This is an attempt to counter that. self.check_delay = 10 def getChannel(self): if self.channel is None: self.channel = self.bot.get_channel(self.config.get("channel")) return self.channel def getRole(self): if self.role is None: self.role = self.getChannel().guild.get_role( self.config.get("role")) return self.role ## ## Commands ## @commands.check(acl.check) @commands.command() async def animal(self, ctx, member: discord.Member): """Send vote embed""" await self.check(member, "manual") ## ## Listeners ## @commands.Cog.listener() async def on_user_update(self, before: discord.User, after: discord.User): # only act if user is verified member = self.getGuild().get_member(after.id) if member is None: return # only act if user is verified if self.getVerifyRole() not in member.roles: return # only act if user has changed their avatar if before.avatar_url == after.avatar_url: return await asyncio.sleep(self.check_delay) await self.check(after, "on_user_update") @commands.Cog.listener() async def on_member_update(self, before: discord.Member, after: discord.Member): # only act if the user has been verified verify = self.getVerifyRole() if not (verify not in before.roles and verify in after.roles): return # only act if their avatar is not default if after.avatar_url == after.default_avatar_url: await self.console.debug(f"{after} verified", "Not an animal (default avatar).") return # lookup user timestamp, only allow new verifications db_user = repo_u.get(after.id) if db_user is not None and db_user.status == "verified": db_user = repo_u.get(after.id) timestamp = datetime.strptime(db_user.changed, "%Y-%m-%d %H:%M:%S") now = datetime.now() if (now - timestamp).total_seconds() > 5: # this was probably temporary unverify, they have been checked before await self.console.debug(f"{after} reverified", "Skipping (unverify).") return await asyncio.sleep(self.check_delay) await self.check(after, "on_member_update") @commands.Cog.listener() async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): """Vote""" if payload.channel_id != self.getChannel().id: return if payload.member.bot: return try: message = await self.getChannel().fetch_message(payload.message_id) except Exception: message = None if (message is None or len(message.embeds) != 1 or message.embeds[0].title != self.text.get("title")): return if str(payload.emoji) not in ("☑️", "❎"): return await message.remove_reaction(payload.emoji, payload.member) animal_id = int(message.embeds[0].description.split(" | ")[1]) if animal_id == payload.member.id: return await message.remove_reaction(payload.emoji, payload.member) try: animal = await self.getChannel().guild.fetch_member(animal_id) except discord.errors.NotFound: animal = None if animal is None: await self.console.error( "animals", f"Could not find member {animal_id}: abort.") return await utils.delete(message) # delete if the user has changed their avatar since the embed creation if str(message.embeds[0].image.url) != str(animal.avatar_url): await self.console.debug( animal, "Avatar has changed since. Vote aborted.") return await utils.delete(message) animal_avatar_url = animal.avatar_url_as(format="jpg") animal_avatar_data = requests.get(animal_avatar_url) animal_avatar = Image.open(BytesIO(animal_avatar_data.content)) animal_avatar_file = tempfile.TemporaryFile() for r in message.reactions: if r.emoji == "☑️" and r.count > self.config.get("limit"): avatar_result: Image.Image = Animals.add_border( animal_avatar, 3, True) avatar_result.save(animal_avatar_file, "png") animal_avatar_file.seek(0) if self.getRole() in animal.roles: # member is an animal and has been before await self.getChannel().send( self.text.get( "result", "yes_yes", nickname=self.sanitise(animal.display_name), ), file=discord.File(fp=animal_avatar_file, filename="animal.png"), ) else: # member is an animal and has not been before try: await animal.add_roles(self.getRole()) await self.getChannel().send( self.text.get("result", "no_yes", mention=animal.mention), file=discord.File(fp=animal_avatar_file, filename="animal.png"), ) except Exception as e: await self.console.error(message, "Could not add animal", e) break elif r.emoji == "❎" and r.count > self.config.get("limit"): avatar_result: Image.Image = Animals.add_border( animal_avatar, 3, False) avatar_result.save(animal_avatar_file, "png") animal_avatar_file.seek(0) if self.getRole() in animal.roles: # member is not an animal and has been before try: await animal.remove_roles(self.getRole()) await self.getChannel().send( self.text.get("result", "yes_no", mention=animal.mention), file=discord.File(fp=animal_avatar_file, filename="animal.png"), ) except Exception as e: await self.console.error(message, "Could not remove animal", e) else: # member is not an animal and has not been before await self.getChannel().send( self.text.get("result", "no_no", mention=animal.mention), file=discord.File(fp=animal_avatar_file, filename="animal.png"), ) break else: return # Edit original message result = [0, 0] for r in message.reactions: if r.emoji == "☑️": result[0] = r.count - 1 elif r.emoji == "❎": result[1] = r.count - 1 await message.edit( embed=None, content=self.text.get("edit", nickname=self.sanitise(animal.display_name), yes=result[0], no=result[1]), ) try: await message.unpin() except Exception as e: await self.console.error(message, "Could not unpin Animal vote embed", e) ## ## Logic ## async def check(self, member: discord.Member, source: str): """Create vote embed""" embed = self.embed( title=self.text.get("title"), description=f"{self.sanitise(str(member))} | {member.id}", ) embed.add_field( name=self.text.get("source", source), value=self.text.get("required", limit=self.config.get("limit")), inline=False, ) embed.set_image(url=member.avatar_url) message = await self.getChannel().send(embed=embed) await message.add_reaction("☑️") await message.add_reaction("❎") try: await message.pin() except Exception as e: await self.console.warning(member, "Could not pin Animal check embed.", e) await asyncio.sleep(0.5) messages = await message.channel.history(limit=5, after=message).flatten() for m in messages: if m.type == discord.MessageType.pins_add: await utils.delete(m) break @staticmethod def add_border(image: Image.Image, border: int, animal: bool) -> Image.Image: """Add border to created image. image: The avatar. border: width of the border. animal: whether the avatar is an animal or not. """ image_size = 160 frame_color = (22, 229, 0, 1) if animal else (221, 56, 31, 1) frame = Image.new("RGBA", (image_size + border * 2, image_size + border * 2), frame_color) frame = image_utils.round_image(frame) avatar = image_utils.round_image(image.resize( (image_size, image_size))) frame.paste(avatar, (border, border), avatar) return frame
class Roles(rubbercog.Rubbercog): """Manage roles and subjects""" def __init__(self, bot): super().__init__(bot) self.config = CogConfig("roles") self.text = CogText("roles") self.limit_programmes = {} self.limit_interests = {} ## ## Getters ## def getLimitProgrammes(self, channel: discord.TextChannel) -> discord.Role: gid = str(channel.guild.id) if gid not in self.limit_programmes: self.limit_programmes[gid] = discord.utils.get( channel.guild.roles, name="---PROGRAMMES") return self.limit_programmes[gid] def getLimitInterests(self, channel: discord.TextChannel) -> discord.Role: gid = str(channel.guild.id) if gid not in self.limit_interests: self.limit_interests[gid] = discord.utils.get(channel.guild.roles, name="---INTERESTS") return self.limit_interests[gid] ## ## Listeners ## @commands.Cog.listener() async def on_message(self, message): """Listen for react-to-role message""" if not isinstance(message.channel, discord.TextChannel): return if message.channel.id not in self.config.get("r2r_channels"): return emote_channel_list = await self._message_to_tuple_list(message) if emote_channel_list is None: # do not throw errors if nothing is found return for emote_channel in emote_channel_list: try: await message.add_reaction(emote_channel[0]) except (discord.errors.Forbidden, discord.errors.HTTPException): continue @commands.Cog.listener() async def on_raw_message_edit(self, payload: discord.RawMessageUpdateEvent): """Listen for react-to-role message changes""" if payload.channel_id not in self.config.get("r2r_channels"): return message_channel = self.bot.get_channel(payload.channel_id) message = await message_channel.fetch_message(payload.message_id) # make a list of current emotes emote_channel_list = await self._message_to_tuple_list(message) for emote_channel in emote_channel_list: try: await message.add_reaction(emote_channel[0]) except (discord.errors.Forbidden, discord.errors.HTTPException): continue @commands.Cog.listener() async def on_raw_reaction_add(self, payload): # extract data from payload payload = await self._reaction_payload_to_tuple(payload) if payload is None: return channel, message, member, emoji = payload emote_channel_list = await self._message_to_tuple_list(message) result = None for emote_channel in emote_channel_list: if str(emoji) == emote_channel[0]: # try both subject and role subject = await self._get_subject(channel, emote_channel[1]) if subject is not None: result = await self._subject_add(message.channel, member, subject) break role = await self._get_role(channel, emote_channel[1]) if role is not None: result = await self._role_add(message.channel, member, role) break else: # another emote was added result = None if not result: try: await message.remove_reaction(emoji, member) except Exception: pass @commands.Cog.listener() async def on_raw_reaction_remove(self, payload): # extract data from payload payload = await self._reaction_payload_to_tuple(payload) if payload is None: return channel, message, member, emoji = payload emote_channel_list = await self._message_to_tuple_list(message) for emote_channel in emote_channel_list: if str(emoji) == emote_channel[0]: # try both subject and role subject = await self._get_subject(channel, emote_channel[1]) if subject is not None: await self._subject_remove(message.channel, member, subject) break role = await self._get_role(channel, emote_channel[1]) if role is not None: await self._role_remove(message.channel, member, role) break ## ## Helper functions ## async def _get_subject(self, location, shortcut: str) -> discord.TextChannel: db_subject = repo_s.get(shortcut) if db_subject is not None: return discord.utils.get(location.guild.text_channels, name=shortcut) return async def _get_role(self, location, role: str) -> discord.Role: return discord.utils.get(location.guild.roles, name=role) async def _message_to_tuple_list(self, message: discord.Message) -> list: """Return (emote, channel/role) list""" # preprocess message content content = message.content.replace("*", "").replace("_", "").replace("#", "") try: content = content.rstrip().split("\n") except ValueError: await message.channel.send(self.text.get("role_help")) return # check every line result = [] for line in content: try: line_ = line.split(" ") emote = line_[0] target = line_[1] if "<#" in emote: # custom emote, get it's ID emote = int(emote.replace("<#", "").replace(">", "")) result.append((emote, target)) except Exception: # do not send errors if message is in #add-* channel if message.channel.id in self.config.get("r2r_channels"): return await self._send( message.channel, self.text.fill("invalid_role_line", line=self.sanitise(line, limit=50)), ) return return result async def _reaction_payload_to_tuple( self, payload: discord.RawMessageUpdateEvent) -> tuple: """Return (channel, message, member, emoji) or None""" # channel channel = self.bot.get_channel(payload.channel_id) if not isinstance(channel, discord.TextChannel): return # message try: message = await channel.fetch_message(payload.message_id) except discord.NotFound: return # halt if not react-to-role message if channel.id not in self.config.get("r2r_channels"): return # member member = message.guild.get_member(payload.user_id) if member.bot: return # emoji if payload.emoji.is_custom_emoji(): emoji = self.bot.get_emoji(payload.emoji.id) or payload.emoji else: emoji = payload.emoji.name return channel, message, member, emoji def _get_teacher_channel( self, subject: discord.TextChannel) -> discord.TextChannel: return discord.utils.get( subject.guild.text_channels, name=subject.name + config.get("channels", "teacher suffix"), ) ## ## Logic ## async def _subject_add( self, source: discord.TextChannel, member: discord.Member, channel: discord.TextChannel, ) -> bool: # check permission for subject_role in self.config.get("subject_roles"): if subject_role in [r.id for r in member.roles]: break else: # they do not have neccesary role await self._send( source, self.text.get("deny_subject", mention=member.mention)) return False await channel.set_permissions(member, view_channel=True) teacher_channel = self._get_teacher_channel(channel) if teacher_channel is not None: await teacher_channel.set_permissions(member, view_channel=True) return True async def _subject_remove( self, source: discord.TextChannel, member: discord.Member, channel: discord.TextChannel, ): # we do not need to check for permissions await channel.set_permissions(member, overwrite=None) teacher_channel = self._get_teacher_channel(channel) if teacher_channel is not None: await teacher_channel.set_permissions(member, overwrite=None) async def _role_add(self, channel: discord.TextChannel, member: discord.Member, role: discord.Role) -> bool: if role < self.getLimitProgrammes( channel) and role > self.getLimitInterests(channel): # role is programme, check if user has permission for programme_role in self.config.get("programme_roles"): if programme_role in [r.id for r in member.roles]: break else: await self._send( channel, self.text.get("deny_programme", mention=member.mention)) return False # check if user already doesn't have some programme role for user_role in member.roles: # fmt: off if user_role < self.getLimitProgrammes(channel) \ and user_role > self.getLimitInterests(channel): await self._send( channel, self.text.get("deny_second_programme", mention=member.mention), ) return False # fmt: on elif role < self.getLimitInterests(channel): # role is below interests limit, continue pass else: # role is limit itself or something above programmes await self._send( channel, self.text.get("deny_high_role", mention=member.mention)) return False await member.add_roles(role) # optionally, hide channel if channel.id in self.config.get("r2h_channels"): await channel.set_permissions(member, read_messages=False) return True async def _role_remove(self, channel: discord.TextChannel, member: discord.Member, role: discord.Role): if role < self.getLimitProgrammes( channel) and role > self.getLimitInterests(channel): # role is programme, check if user has permission for programme_role in self.config.get("programme_roles"): if programme_role in [r.id for r in member.roles]: break else: await self._send( channel, self.text.get("deny_programme", mention=member.mention)) return elif role < self.getLimitInterests(channel): # role is below interests limit, continue pass else: # role is limit itself or something above programmes return await self._send( channel, self.text.get("deny_high_role", mention=member.mention)) await member.remove_roles(role) async def _send(self, channel: discord.TextChannel, text: str): if channel.id in self.config.get("r2r_channels"): return await channel.send(text, delete_after=config.get("delay", "user error"))
class Anonsend(rubbercog.Rubbercog): """Send files anonymously""" def __init__(self, bot): super().__init__(bot) self.text = CogText("anonsend") self.config = CogConfig("anonsend") @commands.group() async def anonsend(self, ctx): """Send an image anonymously. Run `anonsend link` to get the URL to the website. """ await utils.send_help(ctx) @commands.guild_only() @commands.check(acl.check) @anonsend.command(name="add") async def anonsend_add(self, ctx, name: str): """Add channel used for anonymous posts""" try: repo_a.add(guild_id=ctx.guild.id, channel_id=ctx.channel.id, name=name) except ValueError: return await ctx.send(self.text.get("name_exists")) await self.event.sudo( ctx, f"anonsend channel created: `{self.sanitise(name)}`.") await ctx.send(self.text.get("added")) @commands.check(acl.check) @anonsend.command(name="remove") async def anonsend_remove(self, ctx, name: str): """Remove channel used for anonymous posts""" try: repo_a.remove(name=name) except ValueError: return await ctx.send(self.text.get("bad_name")) await self.event.sudo( ctx, f"anonsend channel removed: `{self.sanitise(name)}`.") await ctx.send(self.text.get("removed")) @commands.check(acl.check) @anonsend.command(name="rename") async def anonsend_rename(self, ctx, old_name: str, new_name: str): """Rename anonymous post channel""" try: repo_a.rename(old_name=str, new_name=str) except ValueError as e: return await ctx.send("`" + str(e) + "`") await self.event.sudo( ctx, ("anonsend channel renamed: " f"`{self.sanitise(old_name)}` to " f"`{self.sanitise(new_name)}`."), ) await ctx.send(self.text.get("renamed")) @commands.guild_only() @commands.check(acl.check) @anonsend.command(name="list") async def anonsend_list(self, ctx): """Get mappings for current guild""" channels = repo_a.get_all(ctx.guild.id) await ctx.send("```\n" + "\n".join([str(x) for x in channels]) + "\n```") @commands.check(acl.check) @anonsend.command(name="fetch") async def anonsend_fetch(self, ctx): """Get list of pending files""" url_base = self.config.get( "url") + "/api.php?apikey=" + self.config.get("apikey") response = requests.get(url_base + "&action=list") files = response.json() embed = self.embed(ctx=ctx, title=self.text.get("fetch_server")) for i, (file, timestamp) in enumerate(files.items()): uploaded = datetime.datetime.fromtimestamp(timestamp) embed.add_field( name=file, value=uploaded.strftime("%Y-%m-%d %H:%M:%S") + "\n" + self.text.get( "fetch_age", time=utils.seconds2str( (datetime.datetime.now() - uploaded).seconds), ), ) if i % 24 == 0 and i > 0: await ctx.send(embed=embed) await ctx.send(embed=embed) @commands.check(acl.check) @anonsend.command(name="link", aliases=["url"]) async def anonsend_link(self, ctx): """Get link to the anonsend upload website""" await ctx.send(self.config.get("url")) @commands.dm_only() @anonsend.command(name="submit") async def anonsend_submit(self, ctx, name: str, filename: str): """Send a file""" # get channel target = repo_a.get(name=name) if target is None: return await ctx.send(self.text.get("no_channel")) channel = self.bot.get_channel(target.channel_id) if channel is None: return await ctx.send(self.text.get("channel_not_found")) guild = self.bot.get_guild(target.guild_id) guild_user = guild.get_member(ctx.author.id) if guild_user is None: return await ctx.send(self.text.get("not_in_guild")) url_base = self.config.get( "url") + "/api.php?apikey=" + self.config.get("apikey") url_base += "&file=" + filename # feedback message = await ctx.send(self.text.get("downloading")) # download image response = requests.get(url_base + "&action=download") if response.status_code == 404: return await ctx.send(self.text.get("bad_filename")) if response.status_code != 200: await ctx.send( self.text.get("download_error", message=response.content)) await self.event.error(ctx, "anonsend error: " + response.content) return image_binary = tempfile.TemporaryFile() image_binary.write(response.content) image_binary.seek(0) # feedback await message.edit(content=message.content + " " + self.text.get("uploading")) # send it await channel.send(file=discord.File( fp=image_binary, filename=filename, )) feedback = requests.get(url_base + "&action=delete") if feedback.status_code != 200: await self.event.error( ctx, "delete error: " + str(feedback.status_code)) image_binary.close() # feedback await message.edit(content=message.content + " " + self.text.get("done")) # increment log repo_a.increment(name) await self.event.user( "DMChannel", f"Anonymous image sent to **{self.sanitise(name)}**.")
class Sync(rubbercog.Rubbercog): """Guild synchronisation""" def __init__(self, bot: commands.Bot): super().__init__(bot) self.config = CogConfig("sync") self.text = CogText("sync") self.slave_guild_id = self.config.get("slave_guild_id") self.engineer_ids = self.config.get("roles", "master_engineer_ids") self.slave_verify_id = self.config.get("roles", "slave_verify_id") self.mapping_ids = self.config.get("roles", "mapping") self.mapping = {} self.slave_guild = None self.slave_verify = None def get_slave_guild(self): if self.slave_guild is None: self.slave_guild = self.bot.get_guild(self.slave_guild_id) return self.slave_guild def get_slave_verify(self) -> discord.Role: if self.slave_verify is None: self.slave_verify = self.get_slave_guild().get_role( self.slave_verify_id) return self.slave_verify def get_master_member(self, user_id: int) -> discord.Member: return self.getGuild().get_member(user_id) def get_slave_member(self, user_id: int) -> discord.Member: return self.get_slave_guild().get_member(user_id) def get_slave_role(self, master_role_id: int) -> discord.Role: key = str(master_role_id) if key not in self.mapping_ids: return None if key not in self.mapping.keys(): self.mapping[key] = self.get_slave_guild().get_role( self.mapping_ids[key]) return self.mapping[key] ## ## Commands ## ## ## Listeners ## @commands.Cog.listener() async def on_member_update(self, before: discord.Member, after: discord.Member): """Add member to slave guild if they add one of specified roles""" if before.roles == after.roles: return # roles are guild-specific, we do not need to check the guild ID before_ids = [role.id for role in before.roles] after_ids = [role.id for role in after.roles] for engineer_id in self.engineer_ids: if engineer_id not in before_ids and engineer_id in after_ids: await self.verify_member(after) return if engineer_id in before_ids and engineer_id not in after_ids: await self.unverify_member(after) return @commands.Cog.listener() async def on_member_join(self, member: discord.Member): """Verify user if they join slave server while having specified role on master""" if member.guild.id != self.slave_guild_id: return master_member = self.get_master_member(member.id) master_member_roles = [role.id for role in master_member.roles] for engineer_id in self.engineer_ids: if engineer_id in master_member_roles: await self.verify_member(master_member) return ## ## Logic ## async def verify_member(self, member: discord.Member): # get member object on slave guild slave_member = self.get_slave_member(member.id) if slave_member is not None: # map some of their roles to slave ones to_add = [self.get_slave_verify()] event_roles = [] for role in member.roles: if str(role.id) in self.mapping_ids.keys(): mapped = self.get_slave_role(role.id) to_add.append(mapped) event_roles.append(mapped.name) roles = ", ".join(f"**{self.sanitise(name)}**" for name in event_roles) # add the roles await slave_member.add_roles(*to_add, reason="Sync: verify") await self.event.user(slave_member, f"Verified on slave server with {roles}.") else: # send invitation await member.send( self.text.get("invite", invite_link=self.config.get("slave_invite_link")) ) await self.event.user(member, "Not on slave server: invite sent.") async def unverify_member(self, member: discord.Member): # get member object on slave guild slave_member = self.get_slave_member(member.id) if slave_member is not None: roles = slave_member.roles[1:] # the first is @everyone await slave_member.remove_roles(*roles, reason="Sync: unverify") await self.event.user(slave_member, "Unverified on slave server.") else: await self.event.user(member, "Not on slave server: skipping unverify.")
class Actress(rubbercog.Rubbercog): """Be a human""" def __init__(self, bot): super().__init__(bot) self.config = CogConfig("actress") self.text = CogText("actress") self.supported_formats = ("jpg", "jpeg", "png", "webm", "mp4", "gif") self.path = os.getcwd() + "/data/actress/" try: self.reactions = hjson.load(open(self.path + "reactions.hjson")) except: self.reactions = {} self.usage = {} ## ## Commands ## @commands.check(acl.check) @commands.group(name="send") async def send(self, ctx): """Send message to given channel""" await utils.send_help(ctx) @commands.check(acl.check) @send.command(name="text") async def send_text(self, ctx, channel: discord.TextChannel, *, content: str): """Send a text to text channel channel: Target text channel content: Text """ message = await channel.send(content) await self.event.sudo( ctx, f"Text sent to {channel.mention}:\n<{message.jump_url}>\n>>> _{content}_", ) await self.output.info(ctx, self.text.get("send_text")) @commands.check(acl.check) @send.command(name="dm", aliases=["text-dm"]) async def send_dm(self, ctx, user: discord.User, *, content: str): """Send a DM to a user""" try: message = await user.send(content) except discord.Forbidden: return await ctx.send(self.text.get("dm_forbidden")) await self.event.sudo(ctx, f"DM sent to {user}:\n>>> _{content}_") await self.output.info(ctx, self.text.get("send_text")) @commands.check(acl.check) @send.command(name="image", aliases=["file"]) async def send_image(self, ctx, channel: discord.TextChannel, filename): """Send an image as a bot channel: Target text channel filename: A filename """ now = time.monotonic() try: async with ctx.typing(): message = await channel.send(file=discord.File(self.path + filename)) delta = str(int(time.monotonic() - now)) await self.output.info(ctx, self.text.get("send_file", delta=delta)) await self.event.sudo( ctx, f"Media file sent to {channel.mention}:\n" f"> _{filename}_\n> <{message.jump_url}>", ) except Exception as e: await self.output.error(ctx, self.text.get("FileSendError"), e) @commands.check(acl.check) @commands.group(name="react", aliases=["reaction", "reactions"]) async def react(self, ctx): await utils.send_help(ctx) @commands.check(acl.check) @react.command(name="overview") async def react_overview(self, ctx): """List registered reactions""" embed = self.embed(ctx=ctx, description=self.text.get("embed", "total", count=len( self.reactions))) value = [] for name in self.reactions.keys(): if len(value) + len(name) > 1024: embed.add_field(name="\u200b", value="\n".join(f"`{v}`" for v in value), inline=False) value = [] value.append(name) embed.add_field(name="\u200b", value="\n".join(f"`{v}`" for v in value), inline=False) await ctx.send(embed=embed) @commands.check(acl.check) @react.command(name="list") async def react_list(self, ctx): """See details for reactions""" try: name = next(iter(self.reactions)) reaction = self.reactions[name] embed = self.embed(ctx=ctx, page=(1, len(self.reactions))) except StopIteration: reaction = None embed = self.embed(ctx=ctx) if reaction is not None: embed = self.fill_reaction_embed(embed, name, reaction) message = await ctx.send(embed=embed) if len(self.reactions) > 1: await message.add_reaction("◀") await message.add_reaction("▶") @commands.check(acl.check) @react.command(name="usage", aliases=["stat", "stats"]) async def react_usage(self, ctx): """See reactions usage since start""" items = { k: v for k, v in sorted( self.usage.items(), key=lambda item: item[1], reverse=True) } embed = self.embed(ctx=ctx) content = [] total = 0 template = "`{count:>2}` … **{reaction}**" for reaction, count in items.items(): content.append(template.format(count=count, reaction=reaction)) total += count if len(content) == 0: content.append(self.text.get("embed", "nothing")) embed.add_field(name=self.text.get("embed", "total", count=total), value="\n".join(content)) await ctx.send(embed=embed, delete_after=config.delay_embed) await utils.delete(ctx) @commands.check(acl.check) @react.command(name="add") async def react_add(self, ctx, name: str = None, *, parameters=None): """Add new reaction ``` ?react add <reaction name> type <image | text> match <full | start | end | any> sensitive <true | false> triggers "a b c" "d e" f responses "abc def" enabled <true | false> users 0 1 2 channels 0 1 2 counter 10 ``` """ if name is None: return await utils.send_help(ctx) elif name in self.reactions.keys(): raise ReactionNameExists() reaction = await self.parse_react_message(ctx.message, strict=True) self.reactions[name] = reaction self._save_reactions() await self.output.info(ctx, self.text.get("reaction_add", name=name)) await self.event.sudo(ctx, f"Reaction **{name}** added.") @commands.check(acl.check) @react.command(name="edit") async def react_edit(self, ctx, name: str = None, *, parameters=None): """Edit reaction ``` ?react edit <reaction name> type <image | text> match <full | start | end | any> sensitive <true | false> triggers "a b c" "d e" f responses "abc def" enabled <true | false> users 0 1 2 channels 0 1 2 counter 10 ``` """ if name is None: return await utils.send_help(ctx) elif name not in self.reactions.keys(): raise ReactionNotFound() new_reaction = await self.parse_react_message(ctx.message, strict=False) reaction = self.reactions[name] for key, value in new_reaction.items(): reaction[key] = new_reaction[key] self.reactions[name] = reaction self._save_reactions() await self.output.info(ctx, self.text.get("reaction_edit", name=name)) await self.event.sudo(ctx, f"Reaction **{name}** updated.") @commands.check(acl.check) @react.command(name="remove") async def react_remove(self, ctx, name: str = None): """Remove reaction""" if name is None: return await utils.send_help(ctx) elif name not in self.reactions.keys(): raise ReactionNotFound() del self.reactions[name] self._save_reactions() await self.output.info(ctx, self.text.get("reaction_remove", name=name)) await self.event.sudo(ctx, f"Reaction **{name}** removed.") @commands.check(acl.check) @commands.group(name="image", aliases=["img", "images"]) async def image(self, ctx): """Manage images available to the bot""" await utils.send_help(ctx) @commands.check(acl.check) @image.command(name="list") async def image_list(self, ctx): """List available commands""" files = os.listdir(self.path) template = "`{size:>5} kB` … {filename}" content = [] for file in files: if file.split(".")[-1] not in self.supported_formats: continue size = int(os.path.getsize(self.path + file) / 1024) content.append(template.format(size=size, filename=file)) if len(content) == 0: content.append(self.text.get("image", "no_files")) embed = self.embed(ctx=ctx) embed.add_field(name="\u200b", value="\n".join(content)) await utils.send(ctx, embed=embed) await utils.delete(ctx) @commands.check(acl.check) @image.command(name="download", aliases=["dl"]) async def image_download(self, ctx, url: str, filename: str): """Download new image url: URL of the image filename: Target filename """ if filename.split(".")[-1] not in self.supported_formats: return self.output.error(ctx, self.text.get("bad_extension")) if "/" in filename or "\\" in filename or ".." in filename: return self.output.error(ctx, self.text.get("bad_character")) with open(self.path + filename, "wb") as f: response = get(url) f.write(response.content) await self.output.info(ctx, self.text.get("image", "downloaded")) await utils.delete(ctx) @commands.check(acl.check) @image.command(name="remove", aliases=["delete", "rm", "del"]) async def image_remove(self, ctx, filename: str): """Remove image filename: An image filename """ if "/" in filename or "\\" in filename or ".." in filename: return self.output.error(ctx, self.text.get("image", "bad_character")) os.remove(self.path + filename) await self.output.info(self.text.get("image", "deleted")) await utils.delete(ctx) @commands.check(acl.check) @image.command(name="show") async def image_show(self, ctx, filename: str): """Show an image filename: An image filename """ await self.send_image(ctx, ctx.channel, filename) await utils.delete(ctx) ## ## Listeners ## @commands.Cog.listener() async def on_message(self, message): if message.author.bot: return # fmt: off if hasattr(message.channel, "id") \ and message.channel.id in self.config.get("ignored_channels"): return # fmt: on for name, reaction in self.reactions.items(): # test if not self._reaction_matches(message, reaction): continue # send response = random.choice(reaction["responses"]) if reaction["type"] == "text": response = response.replace("((mention))", message.author.mention) response = response.replace( "((name))", self.sanitise(message.author.display_name)) await message.channel.send(response) elif reaction["type"] == "image": await message.channel.send(file=discord.File(self.path + response)) # log if name in self.usage: self.usage[name] += 1 else: self.usage[name] = 1 # counter if "counter" in reaction: if reaction["counter"] > 1: self.reactions[name]["counter"] -= 1 else: # last usage, disable del self.reactions[name]["counter"] self.reactions[name]["enabled"] = False await self.event.user(message, "Reaction disabled: **{name}**.") self._save_reactions() break @commands.Cog.listener() async def on_reaction_add(self, reaction: discord.Reaction, user: discord.User): # do we care? if (user.bot or len(reaction.message.embeds) != 1 or reaction.message.embeds[0].title != f"{config.prefix}react list"): return if hasattr(reaction, "emoji"): if str(reaction.emoji) == "◀": page_delta = -1 elif str(reaction.emoji) == "▶": page_delta = 1 else: # invalid reaction return await utils.remove_reaction(reaction, user) else: # invalid reaction return await utils.remove_reaction(reaction, user) embed = reaction.message.embeds[0] if embed.footer == discord.Embed.Empty or " | " not in embed.footer.text: return await utils.remove_reaction(reaction, user) # allow only the author if embed.footer.text.split(" | ")[0] != str(user): return await utils.remove_reaction(reaction, user) # get page footer_text = embed.footer.text pages = footer_text.split(" | ")[-1] page_current = int(pages.split("/")[0]) - 1 page = (page_current + page_delta) % len(self.reactions) footer_text = footer_text.replace(pages, f"{page+1}/{len(self.reactions)}") # update embed bot_reaction_name = list(self.reactions.keys())[page] bot_reaction = self.reactions[bot_reaction_name] embed = self.fill_reaction_embed(embed, bot_reaction_name, bot_reaction) embed.set_footer(text=footer_text, icon_url=embed.footer.icon_url) await reaction.message.edit(embed=embed) await utils.remove_reaction(reaction, user) ## ## Helper functions ## def _save_reactions(self): with open(self.path + "reactions.hjson", "w", encoding="utf-8") as f: hjson.dump(self.reactions, f, ensure_ascii=False, indent="\t") async def _remove_reaction(self, reaction, user): try: await reaction.remove(user) except: pass def _reaction_matches(self, message, reaction) -> bool: # check if it is enabled if not reaction["enabled"]: return False # normalise if reaction["sensitive"]: text = message.content triggers = reaction["triggers"] else: text = message.content.lower() triggers = [x.lower() for x in reaction["triggers"]] # check the type if reaction["match"] == "full" and text not in triggers: return False if reaction["match"] == "any": for trigger in triggers: if trigger in text: break else: return False if reaction["match"] == "start": for trigger in triggers: if text.startswith(trigger): break else: return False if reaction["match"] == "end": for trigger in triggers: if text.endswith(trigger): break else: return False # conditions if "users" in reaction and message.author.id not in reaction["users"]: return False if "channels" in reaction and message.channel.id not in reaction[ "channels"]: return False return True ## ## Logic ## async def parse_react_message(self, message: discord.Message, strict: bool) -> dict: content = message.content.replace("`", "").split("\n")[1:] result = {} # fill values for line in content: line = line.split(" ", 1) key = line[0] value = line[1] if key not in ( "type", "match", "sensitive", "triggers", "responses", "enabled", "users", "channels", "counter", ): raise InvalidReactionKey(key=key) # check invalid = False # fmt: off if key == "type" and value not in ("text", "image") \ or key == "match" and value not in ("full", "start", "end", "any") \ or key == "sensitive" and value not in ("true", "false") \ or key == "enabled" and value not in ("true", "false") \ or key == "triggers" and len(value) < 1 \ or key == "responses" and len(value) < 1: invalid = True # fmt: on if invalid: raise ReactionParsingException(key, value) # parse if key in ("sensitive", "enabled"): value = value == "true" elif key in ("triggers", "responses"): # convert to list value = shlex.split(value) elif key in ("users", "channels"): # convert to list of ints try: value = [int(x) for x in shlex.split(value)] except: raise ReactionParsingException(key, value) elif key == "counter": try: value = int(value) except: raise ReactionParsingException(key, value) result[key] = value if strict: # check if all required values are present for key in ("type", "match", "triggers", "response"): if key is None: raise discord.MissingRequiredArgument(param=key) return result def fill_reaction_embed(self, embed: discord.Embed, name: str, reaction: dict) -> discord.Embed: # reset any previous embed.clear_fields() embed.add_field(name="name", value=f"**{name}**") for key in ("triggers", "responses"): value = "\n".join(reaction[key]) embed.add_field(name=key, value=value, inline=False) for key in ("type", "match", "sensitive", "enabled"): embed.add_field(name=key, value=reaction[key]) if "users" in reaction.keys() and reaction["users"] is not None: users = [self.bot.get_user(x) for x in reaction["users"]] value = "\n".join( f"`{user.id}` {user.name if hasattr(user, 'name') else '_unknown_'}" for user in users) embed.add_field(name="users", value=value, inline=False) if "channels" in reaction.keys() and reaction["channels"] is not None: channels = [self.bot.get_channel(x) for x in reaction["channels"]] value = "\n".join(f"`{channel.id}` {channel.mention}" for channel in channels) embed.add_field(name="channels", value=value, inline=False) if "counter" in reaction.keys() and reaction["counter"] is not None: embed.add_field(name="counter", value=str(reaction["counter"])) return embed ## ## Error catching ## @commands.Cog.listener() async def on_command_error(self, ctx: commands.Context, error): # try to get original error if hasattr(ctx.command, "on_error") or hasattr(ctx.command, "on_command_error"): return error = getattr(error, "original", error) # non-rubbergoddess exceptions are handled globally if not isinstance(error, rubbercog.RubbercogException): return # fmt: off # exceptions with parameters if isinstance(error, InvalidReactionKey): await self.output.error( ctx, self.text.get("InvalidReactionKey", key=error.key)) elif isinstance(error, ReactionParsingException): await self.output.error( ctx, self.text.get("ReactionParsingException", key=error.key, value=error.value)) # exceptions without parameters elif isinstance(error, ActressException): await self.output.error(ctx, self.text.get(type(error).__name__))
class Random(rubbercog.Rubbercog): """Pick, flip, roll dice""" def __init__(self, bot): super().__init__(bot) self.text = CogText("random") @commands.cooldown(rate=3, per=20.0, type=commands.BucketType.user) @commands.command() async def pick(self, ctx, *args): """Pick an option""" for i, arg in enumerate(args): if arg.endswith("?"): args = args[i + 1 :] break if not len(args): return option = self.sanitise(random.choice(args)) if option is not None: await ctx.reply(option) await utils.room_check(ctx) @commands.cooldown(rate=3, per=20.0, type=commands.BucketType.user) @commands.command() async def flip(self, ctx): """Yes/No""" option = random.choice(self.text.get("flip")) await ctx.reply(option) await utils.room_check(ctx) @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user) @commands.command() async def random(self, ctx, first: int, second: int = None): """Pick number from interval""" if second is None: second = 0 if first > second: first, second = second, first option = random.randint(first, second) await ctx.reply(option) await utils.room_check(ctx) @commands.cooldown(rate=5, per=20, type=commands.BucketType.channel) @commands.command(aliases=["unsplash"]) async def picsum(self, ctx, *, seed: str = None): """Get random image from picsum.photos""" size = "900/600" url = "https://picsum.photos/" if seed: url += "seed/" + seed + "/" url += f"{size}.jpg?random={ctx.message.id}" # we cannot use the URL directly, because embed will contain other image than its thumbnail image = requests.get(url) if image.status_code != 200: return await ctx.reply(f"E{image.status_code}") # get image info # example url: https://i.picsum.photos/id/857/600/360.jpg?hmac=..... image_id = image.url.split("/id/", 1)[1].split("/")[0] image_info = requests.get(f"https://picsum.photos/id/{image_id}/info") try: image_url = image_info.json()["url"] except Exception: image_url = discord.Embed.Empty footer = "picsum.photos" if seed: footer += f" ({seed})" embed = self.embed(ctx=ctx, title=discord.Embed.Empty, description=image_url, footer=footer) embed.set_image(url=image.url) await ctx.reply(embed=embed) await utils.room_check(ctx) @commands.cooldown(rate=5, per=20, type=commands.BucketType.channel) @commands.command() async def cat(self, ctx): """Get random image of a cat""" data = requests.get("https://api.thecatapi.com/v1/images/search") if data.status_code != 200: return await ctx.reply(f"E{data.status_code}") embed = self.embed(ctx=ctx, title=discord.Embed.Empty, footer="thecatapi.com") embed.set_image(url=data.json()[0]["url"]) await ctx.reply(embed=embed) await utils.room_check(ctx) @commands.cooldown(rate=5, per=20, type=commands.BucketType.channel) @commands.command() async def dog(self, ctx): """Get random image of a dog""" data = requests.get("https://api.thedogapi.com/v1/images/search") if data.status_code != 200: return await ctx.reply(f"E{data.status_code}") embed = self.embed(ctx=ctx, title=discord.Embed.Empty, footer="thedogapi.com") embed.set_image(url=data.json()[0]["url"]) await ctx.reply(embed=embed) await utils.room_check(ctx) @commands.cooldown(rate=5, per=60, type=commands.BucketType.channel) @commands.command() async def xkcd(self, ctx, number: int = None): """Get random xkcd comics Arguments --------- number: Comics number. Omit to get random one. """ # get maximal fetched = await utils.fetch_json("https://xkcd.com/info.0.json") # get random if number is None or number < 1 or number > fetched["num"]: number = random.randint(1, fetched["num"]) # fetch requested if number != fetched["num"]: fetched = await utils.fetch_json(f"https://xkcd.com/{number}/info.0.json") embed = self.embed( ctx=ctx, title=fetched["title"], description="_" + fetched["alt"][:2046] + "_", footer="xkcd.com", ) embed.add_field( name=( f"{fetched['year']}" f"-{str(fetched['month']).zfill(2)}" f"-{str(fetched['day']).zfill(2)}" ), value=( f"https://xkcd.com/{number}\n" + f"https://www.explainxkcd.com/wiki/index.php/{number}" ), inline=False, ) embed.set_image(url=fetched["img"]) await ctx.reply(embed=embed) await utils.room_check(ctx) @commands.cooldown(rate=5, per=60, type=commands.BucketType.channel) @commands.command() async def dadjoke(self, ctx, *, keyword: str = None): """Get random dad joke Arguments --------- keyword: search for a certain keyword in a joke """ if keyword is not None and ("&" in keyword or "?" in keyword): await ctx.reply(self.text.get("joke_notfound")) return await utils.room_check(ctx) param = {"limit": "30"} url = "https://icanhazdadjoke.com" if keyword != None: param["term"] = keyword url += "/search" fetched = requests.get(url, headers={"Accept": "application/json"}, params=param) if keyword != None: res = fetched.json()["results"] if len(res) == 0: await ctx.reply(self.text.get("joke_notfound")) return await utils.room_check(ctx) result = random.choice(res) else: result = fetched.json() embed = self.embed( ctx=ctx, description=result["joke"], footer="icanhazdadjoke.com", url="https://icanhazdadjoke.com/j/" + result["id"], ) await ctx.reply(embed=embed) await utils.room_check(ctx)
class Janitor(rubbercog.Rubbercog): """Manage users, roles and channels""" def __init__(self, bot): super().__init__(bot) self.text = CogText("janitor") @commands.cooldown(rate=2, per=20.0, type=commands.BucketType.user) @commands.check(acl.check) @commands.command() async def hoarders(self, ctx: commands.Context, warn: str = None): """Check for users with multiple programme roles warn: Optional. Use "warn" string to send warnings, else just list the users """ warn = warn == "warn" hoarders = [] limit_top = discord.utils.get(self.getGuild().roles, name="---PROGRAMMES") limit_bottom = discord.utils.get(self.getGuild().roles, name="---INTERESTS") for member in self.getGuild().members: prog = [] for role in member.roles: if role < limit_top and role > limit_bottom: prog.append(role.name) if len(prog) > 1: hoarders.append([member, prog]) if len(hoarders) == 0: return await ctx.send(self.text.get("no_hoarders")) embed = self.embed(ctx=ctx, title=self.text.get("embed", "title")) if warn: msg = await ctx.send( self.text.get("sending", num=1, all=len(hoarders))) for num, (hoarder, progs) in enumerate(hoarders): # fmt: off embed.add_field( name=self.text.get("embed", "user"), value=f"**{self.sanitise(hoarder.name)}** ({hoarder.id})", ) embed.add_field(name=self.text.get("embed", "status"), value=hoarder.status) embed.add_field( name=self.text.get("embed", "programmes"), value=", ".join(progs), inline=False, ) if warn: if num % 5 == 0: await msg.edit(content=self.text.get( "sending", num=num, all=len(hoarders))) await hoarder.send( self.text.get("warning", guild=self.getGuild().name)) if num % 8 == 0: # There's a limit of 25 fields per embed await ctx.send(embed=embed) embed = embed.clear_fields() # fmt: on if warn: await msg.edit(content=self.text.get("sent", num=len(hoarders))) await ctx.send(embed=embed) @commands.guild_only() @commands.check(acl.check) @commands.bot_has_permissions(manage_messages=True) @commands.command() async def purge(self, ctx, limit: int, pinMode: str = "pinSkip"): """Delete messages from current text channel limit: how many messages should be deleted mode: pinSkip (default) | pinStop | pinIgnore """ if pinMode not in ("pinSkip", "pinStop", "pinIgnore"): return await ctx.send_help(ctx.invoked_with) now = time.monotonic() messages = await ctx.channel.history(limit=limit).flatten() total = 0 for message in messages: if message.pinned and pinMode == "pinStop": break elif message.pinned and pinMode == "pinSkip": continue try: await message.delete() total += 1 except discord.errors.HTTPException: pass delta = str(int(time.monotonic() - now)) await self.event.sudo(ctx, f"Purged {total} posts in {delta}s.") @commands.check(acl.check) @commands.bot_has_permissions(manage_channels=True) @commands.command(name="teacher_channel", aliases=["teacher-channel"]) async def teacher_channel(self, ctx, channel: discord.TextChannel): """Create subject channel will be visible for the subject's teacher, too channel: Subject channel to be duplicated """ if channel.name not in config.subjects: return await self.output.error( ctx, self.text.get("not_subject", channel=channel.mention)) ch = await channel.clone(name=channel.name + config.get("channels", "teacher suffix")) await ch.edit(position=channel.position + 1) await ctx.send(self.text.get("teacher_channel", channel=ch.mention)) await self.event.sudo(ctx, f"Teacher channel {ch.name}")
class Karma(rubbercog.Rubbercog): """Karma""" def __init__(self, bot): super().__init__(bot) self.config = CogConfig("karma") self.text = CogText("karma") ## ## Commands ## @commands.group(name="karma") async def karma(self, ctx): """Karma""" if ctx.invoked_subcommand is None: await self.karma_stalk(ctx, member=ctx.author) @commands.cooldown(rate=2, per=30, type=commands.BucketType.user) @karma.command(name="stalk") async def karma_stalk(self, ctx, member: discord.Member): """See someone's karma""" k = repo_k.get_karma(member.id) embed = self.embed( ctx=ctx, description=self.text.get("stalk_user", user=self.sanitise(member.display_name)), ) embed.add_field( name=self.text.get("stalk_karma"), value=f"**{k.karma.value}** ({k.karma.position}.)", inline=False, ) embed.add_field( name=self.text.get("stalk_positive"), value=f"**{k.positive.value}** ({k.positive.position}.)", ) embed.add_field( name=self.text.get("stalk_negative"), value=f"**{k.negative.value}** ({k.negative.position}.)", ) await ctx.send(embed=embed) await utils.room_check(ctx) @commands.cooldown(rate=2, per=30, type=commands.BucketType.user) @karma.command(name="emote", aliases=["emoji"]) async def karma_emote(self, ctx, emote: str): """See emote's karma""" if not self._isUnicode(emote): try: emote_id = int(self._emoteToID(emote)) emote = await ctx.guild.fetch_emoji(emote_id) except (ValueError, IndexError): return await utils.send_help(ctx) except discord.NotFound: return await ctx.send(self.text.get("emoji_not_found")) value = repo_k.emoji_value_raw(emote) if value is None: return await ctx.send(self.text.get("emoji_not_voted")) await ctx.send( self.text.get("emoji", emoji=str(emote), value=str(value))) await utils.room_check(ctx) @commands.guild_only() @commands.cooldown(rate=2, per=30, type=commands.BucketType.user) @karma.command(name="emotes", aliases=["emojis"]) async def karma_emotes(self, ctx): """See karma for all emotes""" emotes = await ctx.guild.fetch_emojis() emotes = [e for e in emotes if not e.animated] content = [] emotes_positive = self._getEmoteList(emotes, "1") if len(emotes_positive) > 0: content.append(self.text.get("emojis_positive")) content += self._emoteListToMessage(emotes_positive) emotes_neutral = self._getEmoteList(emotes, "0") if len(emotes_neutral) > 0: content.append(self.text.get("emojis_neutral")) content += self._emoteListToMessage(emotes_neutral) emotes_negative = self._getEmoteList(emotes, "-1") if len(emotes_negative) > 0: content.append(self.text.get("emojis_negative")) content += self._emoteListToMessage(emotes_negative) emotes_nonvoted = self._getNonvotedEmoteList(emotes) if len(emotes_nonvoted) > 0: content.append(self.text.get("emojis_not_voted")) content += self._emoteListToMessage(emotes_nonvoted) if len(content) == 0: content.append(self.text.get("no_emojis")) line = "" for items in [x for x in content]: if items[0] != "<": # description if len(line): await ctx.send(line) line = "" await ctx.send(items) continue if line.count("\n") >= 3: await ctx.send(line) line = "" line += "\n" + items await ctx.send(line) await utils.room_check(ctx) @commands.guild_only() @commands.check(acl.check) @karma.command(name="vote") async def karma_vote(self, ctx, emote: str = None): """Vote for emote's karma value""" if emote is None: emojis = await ctx.guild.fetch_emojis() emojis = [e for e in emojis if not e.animated] nonvoted = self._getNonvotedEmoteList(emojis) if len(nonvoted) == 0: return await ctx.author.send( self.text.get("all_emojis_voted", guild=ctx.guild.name)) emote = nonvoted[0] message = await ctx.send( self.text.get( "vote_info", emoji=emote, time=self.config.get("vote time"), limit=self.config.get("vote limit"), )) # set default of zero, so we can run the command multiple times repo_k.set_emoji_value(str(self._emoteToID(emote)), 0) # add options and vote await message.add_reaction("☑️") await message.add_reaction("0⃣") await message.add_reaction("❎") await self.event.sudo(ctx, f"Vote over value of {emote} started.") await asyncio.sleep(self.config.get("vote time") * 60) # update cached message message = await ctx.channel.fetch_message(message.id) positive = 0 negative = 0 neutral = 0 for reaction in message.reactions: if reaction.emoji == "☑️": positive = reaction.count - 1 elif reaction.emoji == "❎": negative = reaction.count - 1 elif reaction.emoji == "0⃣": neutral = reaction.count - 1 if positive + negative + neutral < self.config.get("vote limit"): await self.event.sudo(ctx, f"Vote for {emote} failed.") return await ctx.send(self.text.get("vote_failed", emoji=emote)) result = 0 if positive > negative + neutral: result = 1 elif negative > positive + neutral: result = -1 repo_k.set_emoji_value(str(self._emoteToID(emote)), result) await ctx.send(self.text.get("vote_result", emoji=emote, value=result)) await self.event.sudo(ctx, f"{emote} karma value voted as {result}.") @commands.check(acl.check) @karma.command(name="set") async def karma_set(self, ctx, emoji: discord.Emoji, value: int): """Set karma value without public vote""" repo_k.set_emoji_value(str(self._emoteToID(emoji)), value) await ctx.send(self.text.get("emoji", emoji=emoji, value=value)) await self.event.sudo(ctx, f"Karma of {emoji} set to {value}.") await utils.delete(ctx) @commands.cooldown(rate=2, per=30, type=commands.BucketType.user) @karma.command(name="message") async def karma_message(self, ctx, link: str): """Get karma for given message""" converter = commands.MessageConverter() try: message = await converter.convert(ctx=ctx, argument=link) except Exception as error: return await self.output.error(ctx, "Message not found", error) embed = self.embed(ctx=ctx, description=f"{message.author}") # fmt: off count = True if message.channel.id in self.config.get("banned channels") \ or ( not self.config.get("count subjects") and repo_s.get(message.channel.name) is not None ): count = False for word in self.config.get("banned words"): if word in message.content: count = False break # fmt: on output = {"negative": [], "neutral": [], "positive": []} karma = 0 for reaction in message.reactions: emote = reaction.emoji value = repo_k.emoji_value_raw(emote) if value == 1: output["positive"].append(emote) karma += reaction.count async for user in reaction.users(): if user.id == message.author.id: karma -= 1 break elif value == -1: output["negative"].append(emote) karma -= reaction.count async for user in reaction.users(): if user.id == message.author.id: karma += 1 break else: output["neutral"].append(emote) embed.add_field(name="Link", value=message.jump_url, inline=False) if count: for key, value in output.items(): if len(value) == 0: continue emotes = " ".join(str(emote) for emote in value) embed.add_field(name=self.text.get("embed_" + key), value=emotes) # fmt: off embed.add_field( name=self.text.get("embed_total"), value=f"**{karma}**", inline=False, ) # fmt: on else: embed.add_field(name="\u200b", value=self.text.get("embed_disabled"), inline=False) await ctx.send(embed=embed) await utils.room_check(ctx) @commands.check(acl.check) @karma.command(name="give") async def karma_give(self, ctx, member: discord.Member, value: int): """Give karma points to someone""" repo_k.update_karma(member=member, giver=ctx.author, emoji_value=value) await ctx.send(self.text.get("give", "given" if value > 0 else "taken")) await self.event.sudo(ctx, f"{member} got {value} karma points.") @commands.cooldown(rate=3, per=30, type=commands.BucketType.channel) @commands.command(aliases=["karmaboard"]) async def leaderboard(self, ctx, offset: int = 0): """Karma leaderboard""" await self.sendBoard(ctx, "desc", offset) @commands.cooldown(rate=3, per=30, type=commands.BucketType.channel) @commands.command() async def loserboard(self, ctx, offset: int = 0): """Karma leaderboard, from the worst""" await self.sendBoard(ctx, "asc", offset) @commands.cooldown(rate=3, per=30, type=commands.BucketType.channel) @commands.command() async def givingboard(self, ctx, offset: int = 0): """Karma leaderboard""" await self.sendBoard(ctx, "give", offset) @commands.cooldown(rate=3, per=30, type=commands.BucketType.channel) @commands.command(aliases=["stealingboard"]) async def takingboard(self, ctx, offset: int = 0): """Karma leaderboard""" await self.sendBoard(ctx, "take", offset) ## ## Listeners ## @commands.Cog.listener() async def on_raw_reaction_add(self, payload): """Karma add""" parsed_payload = await self._payloadToReaction(payload) if parsed_payload is None: return channel, member, message, emote = parsed_payload count = self.doCountKarma(member=member, message=message) if not count: return repo_k.karma_emoji(message.author, member, emote) @commands.Cog.listener() async def on_raw_reaction_remove(self, payload): """Karma remove""" parsed_payload = await self._payloadToReaction(payload) if parsed_payload is None: return channel, member, message, emote = parsed_payload count = self.doCountKarma(member=member, message=message) if not count: return repo_k.karma_emoji_remove(message.author, member, emote) @commands.Cog.listener() async def on_reaction_add(self, reaction, user): """Scrolling, vote""" if user.bot: return if reaction.message.channel.id == config.get("channels", "vote"): await self.checkVoteEmote(reaction, user) if str(reaction) in ("⏪", "◀", "▶"): await self.checkBoardEmoji(reaction, user) ## ## Helper functions ## def _isUnicode(self, text): demojized = demojize(text) if demojized.count(":") != 2: return False if demojized.split(":")[2] != "": return False return demojized != text def _getEmoteList(self, guild_emotes: list, value: int) -> list: db_emotes = repo_k.getEmotesByValue(value) result = [] for guild_emote in guild_emotes: if str(guild_emote.id) in db_emotes: result.append(guild_emote) return result def _getNonvotedEmoteList(self, guild_emotes: list) -> list: db_emotes = [x.emoji_ID for x in repo_k.get_all_emojis()] result = [] for guild_emote in guild_emotes: if str(guild_emote.id) not in db_emotes: result.append(guild_emote) return result def _emoteListToMessage(self, emotes: list) -> List[str]: line = "" result = [] for i, emote in enumerate(emotes): if i % 8 == 0: result.append(line) line = "" line += f"{emote} " result.append(line) return [r for r in result if len(r) > 0] def _emoteToID(self, emote: str): if ":" in str(emote): return int(str(emote).split(":")[2][:-1]) return emote async def _payloadToReaction( self, payload: discord.RawReactionActionEvent) -> tuple: """Return (channel, member, message, emote)""" channel = self.bot.get_channel(payload.channel_id) if channel is None or not isinstance(channel, discord.TextChannel): return member = channel.guild.get_member(payload.user_id) if member is None or member.bot: return try: message = await channel.fetch_message(payload.message_id) except discord.NotFound: return if message is None: return if payload.emoji.is_custom_emoji(): emote = payload.emoji.id else: emote = payload.emoji.name return channel, member, message, emote async def _remove_reaction(self, reaction, user): try: await reaction.remove(user) except: pass ## ## Logic ## def doCountKarma(self, *, member: discord.Member, message: discord.Message) -> bool: """Return True only if the message should be counted""" # do not count author's reactions if member.id == message.author.id: return False # only count master and slave guilds if message.guild.id not in (config.guild_id, config.slave_id): return False # do not count banned channels if message.channel.id in self.config.get("banned channels"): return False # do not count banned roles if self.config.get("banned roles") in map(lambda x: x.id, member.roles): return False # do not count banned strings for word in self.config.get("banned words"): if word in message.content: return False # optionally, do not count subject channels if not self.config.get("count subjects"): if repo_s.get(message.channel.name) is not None: return False return True async def sendBoard(self, ctx: commands.Context, parameter: str, offset: int): """Send karma board parameter: desc | asc | give | take """ # convert offset to be zero-base offset -= 1 max_offset = repo_k.getMemberCount() - self.config.get( "leaderboard limit") if offset < 0: offset = 0 elif offset > max_offset: offset = max_offset # fmt: off title = "{title} {ordering}".format( title=self.text.get("board_title"), ordering=self.text.get("board_" + parameter + "_t"), ) # fmt: on description = self.text.get("board_" + parameter + "_d") embed = self.embed(ctx=ctx, title=title, description=description) embed = self.fillBoard(embed, member=ctx.author, order=parameter, offset=offset) message = await ctx.send(embed=embed) await message.add_reaction("⏪") await message.add_reaction("◀") await message.add_reaction("▶") await utils.room_check(ctx) def fillBoard(self, embed, *, member, order: str, offset: int) -> discord.Embed: limit = self.config.get("leaderboard limit") template = "`{position:>2}` … `{karma:>5}` {username}" # get repository parameters column = "karma" if order == "give": column = "positive" elif order == "take": column = "negative" if order == "desc": attr = DB_Karma.karma.desc() elif order == "asc": attr = DB_Karma.karma elif order == "give": attr = DB_Karma.positive.desc() elif order == "take": attr = DB_Karma.negative.desc() # construct first field value = [] board = repo_k.getLeaderboard(attr, offset, limit) if not board or not board.count(): return None user_in_list = False for i, db_user in enumerate(board, start=offset): # fmt: off user = self.getGuild().get_member(int(db_user.discord_id)) username = (self.sanitise(user.display_name) if hasattr( user, "display_name") else "_unknown_") if int(db_user.discord_id) == member.id: username = f"**{username}**" user_in_list = True value.append( template.format( position=i + 1, karma=getattr(db_user, column), username=username, )) # fmt: on embed.clear_fields() if offset == 0: name = self.text.get("board_1", num=limit) else: name = self.text.get("board_x", num=limit, offset=offset + 1) embed.add_field(name=name, value="\n".join(value), inline=False) # construct user field, if they are not included in first one if not user_in_list: k = repo_k.get_karma(member.id) # get right values if order in ("desc", "asc"): value, position = k.karma.value, k.karma.position elif order == "give": value, position = k.positive.value, k.positive.position else: value, position = k.negative.value, k.negative.position username = "******" + self.sanitise(member.display_name) + "**" embed.add_field( name=self.text.get("board_user"), value=template.format(position=position, karma=value, username=username), ) return embed async def checkVoteEmote(self, reaction, user): """Check if the emote is vote emote""" if not hasattr(reaction, "emoji"): return await self._remove_reaction(reaction, user) if not reaction.message.content.startswith( self.text.get("vote_info")[:25]): return if str(reaction.emoji) not in ("☑️", "0⃣", "❎"): await self._remove_reaction(reaction, user) async def checkBoardEmoji(self, reaction, user): """Check if the leaderboard should be scrolled""" if user.bot: return if str(reaction) not in ("⏪", "◀", "▶"): return # fmt: off if len(reaction.message.embeds) != 1 \ or type(reaction.message.embeds[0].title) != str \ or not reaction.message.embeds[0].title.startswith(self.text.get("board_title")): return # fmt: on embed = reaction.message.embeds[0] # get ordering for o in ("desc", "asc", "give", "take"): if embed.title.endswith(self.text.get("board_" + o + "_t")): order = o break # get current offset if "," in embed.fields[0].name: offset = int(embed.fields[0].name.split(" ")[-1]) - 1 else: offset = 0 # get new offset if str(reaction) == "⏪": offset = 0 elif str(reaction) == "◀": offset -= self.config.get("leaderboard limit") elif str(reaction) == "▶": offset += self.config.get("leaderboard limit") if offset < 0: offset = 0 # apply embed = self.fillBoard(embed, member=user, order=order, offset=offset) if embed: await reaction.message.edit(embed=embed) await utils.remove_reaction(reaction, user)
class Base(rubbercog.Rubbercog): """About""" def __init__(self, bot: commands.Bot): super().__init__(bot) self.config = CogConfig("base") self.text = CogText("base") self.status_loop.start() self.status = "online" def cog_unload(self): self.status_loop.cancel() ## ## Loops ## @tasks.loop(minutes=1) async def status_loop(self): """Observe latency to the Discord API. If it goes below 0.25s, "online" will be switched to "idle", over 0.50s is "dnd". """ if self.bot.latency <= 0.25: status = "online" elif self.bot.latency <= 0.5: status = "idle" else: status = "dnd" if self.status != status: self.status = status await self.console.debug( "latency", f"Updating status to {status} (latency {self.bot.latency:.2f})." ) await utils.set_presence(self.bot, status=getattr(discord.Status, status)) @status_loop.before_loop async def before_status_loop(self): if not self.bot.is_ready(): await self.bot.wait_until_ready() ## ## Commands ## @commands.cooldown(rate=1, per=10.0, type=commands.BucketType.channel) @commands.command() async def uptime(self, ctx): """Bot uptime""" now = datetime.datetime.now().replace(microsecond=0) delta = now - boottime embed = self.embed(ctx=ctx) embed.add_field(name="Boot", value=str(boottime), inline=False) embed.add_field(name="Uptime", value=str(delta), inline=False) await ctx.send(embed=embed) await utils.delete(ctx.message) @commands.cooldown(rate=1, per=10.0, type=commands.BucketType.channel) @commands.command() async def ping(self, ctx): """Bot latency""" await ctx.reply("pong: **{:.2f} s**".format(self.bot.latency)) ## ## Listeners ## @commands.Cog.listener() async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): """Pinning functionality""" channel = self.bot.get_channel(payload.channel_id) if channel is None or not isinstance(channel, discord.TextChannel): return if payload.emoji.name not in ("📌", "📍", "🔖"): return try: message = await channel.fetch_message(payload.message_id) except discord.errors.NotFound: return if payload.emoji.is_custom_emoji(): return reaction_author: discord.User = self.bot.get_user(payload.user_id) if reaction_author.bot: return if message.type != discord.MessageType.default: await message.remove_reaction(payload.emoji, reaction_author) return if payload.emoji.name == "📍" and not reaction_author.bot: await reaction_author.send(self.text.get("bad pin")) await message.remove_reaction(payload.emoji, reaction_author) return if payload.emoji.name == "🔖": await self.bookmark_message(message, reaction_author) await message.remove_reaction(payload.emoji, reaction_author) return for reaction in message.reactions: if reaction.emoji != "📌": continue if message.pinned: return await reaction.clear() if channel.id in self.config.get("unpinnable"): return await reaction.clear() if reaction.count < self.config.get("pins"): return users = await reaction.users().flatten() user_names = ", ".join([str(user) for user in users]) log_embed = self.embed(title=self.text.get("pinned"), description=user_names) if len(message.content): value = utils.id_to_datetime( message.id).strftime("%Y-%m-%d %H:%M:%S") log_embed.add_field(name=str(message.author), value=value) url_text = self.text.get( "link text", channel=channel.name, guild=channel.guild.name, ) if len(message.content): log_embed.add_field( name=self.text.get("content"), value=message.content[:1024], inline=False, ) if len(message.content) >= 1024: log_embed.add_field( name="\u200b", value=message.content[1024:], inline=False, ) if len(message.attachments): log_embed.add_field( name=self.text.get("content"), value=self.text.get("attachments", count=len(message.attachments)), inline=False, ) log_embed.add_field( name=self.text.get("link"), value=f"[{url_text}]({message.jump_url})", inline=False, ) try: await message.pin() except discord.errors.HTTPException as e: await self.event.user(channel, "Could not pin message.", e) error_embed = self.embed( title=self.text.get("pin error"), description=user_names, url=message.jump_url, ) await message.channel.send(embed=error_embed) return event_channel = self.bot.get_channel( config.get("channels", "events")) await event_channel.send(embed=log_embed) await reaction.clear() await message.add_reaction("📍") async def bookmark_message( self, message: discord.Message, user: Union[discord.Member, discord.User], ): embed = self.embed( title=self.text.get("bookmark", "title"), description=message.content, author=message.author, ) timestamp = utils.id_to_datetime( message.id).strftime("%Y-%m-%d %H:%M:%S") embed.add_field( name=self.sanitise(message.author.display_name), value=self.text.get( "bookmark", "info", timestamp=timestamp, channel=message.channel.name, link=message.jump_url, ), inline=False, ) info = set() if len(message.attachments): embed.add_field( name=self.text.get("bookmark", "files"), value=self.text.get("bookmark", "total", count=len(message.attachments)), ) if len(message.embeds): embed.add_field( name=self.text.get("bookmark", "embeds"), value=self.text.get("bookmark", "total", count=len(message.embeds)), ) await user.send(embed=embed) await self.event.user( user, f"Bookmarked message in #{message.channel.name}\n> {message.jump_url}", escape_markdown=False, )
class Stalker(rubbercog.Rubbercog): """A cog for database lookups""" def __init__(self, bot: commands.Bot): super().__init__(bot) self.text = CogText("stalker") def dbobj2email(self, user): if user is not None: if user.group == "FEKT" and "@" not in user.login: email = user.login + "@stud.feec.vutbr.cz" elif user.group == "VUT" and "@" not in user.login: email = user.login + "@vutbr.cz" else: email = user.login return email return @commands.check(acl.check) @commands.group(name="whois", aliases=["gdo"]) async def whois(self, ctx: commands.Context): """Get information about user""" await utils.send_help(ctx) @commands.check(acl.check) @whois.command(name="member", aliases=["tag", "user", "id"]) async def whois_member(self, ctx: commands.Context, member: discord.Member): """Get information about guild member member: A guild member """ db_member = repository.get(member.id) embed = self.whois_embed(ctx, member, db_member) await ctx.send(embed=embed) await self.event.sudo(ctx, f"Database lookup for member **{member}**.") @commands.check(acl.check) @whois.command(name="email", aliases=["login", "xlogin"]) async def whois_email(self, ctx: commands.Context, email: str = None): """Get information about xlogin email: An e-mail """ db_member = repository.getByLogin(email) if db_member is None: return self.output.info(ctx, self.text.get("not_found")) member = self.getGuild().get_member(db_member.discord_id) if member is None: return self.output.info(ctx, self.text.get("not_in_guild")) embed = self.whois_embed(ctx, member, db_member) await ctx.send(embed=embed) await self.event.sudo(ctx, f"Database lookup for e-mail **{email}**.") @commands.check(acl.check) @whois.command(name="logins", aliases=["emails"]) async def whois_logins(self, ctx, prefix: str): """Filter database by login""" users = repository.getByPrefix(prefix=prefix) # parse data items = [] template = "`{name:<10}` … {email}" for user in users: member = self.bot.get_user(user.discord_id) name = member.name if member is not None else "" email = self.dbobj2email(user) items.append(template.format(name=name, email=email)) # construct embed fields fields = [] field = "" for item in items: if len(field + item) > 1000: fields.append(field) field = "" field = field + "\n" + item fields.append(field) # create embed embed = self.embed(ctx=ctx, description=self.text.get("prefix", "result", num=len(users))) for field in fields[:5]: # there is a limit of 6000 characters in total embed.add_field(name="\u200b", value=field) if len(fields) > 5: embed.add_field( name=self.text.get("prefix", "too_many"), value=self.text.get("prefix", "omitted"), inline=False, ) await ctx.send(embed=embed) await self.event.sudo(ctx, f"Database lookup for e-mail prefix **{prefix}**.") @commands.check(acl.check) @commands.group(aliases=["db"]) async def database(self, ctx: commands.Context): """Manage users""" await utils.send_help(ctx) @commands.check(acl.check) @database.command(name="add") async def database_add( self, ctx: commands.Context, member: discord.Member = None, login: str = None, group: discord.Role = None, ): """Add user to database member: A server member login: e-mail group: A role from `roles_native` or `roles_guest` in config file """ if member is None or login is None or group is None: return await utils.send_help(ctx) # define variables guild = self.bot.get_guild(config.guild_id) verify = discord.utils.get(guild.roles, name="VERIFY") if repository.get(member.id) is not None: return await self.output.error(ctx, self.text.get("db", "duplicate")) try: repository.add( discord_id=member.id, login=login, group=group.name, status="verified", code="MANUAL", ) except Exception as e: return await self.output.error(ctx, self.text.get("db", "write_error"), e) # assign roles, if neccesary if verify not in member.roles: await member.add_roles(verify) if group not in member.roles: await member.add_roles(group) # display the result embed = self.whois_embed(ctx, member, repository.get(member.id)) await ctx.send(embed=embed) await self.event.sudo(ctx, f"New user {member} ({group.name})") @commands.check(acl.check) @database.command(name="remove", aliases=["delete"]) async def database_remove(self, ctx: commands.Context, member: discord.Member): """Remove user from database""" result = repository.deleteId(discord_id=member.id) if result < 1: return await self.output.error(ctx, self.text.get("db", "delete_error")) await ctx.send(self.text.get("db", "delete_success", num=result)) await self.event.sudo(ctx, f"Member {member} ({member.id}) removed from database.") @commands.check(acl.check) @database.command(name="update") async def database_update(self, ctx, member: discord.Member, key: str, *, value): """Update user entry in database key: value - login: e-mail - group: one of the groups defined in gatekeeper mapping - status: [unknown, pending, verified, kicked, banned] - comment: commentary on user """ if key not in ("login", "group", "status", "comment"): return await self.output.error(ctx, self.text.get("db", "invalid_key")) if key == "login": repository.update(member.id, login=value) elif key == "group": # get list of role names, defined in role_ids = config.get("roles", "native") + config.get("roles", "guests") role_names = [ x.name for x in [self.getGuild().get_role(x) for x in role_ids] if hasattr(x, "name") ] value = value.upper() if value not in role_names: return await self.output.error(ctx, self.text.get("db", "invalid_value")) repository.update(member.id, group=value) elif key == "status": if value not in ("unknown", "pending", "verified", "kicked", "banned"): return await self.output.error(ctx, self.text.get("db", "invalid_value")) repository.update(member.id, status=value) elif key == "comment": repository.update(member.id, comment=value) await self.event.sudo(ctx, f"Updated {member}: {key} = {value}.") await ctx.send(self.text.get("db", "update_success")) @commands.check(acl.check) @database.command(name="show") async def database_show(self, ctx, param: str): """Filter users by parameter param: [unverified, pending, kicked, banned] """ if param not in ("unverified", "pending", "kicked", "banned"): return await utils.send_help(ctx) await self._database_show_filter(ctx, param) @commands.check(acl.check) @commands.command(name="guild", aliases=["server"]) async def guild(self, ctx: commands.Context): """Display general about guild""" embed = self.embed(ctx=ctx) g = self.getGuild() # guild embed.add_field( name=f"Guild **{g.name}**", inline=False, value=f"Created {g.created_at.strftime('%Y-%m-%d')}," f" owned by **{g.owner.name}**", ) # verification states = ", ".join( "**{}** {}".format(repository.countStatus(state), state) for state in config.db_states ) embed.add_field(name="Verification states", value=states, inline=False) # roles role_ids = config.get("roles", "native") + config.get("roles", "guests") roles = [] for role_id in role_ids: role = self.getGuild().get_role(role_id) if role is not None: roles.append(f"**{role}** {repository.countGroup(role.name)}") else: roles.append(f"**{role_id}** {repository.countGroup(role_id)}") roles = ", ".join(roles) embed.add_field(name="Roles", value=f"Total count {len(g.roles)}\n{roles}", inline=False) # channels embed.add_field( name=f"{len(g.categories)} categories", value=f"{len(g.text_channels)} text channels, {len(g.voice_channels)} voice channels", ) # users embed.add_field( name="Users", value=f"Total count **{g.member_count}**, {g.premium_subscription_count} boosters", ) await ctx.send(embed=embed) ## ## Logic ## async def _database_show_filter(self, ctx: commands.Context, status: str = None): """Helper function for all databas_show_* functions""" if status is None or status not in config.db_states: return await utils.send_help(ctx) users = repository.filterStatus(status=status) embed = self.embed(ctx=ctx) embed.add_field(name="Result", value="{} users found".format(len(users)), inline=False) if users: embed.add_field(name="-" * 60, value="LIST:", inline=False) for user in users: member = discord.utils.get(self.getGuild().members, id=user.discord_id) if member: name = "**{}**, {}".format(member.name, member.id) else: name = "**{}**, {} _(not on server)_".format(user.discord_id, user.group) d = user.changed date = (d[:4] + "-" + d[4:6] + "-" + d[6:]) if (d and len(d) == 8) else "_(none)_" embed.add_field( name=name, value="{}\nLast action on {}".format(self.dbobj2email(user), date) ) await ctx.send(embed=embed, delete_after=config.delay_embed) await utils.delete(ctx) def whois_embed(self, ctx, member: discord.Member, db_member: object) -> discord.Embed: """Construct the whois embed""" embed = self.embed(ctx=ctx, title="Whois", description=member.mention) embed.add_field( name=self.text.get("whois", "information"), value=self.text.get( "whois", "account_information", name=self.sanitise(member.display_name), account_since=utils.id_to_datetime(member.id).strftime("%Y-%m-%d"), member_since=member.joined_at.strftime("%Y-%m-%d"), ), inline=False, ) if db_member is not None: embed.add_field( name=self.text.get("whois", "email"), value=self.dbobj2email(db_member) if db_member.login else self.text.get("whois", "missing"), ) embed.add_field( name=self.text.get("whois", "code"), value=db_member.code if db_member.code else self.text.get("whois", "missing"), ) embed.add_field( name=self.text.get("whois", "status"), value=db_member.status if db_member.status else self.text.get("whois", "missing"), ) embed.add_field( name=self.text.get("whois", "group"), value=db_member.group if db_member.group else self.text.get("whois", "missing"), ) embed.add_field( name=self.text.get("whois", "changed"), value=db_member.changed if db_member.changed else self.text.get("whois", "missing"), ) if db_member.comment and len(db_member.comment): embed.add_field( name=self.text.get("whois", "comment"), value=db_member.comment, inline=False ) role_list = ", ".join(list((r.name) for r in member.roles[::-1])[:-1]) embed.add_field( name=self.text.get("whois", "roles"), value=role_list if len(role_list) else self.text.get("whois", "missing"), ) return embed @commands.check(acl.check) @commands.command(name="channelinfo", aliases=['ci']) async def channelinfo(self, ctx: commands.Context, channel: discord.TextChannel = None): if channel is None: channel = ctx.channel webhooks = await channel.webhooks() channel_embed = discord.Embed( title=f"Information about `#{str(channel)}`", description="```css\nRole overwrites```", colour = discord.Color.green() ) channel_embed.set_footer(text=f"Channel ID: {channel.id}") channel_embed.add_field(name="Channel topic", value=channel.topic) channel_embed.add_field(name="Number of people in channel", value=len(channel.members)) channel_embed.add_field(name="Webhooks", value=len(webhooks)) roles = [] users = [] for overwrite in channel.overwrites: if isinstance(overwrite, discord.Role): roles.append(overwrite) else: users.append(overwrite) if roles: channel_embed.description += '\n'.join( [f"{count}) {role.mention} (Permissions value: {role.permissions.value})" for count, role in enumerate(roles, start=1)] ) if users: channel_embed.description += '\n\n```css\nUser overwrites```' + '\n'.join( [f"{count}) {user.mention} (Permissions value: {channel.permissions_for(user).value})" for count, user in enumerate(users, start=1)] ) await ctx.channel.send(embed=channel_embed)
class Review(rubbercog.Rubbercog): """Subject reviews""" def __init__(self, bot): super().__init__(bot) self.text = CogText("review") ## ## Commands ## @commands.cooldown(rate=5, per=60, type=commands.BucketType.user) @commands.check(acl.check) @commands.group(name="review") async def review(self, ctx): """Manage your subject reviews""" await utils.send_help(ctx) @commands.check(acl.check) @review.command(name="subject", aliases=["see"]) async def review_subject(self, ctx, subject: str): """See subject's reviews""" db_subject = repo_s.get(subject) if db_subject is None: return await ctx.send(self.text.get("no_subject")) title = self.text.get("embed", "title") + subject name = db_subject.name if db_subject.name is not None else discord.Embed.Empty if name is not discord.Embed.Empty and db_subject.category is not None: name += f" ({db_subject.category})" db_reviews = repo_r.get_subject_reviews(subject) if db_reviews.count() == 0: return await ctx.send( self.text.get("no_reviews", mention=ctx.author.mention)) _total = 0 for db_review in db_reviews: _total += db_review.Review.tier average = _total / db_reviews.count() review = db_reviews[0].Review embed = self.embed(ctx=ctx, title=title, description=name, page=(1, db_reviews.count())) embed = self.fill_subject_embed(embed, review, average) message = await ctx.send(embed=embed) if db_reviews.count() > 1: await message.add_reaction("◀") await message.add_reaction("▶") if db_reviews.count() > 0: await message.add_reaction("👍") await message.add_reaction("🛑") await message.add_reaction("👎") @commands.check(acl.check) @review.command(name="list", aliases=["available"]) async def review_list(self, ctx): """Get list of reviewed subjects""" subjects = set() for r in repo_r.get_all_reviews(): subjects.add(r.subject) if not len(subjects): return await ctx.send(self.text.get("empty"), mention=ctx.author.mention) await ctx.send(">>> " + ", ".join(f"`{s}`" for s in sorted(subjects))) @commands.check(acl.check) @review.command(name="my-list") async def review_mylist(self, ctx): """Get list of your reviewed subjects""" subjects = set() for r in repo_r.get_all_reviews(): if r.discord_id == ctx.author.id: subjects.add(r.subject) if not len(subjects): return await ctx.send(self.text.get("empty"), mention=ctx.author.mention) await ctx.send(">>> " + ", ".join(f"`{s}`" for s in sorted(subjects))) @commands.check(acl.check) @review.command(name="add", aliases=["update"]) async def review_add(self, ctx, subject: str, mark: int, *, text: str): """Add a review subject: Subject code mark: 1-5 (one being best) text: Your review """ result = await self.add_review(ctx, subject, mark, text, anonymous=False) if result is not None: await self.event.user( ctx, f"Review **#{result.id}** for **{subject}**.") await ctx.send(self.text.get("added")) @commands.check(acl.check) @review.command(name="add-anonymous", aliases=["anonymous", "anon"]) async def review_add_anonymous(self, ctx, subject: str, mark: int, *, text: str): """Add anonymous review subject: Subject code mark: 1-5 (one being best) text: Your review """ result = await self.add_review(ctx, subject, mark, text, anonymous=True) if result is not None: await utils.delete(ctx.message) await self.event.user( ctx, f"Anonymous review **#{result.id}** for **{subject}**.") await ctx.send(self.text.get("added")) @commands.check(acl.check) @review.command(name="remove") async def review_remove(self, ctx, subject: str): """Remove your review subject: Subject code """ review = repo_r.get_review_by_author_subject(ctx.author.id, subject) if review is None: return await ctx.send( self.text.get("no_review", mention=ctx.author.mention)) repo_r.remove(review.id) await self.event.user(ctx, f"Removed review for **{subject}**.") return await ctx.send(self.text.get("removed")) @commands.check(acl.check) @commands.group(name="sudo_review") async def sudo_review(self, ctx): """Manage other user's reviews""" await utils.send_help(ctx) @commands.check(acl.check) @sudo_review.command(name="remove") async def sudo_review_remove(self, ctx, id: int): """Remove someone's review""" db_review = repo_r.get(id) if db_review is None: return await ctx.send( self.text.get("no_review", mention=ctx.author.mention)) repo_r.remove(id) await self.event.sudo(ctx, f"Review **#{id}** removed") return await ctx.send(self.text.get("removed")) @commands.check(acl.check) @commands.group(name="subject") async def subject(self, ctx): """Manage subjects""" await utils.send_help(ctx) @commands.check(acl.check) @subject.command(name="info") async def subject_info(self, ctx, subject: str): """Get information about subject subject: Subject code """ db_subject = repo_s.get(subject) if db_subject is None: return await ctx.send(self.text.get("no_subject")) embed = self.embed(ctx=ctx, title=db_subject.shortcut) if db_subject.name or db_subject.category: embed.add_field( name=db_subject.name or "\u200b", value=db_subject.category or "\u200b", inline=False, ) embed.add_field( name=self.text.get("info", "reviews"), value=self.text.get("info", "count", count=len(db_subject.reviews), subject=subject) if len(db_subject.reviews) else self.text.get("info", "none"), inline=False, ) await ctx.send(embed=embed) @commands.check(acl.check) @subject.command(name="add") async def subject_add(self, ctx, subject: str, name: str, category: str): """Add subject subject: Subject code name: Subject name category: Subject faculty or other assignment """ db_subject = repo_s.get(subject) if db_subject is not None: return await ctx.send(self.text.get("subject_exists")) repo_s.add(subject, name, category) await self.event.sudo(ctx, f"Subject **{subject}** added.") await ctx.send(self.text.get("subject_added")) @commands.check(acl.check) @subject.command(name="update") async def subject_update(self, ctx, subject: str, name: str, category: str): """Update subject subject: Subject code name: Subject name category: Subject faculty or other assignment """ db_subject = repo_s.get(subject) if db_subject is None: return await ctx.send(self.text.get("no_subject")) repo_s.update(subject, name=name, category=category) await self.event.sudo(ctx, f"Subject **{subject}** updated.") await ctx.send(self.text.get("subject_updated")) @commands.check(acl.check) @subject.command(name="remove") async def subject_remove(self, ctx, subject: str): """Remove subject subject: Subject code """ db_subject = repo_s.get(subject) if db_subject is None: return await ctx.send(self.text.get("no_subject")) repo_s.remove(subject) await self.event.sudo(ctx, f"Subject **{subject}** removed.") await ctx.send(self.text.get("subject_removed")) ## ## Listeners ## @commands.Cog.listener() async def on_reaction_add(self, reaction: discord.Reaction, user: discord.User): # check for wanted embed if (user.bot or len(reaction.message.embeds) != 1 or not isinstance(reaction.message.embeds[0].title, str) or not reaction.message.embeds[0].title.startswith( self.text.get("embed", "title"))): return scroll = False vote = False scroll_delta = 0 vote_value = 0 # check for scroll availability if hasattr(reaction, "emoji"): # scrolling if str(reaction.emoji) == "◀": scroll = True scroll_delta = -1 elif str(reaction.emoji) == "▶": scroll = True scroll_delta = 1 # voting elif str(reaction.emoji) == "👍": vote = True vote_value = 1 elif str(reaction.emoji) == "🛑": vote = True vote_value = 0 elif str(reaction.emoji) == "👎": vote = True vote_value = -1 # invalid else: # invalid reaction return await self._remove_reaction(reaction, user) else: # invalid reaction return await self._remove_reaction(reaction, user) embed = reaction.message.embeds[0] if embed.footer == discord.Embed.Empty or " | " not in embed.footer.text: return await self._remove_reaction(reaction, user) # get reviews for given subject subject = embed.title.replace(self.text.get("embed", "title"), "") reviews = repo_r.get_subject_reviews(subject) _total = 0 for review in reviews: _total += review.Review.tier average = _total / reviews.count() # get page footer_text = embed.footer.text if scroll: pages = footer_text.split(" | ")[-1] page_current = int(pages.split("/")[0]) - 1 page = (page_current + scroll_delta) % reviews.count() footer_text = footer_text.replace(pages, f"{page+1}/{reviews.count()}") else: page = 0 # get new review review = reviews[page].Review if vote: # apply vote if user.id == review.discord_id: return await self._remove_reaction(reaction, user) if vote_value == 0: repo_r.remove_vote(review.id, str(user.id)) else: repo_r.add_vote(review.id, vote_value == 1, str(user.id)) # update embed embed = self.fill_subject_embed(embed, review, average) embed.set_footer(text=footer_text, icon_url=embed.footer.icon_url) await reaction.message.edit(embed=embed) await self._remove_reaction(reaction, user) ## ## Helper functions ## async def _remove_reaction(self, reaction, user): try: await reaction.remove(user) except Exception: pass ## ## Logic ## def fill_subject_embed(self, embed: discord.Embed, review: object, average: float) -> discord.Embed: # reset any previous embed.clear_fields() # add content # fmt: off name = self.bot.get_user(int(review.discord_id)) or self.text.get( "embed", "no_user") if review.anonym: name = self.text.get("embed", "anonymous") embed.add_field( inline=False, name=self.text.get("embed", "num", num=str(review.id)), value=self.text.get("embed", "average", num=f"{average:.1f}"), ) embed.add_field(name=name, value=review.date) embed.add_field(name=self.text.get("embed", "mark"), value=review.tier) embed.add_field( inline=False, name=self.text.get("embed", "text"), value=review.text_review[:1024], ) if len(review.text_review) > 1024: embed.add_field( inline=False, name="\u200b", value=review.text_review[1024:], ) # fmt: on embed.add_field(name="👍", value=f"{repo_r.get_votes_count(review.id, True)}") embed.add_field(name="👎", value=f"{repo_r.get_votes_count(review.id, False)}") return embed async def add_review(self, ctx, subject: str, mark: int, text: str, anonymous: bool): """Add and return review""" if mark < 1 or mark > 5: return await ctx.send(self.text.get("wrong_mark")) # check if subject is in database db_subject = repo_s.get(subject) if db_subject is None: await ctx.send(self.text.get("no_subject")) return if text is None or not len(text): await ctx.send(self.text.get("no_text")) return past_review = repo_r.get_review_by_author_subject( ctx.author.id, subject) if past_review is None: # add result = repo_r.add_review(ctx.author.id, subject, mark, anonymous, text) else: # update result = repo_r.update_review(past_review.id, mark, anonymous, text) return result
class Warden(rubbercog.Rubbercog): """Repost detector""" def __init__(self, bot): super().__init__(bot) self.config = CogConfig("warden") self.text = CogText("warden") self.limit_full = 3 self.limit_hard = 7 self.limit_soft = 14 @commands.check(acl.check) @commands.group() async def scan(self, ctx): """Scan for reposts""" await utils.send_help(ctx) @commands.check(acl.check) @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) @commands.bot_has_permissions(read_message_history=True) @scan.command(name="history") async def scan_history(self, ctx, limit: int): """Scan current channel for images and save them as hashes. limit: How many messages should be scanned. Negative to scan all. """ if limit < 0: limit = None async with ctx.typing(): messages = await ctx.channel.history(limit=limit).flatten() status = await ctx.send(self.text.get("scan_history", "title", count=len(messages))) await asyncio.sleep(1) ctr_nofile: int = 0 ctr_hashes: int = 0 now = time.time() for i, message in enumerate(messages, 1): if i % 20 == 0: await status.edit( content=self.text.get( "scan_history", "scanning", count=i, total=len(messages), percent="{:.1f}".format(i / len(messages) * 100), hashes=ctr_hashes, ) ) if not len(message.attachments): ctr_nofile += 1 continue hashes = [x async for x in self.save_hashes(message)] ctr_hashes += len(hashes) await status.edit( content=self.text.get( "scan_history", "complete", messages=len(messages), hashes=ctr_hashes, seconds="{:.1f}".format(time.time() - now), ) ) @commands.check(acl.check) @scan.command(name="compare", aliases=["messages"]) async def scan_compare(self, ctx, messages: commands.Greedy[discord.Message]): """Display hashes of given messages. messages: Space separated list of messages. """ text = [] for message in messages: db_images = repo_i.get_by_message(message.id) if not len(db_images): continue text.append(self.text.get("compare", "header", message_id=message.id)) for db_image in db_images: text.append(self.text.get("compare", "line", hash=db_image.dhash[2:])) text.append("") if not len(text): return await ctx.send("compare", "not_found") await ctx.send("\n".join(text)) # def _in_repost_channel(self, message: discord.Message) -> bool: if message.channel.id not in self.config.get("deduplication channels"): return False if message.attachments is None or not len(message.attachments): return False if message.author.bot: return False return True @commands.Cog.listener() async def on_message(self, message: discord.Message): if self._in_repost_channel(message): await self.check_message(message) @commands.Cog.listener() async def on_raw_message_delete(self, payload: discord.RawMessageDeleteEvent): repo_i.delete_by_message(payload.message_id) @commands.Cog.listener() async def on_message_delete(self, message: discord.Message): if not self._in_repost_channel(message): return # try to detect and delete repost embed messages = await message.channel.history( after=message, limit=3, oldest_first=True ).flatten() for report in messages: if not report.author.bot: continue if len(report.embeds) != 1 or type(report.embeds[0].footer.text) != str: continue if str(message.id) != report.embeds[0].footer.text.split(" | ")[1]: continue try: await report.delete() except discord.errors.HTTPException as e: await self.console.error(message, "Could not delete repost report.", e) break @commands.Cog.listener() async def on_reaction_add(self, reaction, user): """Handle 'This is a repost' report. The footer contains reposter's user ID and repost message id. """ if reaction.message.channel.id not in self.config.get("deduplication channels"): return if user.bot: return if not reaction.message.author.bot: return emoji = str(reaction.emoji) if emoji != "❎": return try: repost_message_id = int(reaction.message.embeds[0].footer.text.split(" | ")[1]) repost_message = await reaction.message.channel.fetch_message(repost_message_id) except discord.errors.HTTPException: return await self.console.error(reaction.message, "Could not find the repost message.") for report_reaction in reaction.message.reactions: if str(report_reaction) != "❎": continue if ( emoji == "❎" and str(report_reaction) == "❎" and report_reaction.count > self.config.get("not duplicate limit") ): # remove bot's reaction, it is not a repost try: await repost_message.remove_reaction("♻️", self.bot.user) await repost_message.remove_reaction("🤷🏻", self.bot.user) await repost_message.remove_reaction("🤔", self.bot.user) except discord.errors.HTTPException as exc: return await self.console.error( reaction.message, "Could not remove bot's reaction.", exc ) return await utils.delete(reaction.message) # async def save_hashes(self, message: discord.Message): for attachment in message.attachments: if attachment.size > self.config.get("max_size") * 1024: continue extension = attachment.filename.split(".")[-1].lower() if extension not in ("jpg", "jpeg", "png", "webp", "gif"): continue fp = BytesIO() await attachment.save(fp) try: image = Image.open(fp) except OSError: continue h = dhash.dhash_int(image) repo_i.add_image( channel_id=message.channel.id, message_id=message.id, attachment_id=attachment.id, dhash=str(hex(h)), ) yield h async def check_message(self, message: discord.Message): """Check if message contains duplicate image.""" image_hashes = [x async for x in self.save_hashes(message)] if len(message.attachments) > len(image_hashes): await message.add_reaction("▶") await asyncio.sleep(2) await message.remove_reaction("▶", self.bot.user) duplicates = {} all_images = None for image_hash in image_hashes: # try to look up hash directly images = repo_i.get_hash(str(hex(image_hash))) for image in images: # skip current message if image.message_id == message.id: continue # add to duplicates duplicates[image] = 0 await self.console.debug(message, "Full dhash match found.") break # move on to the next hash continue # full match not found, iterate over whole database if all_images is None: all_images = repo_i.get_all() minimal_distance = 128 duplicate = None for image in all_images: # skip current image if image.message_id == message.id: continue # do the comparison db_image_hash = int(image.dhash, 16) distance = dhash.get_num_bits_different(db_image_hash, image_hash) if distance < minimal_distance: duplicate = image minimal_distance = distance if minimal_distance < self.limit_soft: duplicates[duplicate] = minimal_distance for image_hash, distance in duplicates.items(): await self.report_duplicate(message, image_hash, distance) async def report_duplicate(self, message: discord.Message, original: object, distance: int): """Send report. message: The new message containing attachment repost. original: The original attachment. distance: Hamming distance between the original and repost. """ if distance <= self.limit_full: level = "title_full" await message.add_reaction("♻️") elif distance <= self.limit_hard: level = "title_hard" await message.add_reaction("🤔") else: level = "title_soft" await message.add_reaction("🤷🏻") similarity = "{:.1f} %".format((1 - distance / 128) * 100) timestamp = utils.id_to_datetime(original.attachment_id).strftime("%Y-%m-%d %H:%M:%S") try: original_channel = message.guild.get_channel(original.channel_id) original_message = await original_channel.fetch_message(original.message_id) author = discord.utils.escape_markdown(original_message.author.display_name) link = f"[**{author}**, {timestamp}]({original_message.jump_url})" except discord.errors.NotFound: link = "404 " + emote.sad description = self.text.get( "report", "description", name=discord.utils.escape_markdown(message.author.display_name), similarity=similarity, ) embed = discord.Embed( title=self.text.get("report", level), color=config.color, description=description, ) embed.add_field( name=self.text.get("report", "original"), value=link, inline=False, ) embed.add_field( name=self.text.get("report", "help"), value=self.text.get( "report", "help_content", limit=self.config.get("not duplicate limit"), ), inline=False, ) embed.set_footer(text=f"{message.author.id} | {message.id}") report = await message.reply(embed=embed) await report.add_reaction("❎")
class Howto(rubbercog.Rubbercog): """See information about school related topics""" def __init__(self, bot): super().__init__(bot) self.text = CogText("howto") try: self.data = hjson.load(open("data/howto/howto.hjson")) except: print("Howto init(): Could not load HOWTO source file." ) # noqa: T001 self.data = OrderedDict() ## ## Commands ## @commands.check(check.is_verified) @commands.command() async def howto(self, ctx, *args): """See information about school related topics""" args = [x for x in args if len(x) > 0] name = "\u200b" content = self.data # get the requested item for directory in args: if directory not in content.keys(): await utils.delete(ctx) raise HowtoException() name = directory content = content.get(name) title = "{prefix}{command} {args}".format( prefix=config.prefix, command=ctx.command.qualified_name, args=self.sanitise(" ".join(args), limit=30), ) embed = self.embed(ctx=ctx, title=title) # fill the embed if isinstance(content, OrderedDict): embed = self._add_listing(embed, name, content) elif isinstance(content, list): embed = self._add_list_content(embed, name, content) else: embed = self._add_str_content(embed, name, str(content)) # done await ctx.send(embed=embed) await utils.delete(ctx) await utils.room_check(ctx) ## ## Helper functions ## def _add_listing(self, embed: discord.Embed, name: str, directory: OrderedDict): """List items in howto directory name: directory name directory: howto category (HJSON {}) """ value = "\n".join(["→ " + x for x in directory.keys()]) embed.add_field(name=name, value=value) return embed def _add_list_content(self, embed: discord.Embed, name: str, item: list): """Add item content to embed name: item name item: list """ for i, step in enumerate(item): embed.add_field(name=f"#{i+1}", value=step, inline=False) return embed def _add_str_content(self, embed: discord.Embed, name: str, item: str): """Add item content to embed name: item name item: string """ embed.add_field(name="\u200b", value=item) return embed ## ## Error catching ## @commands.Cog.listener() async def on_command_error(self, ctx: commands.Context, error): # try to get original error if hasattr(ctx.command, "on_error") or hasattr(ctx.command, "on_command_error"): return error = getattr(error, "original", error) # non-rubbergoddess exceptions are handled globally if not isinstance(error, rubbercog.RubbercogException): return # fmt: off elif isinstance(error, HowtoException): await self.output.warning( ctx, self.text.get("HowtoException", mention=ctx.author.mention))
class Comments(rubbercog.Rubbercog): """Manage user information""" def __init__(self, bot): super().__init__(bot) self.text = CogText("comments") @commands.guild_only() @commands.check(acl.check) @commands.group(name="comment") async def comments(self, ctx): """Manage comments on guild users""" await utils.send_help(ctx) @commands.check(acl.check) @comments.command(name="list") async def comments_list(self, ctx, user: Union[discord.Member, discord.User]): comments: List[Comment] = Comment.get(ctx.guild.id, user.id) if not len(comments): return await ctx.reply(self.text.get("list", "none")) def format_comment(comment: Comment) -> str: timestamp: str = comment.timestamp.strftime("%Y-%m-%d %H:%M") author: discord.Member = ctx.guild.get_member(comment.author_id) author_name: str = (f"{comment.author_id}" if author is None else discord.utils.escape_markdown( author.display_name)) text: str = "\n".join( [f"> {line}" for line in comment.text.split("\n")]) return f"**{author_name}**, {timestamp} (ID {comment.id}):\n{text}" response: str = "\n".join( [format_comment(comment) for comment in comments]) stubs: List[str] = utils.paginate(response) await ctx.reply(stubs[0]) if len(stubs) > 1: for stub in utils.paginate(response): await ctx.send(stub) @commands.check(acl.check) @comments.command(name="add") async def comments_add(self, ctx, user: Union[discord.Member, discord.User, int], *, text: str): Comment.add( guild_id=ctx.guild.id, author_id=ctx.author.id, user_id=user.id, text=text, ) await ctx.reply(self.text.get("add", "ok")) await self.event.sudo(ctx, f"Comment added on {user}.") @commands.check(acl.check) @comments.command(name="remove") async def comments_remove(self, ctx, id: int): success = Comment.remove(guild_id=ctx.guild.id, id=id) if not success: return await ctx.reply("remove", "none") await ctx.reply(self.text.get("remove", "ok")) await self.event.sudo(ctx, f"Comment with ID {id} removed.")
class ACL(rubbercog.Rubbercog): """Permission control""" def __init__(self, bot): super().__init__(bot) self.text = CogText("acl") ## ## Commands ## @commands.guild_only() @commands.check(acl.check) @commands.group(name="acl") async def acl_(self, ctx): """Permission control""" await utils.send_help(ctx) ## Groups @commands.check(acl.check) @acl_.group(name="group", aliases=["g"]) async def acl_group(self, ctx): """ACL group control""" await utils.send_help(ctx) @commands.check(acl.check) @acl_group.command(name="list", aliases=["l"]) async def acl_group_list(self, ctx): """List ACL groups""" groups = repo_a.get_groups(ctx.guild.id) if not len(groups): return await ctx.send(self.text.get("group_list", "nothing")) # compute relationships between groups relationships = {} for group in groups: if group.name not in relationships: relationships[group.name] = [] if group.parent is not None: # add to parent's list if group.parent not in relationships: relationships[group.parent] = [] relationships[group.parent].append(group) # add relationships to group objects for group in groups: group.children = relationships[group.name] group.level = 0 def bfs(queue) -> list: visited = [] while queue: group = queue.pop(0) if group not in visited: visited.append(group) # build levels for intendation for child in group.children: child.level = group.level + 1 queue = group.children + queue return visited result = "" template = "{group_id:<2} {name:<20} {role:<18}" for group in bfs(groups): result += "\n" + template.format( group_id=group.id, name=" " * group.level + group.name, role=group.role_id, ) await ctx.send("```" + result + "```") @commands.check(acl.check) @acl_group.command(name="get", aliases=["info"]) async def acl_group_get(self, ctx, name: str): """Get ACL group""" result = repo_a.get_group(ctx.guild.id, name) if result is None: return await ctx.send(self.text.get("group_get", "nothing")) await ctx.send(embed=self.get_group_embed(ctx, result)) @commands.check(acl.check) @acl_group.command(name="add", aliases=["a"]) async def acl_group_add(self, ctx, name: str, parent: str, role_id: int): """Add ACL group name: string matching `[a-zA-Z-]+` parent: parent group name role_id: Discord role ID To unlink the group from any parents, set parent to "". To set up virtual group with no link to discord roles, set role_id to 0. """ regex = r"[a-zA-Z-]+" if re.fullmatch(regex, name) is None: return await ctx.send(self.text.get("group_regex", regex=regex)) if len(parent) == 0: parent = None result = repo_a.add_group(ctx.guild.id, name, parent, role_id) await ctx.send(embed=self.get_group_embed(ctx, result)) await self.event.sudo(ctx, f"ACL group added: **{result.name}**.") @commands.check(acl.check) @acl_group.command(name="edit", aliases=["e"]) async def acl_group_edit(self, ctx, name: str, param: str, value): """Edit ACL group name: string matching `[a-zA-Z-]+` Options: name, string matching `[a-zA-Z-]+` parent, parent group name role_id, Discord role ID To unlink the group from any parents, set parent to "". To set up virtual group with no link to discord roles, set role_id to 0. """ if param == "name": regex = r"[a-zA-Z-]+" if re.fullmatch(regex, value) is None: return await ctx.send(self.text.get("group_regex", regex=regex)) result = repo_a.edit_group(ctx.guild.id, name=name, new_name=value) elif param == "parent": result = repo_a.edit_group(ctx.guild.id, name=name, parent=value) elif param == "role_id": result = repo_a.edit_group(ctx.guild.id, name=name, role_id=int(value)) else: raise discord.BadArgument() await ctx.send(embed=self.get_group_embed(ctx, result)) await self.event.sudo( ctx, f"ACL group **{result.name}** updated: **{param}={value}**.") @commands.check(acl.check) @acl_group.command(name="remove", aliases=["delete", "r", "d"]) async def acl_group_remove(self, ctx, name: str): """Remove ACL group""" result = repo_a.delete_group(ctx.guild.id, name) await ctx.send(embed=self.get_group_embed(ctx, result)) await self.event.sudo( ctx, f"ACL group removed: **{result.get('name')}** (#{result.get('id')})." ) ## Rules @commands.guild_only() @commands.check(acl.check) @acl_.group(name="rule") async def acl_rule(self, ctx): """Command control""" await utils.send_help(ctx) @commands.guild_only() @commands.check(acl.check) @acl_rule.command(name="get", aliases=["info"]) async def acl_rule_get(self, ctx, command: str): """See command's policy""" rule = repo_a.get_rule(ctx.guild.id, command) if rule is None: return await ctx.send(self.text.get("rule_get", "nothing")) embed = self.get_rule_embed(ctx, rule) await ctx.send(embed=embed) @commands.guild_only() @commands.check(acl.check) @acl_rule.command(name="import") async def acl_rule_import(self, ctx, mode: str): """Import command rules mode: `append` or `replace` """ if len(ctx.message.attachments) != 1: return await ctx.send(self.text.get("import", "wrong_file")) if not ctx.message.attachments[0].filename.endswith("json"): return await ctx.send(self.text.get("import", "wrong_json")) if mode not in ("append", "replace"): return await ctx.send(self.text.get("import", "wrong_mode")) # Download data_file = tempfile.TemporaryFile() await ctx.message.attachments[0].save(data_file) data_file.seek(0) try: data = json.load(data_file) except json.decoder.JSONDecodeError as exc: return await ctx.send( self.text.get("import", "wrong_json") + f"\n> `{str(exc)}`") new, edited, rejected = await self._import_json(ctx, data, mode=mode) await ctx.send( self.text.get("import", "imported", new=len(new), edited=len(edited))) result = "" for (msg, command, reason) in rejected: result += "\n> " + self.text.get( "import", msg, command=command, reason=reason) if len(result) > 1900: await ctx.send(result) result = "" if len(result): await ctx.send(result) data_file.close() await self.event.sudo( ctx, f"Added {len(new)} and edited {len(edited)} ACL rules.") @commands.guild_only() @commands.check(acl.check) @acl_rule.command(name="export") async def acl_rule_export(self, ctx): """Export command rules""" filename = f"acl_{ctx.guild.id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" rules = repo_a.get_rules(ctx.guild.id) export = dict() for rule in rules: export[rule.command] = { "default": rule.default, "group allow": [g.group.name for g in rule.groups if g.allow], "group deny": [g.group.name for g in rule.groups if not g.allow], "user allow": [u.user_id for u in rule.users if u.allow], "user deny": [u.user_id for u in rule.users if not u.allow], } file = tempfile.TemporaryFile(mode="w+") json.dump(export, file, indent="\t", sort_keys=True) file.seek(0) await ctx.send(file=discord.File(fp=file, filename=filename)) file.close() await self.event.sudo(ctx, "ACL rules exported.") @commands.check(acl.check) @acl_rule.command(name="flush", ignore_extra=False) async def acl_rule_flush(self, ctx): """Flush all the command rules.""" count = repo_a.delete_rules(ctx.guild.id) await ctx.send(self.text.get("rule_flush", count=count)) await self.event.sudo(ctx, "ACL rules flushed.") ## ## Logic ## def get_rule_embed(self, ctx, rule: ACL_rule) -> discord.Embed: embed = discord.Embed( ctx=ctx, title=self.text.get("rule_get", "title", command=rule.command), ) embed.add_field( name=self.text.get("rule_get", "default"), value=str(rule.default), inline=False, ) if len([g for g in rule.groups if g.allow is True]): embed.add_field( name=self.text.get("rule_get", "group_allow"), value=", ".join( [g.group.name for g in rule.groups if g.allow is True]), ) if len([g for g in rule.groups if g.allow is False]): embed.add_field( name=self.text.get("rule_get", "group_deny"), value=", ".join( [g.group.name for g in rule.groups if g.allow is False]), ) if len([u for u in rule.users if u.allow is True]): embed.add_field( name=self.text.get("rule_get", "user_allow"), value=", ".join( [(self.bot.get_user(u.user_id) or str(u.user_id)) for u in rule.users if u.allow is True], ), ) if len([u for u in rule.users if u.allow is False]): embed.add_field( name=self.text.get("rule_get", "user_deny"), value=", ".join([(self.bot.get_user(u.user_id) or str(u.user_id)) for u in rule.users if u.allow is False]), ) return embed def get_group_embed(self, ctx: commands.Context, group: Tuple[ACL_group, dict]) -> discord.Embed: if type(group) == ACL_group: group = group.mirror() role = ctx.guild.get_role(group["role_id"]) embed = discord.Embed( ctx=ctx, title=self.text.get("group_get", "title", name=group["name"]), ) if role is not None: embed.add_field( name=self.text.get("group_get", "discord"), value=f"{role.name} ({role.id})", inline=False, ) if group["parent"] is not None: embed.add_field( name=self.text.get("group_get", "parent"), value=group["parent"], inline=False, ) return embed async def _import_json( self, ctx: commands.Context, data: dict, mode: str) -> Tuple[List[str], List[str], List[Tuple[str, str]]]: """Import JSON rules Returns ------- list: New commands list: Altered commands list: Rejected commands as (skip/warn, command, reason) tuple """ result_new = list() result_alt = list() result_rej = list() # check data for command, attributes in data.items(): # CHECK bad: bool = False # bool if "default" in attributes: if attributes["default"] not in (True, False): result_rej.append(( "skip", command, self.text.get("import", "bad_bool", key="default"), )) bad = True else: attributes["default"] = False if "direct" in attributes: if attributes["direct"] not in (True, False): result_rej.append(( "skip", command, self.text.get("import", "bad_bool", key="direct"), )) bad = True else: attributes["direct"] = False # lists for keyword in ("group allow", "group deny", "user allow", "user deny"): if keyword in attributes: if type(attributes[keyword]) != list: result_rej.append(( "skip", command, self.text.get("import", "bad_list", key=keyword), )) bad = True else: attributes[keyword] = list() # groups for keyword in ("group allow", "group deny"): for group in attributes[keyword]: if type(group) != str: result_rej.append(( "skip", command, self.text.get("import", "bad_text", key=group), )) bad = True # users for keyword in ("user allow", "user deny"): for user in attributes[keyword]: if type(user) != int: result_rej.append(( "skip", command, self.text.get("import", "bad_int", key=user), )) bad = True if bad: # do not proceed to adding continue # ADD try: repo_a.add_rule(ctx.guild.id, command, attributes["default"]) result_new.append(command) except acl_repo.Duplicate: if mode == "replace": repo_a.delete_rule(ctx.guild.id, command) repo_a.add_rule(ctx.guild.id, command, attributes["default"]) result_alt.append(command) else: result_rej.append( ("skip", command, self.text.get("import", "duplicate"))) continue for group in attributes["group allow"]: try: repo_a.add_group_constraint(ctx.guild.id, command, group, allow=True) except acl_repo.NotFound: result_rej.append(( "warn", command, self.text.get( "import", "no_group", name=group, ), )) for group in attributes["group deny"]: try: repo_a.add_group_constraint(ctx.guild.id, command, group, allow=False) except acl_repo.NotFound: result_rej.append(( "warn", command, self.text.get( "import", "no_group", name=group, ), )) for user in attributes["user allow"]: repo_a.add_user_constraint(ctx.guild.id, command, user, allow=True) for user in attributes["user deny"]: repo_a.add_user_constraint(ctx.guild.id, command, user, allow=False) return result_new, result_alt, result_rej
class Librarian(rubbercog.Rubbercog): """Knowledge and information based commands""" # TODO Move czech strings to text.default.json def __init__(self, bot): super().__init__(bot) self.config = CogConfig("librarian") self.text = CogText("librarian") @commands.command(aliases=["svátek"]) async def svatek(self, ctx): url = f"http://svatky.adresa.info/json?date={date.today().strftime('%d%m')}" res = await self.fetch_json(url) names = [] for i in res: names.append(i["name"]) await ctx.send(self.text.get("nameday", "cs", name=", ".join(names))) @commands.command(aliases=["sviatok"]) async def meniny(self, ctx): url = f"http://svatky.adresa.info/json?lang=sk&date={date.today().strftime('%d%m')}" res = await self.fetch_json(url) names = [] for i in res: names.append(i["name"]) await ctx.send(self.text.get("nameday", "sk", name=", ".join(names))) @commands.command(aliases=["tyden", "týden", "tyzden", "týždeň"]) async def week(self, ctx: commands.Context): """See if the current week is odd or even""" cal_week = date.today().isocalendar()[1] stud_week = cal_week - self.config.get("starting_week") even, odd = self.text.get("week", "even"), self.text.get("week", "odd") cal_type = even if cal_week % 2 == 0 else odd stud_type = even if stud_week % 2 == 0 else odd embed = self.embed(ctx=ctx) embed.add_field( name=self.text.get("week", "study"), value="{} ({})".format(stud_type, stud_week), ) embed.add_field( name=self.text.get("week", "calendar"), value="{} ({})".format(cal_type, cal_week), ) await ctx.send(embed=embed) await utils.delete(ctx) await utils.room_check(ctx) @commands.command(aliases=["počasí", "pocasi", "počasie", "pocasie"]) async def weather(self, ctx, *, place: str = "Brno"): token = self.config.get("weather_token") place = place[:100] if "&" in place: return await ctx.send(self.text.get("weather", "place_not_found")) url = ("https://api.openweathermap.org/data/2.5/weather?q=" + place + "&units=metric&lang=cz&appid=" + token) res = await self.fetch_json(url) """ Example response { "coord":{ "lon":16.61, "lat":49.2 }, "weather":[ { "id":800, "temp_maixn":"Clear", "description":"jasno", "icon":"01d" } ], "base":"stations", "main":{ "temp":21.98, "feels_like":19.72, "temp_min":20.56, "temp_max":23, "pressure":1013, "humidity":53 }, "visibility":10000, "wind":{ "speed":4.1, "deg":50 }, "clouds":{ "all":0 }, "dt":1595529518, "sys":{ "type":1, "id":6851, "country":"CZ", "sunrise":1595474051, "sunset":1595529934 }, "timezone":7200, "id":3078610, "name":"Brno", "cod":200 } """ if str(res["cod"]) == "404": return await ctx.send(self.text.get("weather", "place_not_found")) elif str(res["cod"]) == "401": return await ctx.send(self.text.get("weather", "token")) elif str(res["cod"]) != "200": return await ctx.send( self.text.get("weather", "place_error", message=res["message"])) title = res["weather"][0]["description"] description = self.text.get("weather", "description", name=res["name"], country=res["sys"]["country"]) if description.endswith("CZ"): description = description[:-4] embed = self.embed(ctx=ctx, title=title[0].upper() + title[1:], description=description) embed.set_thumbnail(url="https://openweathermap.org/img/w/{}.png". format(res["weather"][0]["icon"])) embed.add_field( name=self.text.get("weather", "temperature"), value=self.text.get( "weather", "temperature_value", real=round(res["main"]["temp"], 1), feel=round(res["main"]["feels_like"], 1), ), inline=False, ) embed.add_field( name=self.text.get("weather", "humidity"), value=str(res["main"]["humidity"]) + " %", ) embed.add_field( name=self.text.get("weather", "clouds"), value=(str(res["clouds"]["all"]) + " %"), ) if "visibility" in res: embed.add_field( name=self.text.get("weather", "visibility"), value=f"{int(res['visibility']/1000)} km", ) embed.add_field(name=self.text.get("weather", "wind"), value=f"{res['wind']['speed']} m/s") await utils.send(ctx, embed=embed) await utils.room_check(ctx) @commands.command(aliases=["b64"]) async def base64(self, ctx, direction: str, *, data: str): """Get base64 data direction: [encode, e, -e; decode, d, -d] text: string (under 1000 characters) """ if data is None or not len(data): return await utils.send_help(ctx) data = data[:1000] if direction in ("encode", "e", "-e"): direction = "encode" result = base64.b64encode(data.encode("utf-8")).decode("utf-8") elif direction in ("decode", "d", "-d"): direction = "decode" try: result = base64.b64decode(data.encode("utf-8")).decode("utf-8") except Exception as e: return await ctx.send(f"> {e}") else: return await utils.send_help(ctx) quote = self.sanitise(data[:50]) + ("…" if len(data) > 50 else "") await ctx.send(f"**base64 {direction}** ({quote}):\n> ```{result}```") await utils.room_check(ctx) @commands.command() async def hashlist(self, ctx): """Get list of available hash functions""" result = "**hashlib**\n" result += "> " + " ".join(sorted(hashlib.algorithms_available)) await ctx.send(result) @commands.command() async def hash(self, ctx, fn: str, *, data: str): """Get hash function result Run hashlist command to see available algorithms """ if fn in hashlib.algorithms_available: result = hashlib.new(fn, data.encode("utf-8")).hexdigest() else: return await ctx.send(self.text.get("invalid_hash")) quote = self.sanitise(data[:50]) + ("…" if len(data) > 50 else "") await ctx.send(f"**{fn}** ({quote}):\n> ```{result}```") async def fetch_json(self, url: str) -> dict: """Fetch data from a URL and return a dict""" async with aiohttp.ClientSession() as cs: async with cs.get(url) as r: return await r.json()
class Points(rubbercog.Rubbercog): """Get points by having conversations""" def __init__(self, bot): super().__init__(bot) self.config = CogConfig("points") self.text = CogText("points") self.limits_message = self.config.get("points_message") self.timer_message = self.config.get("timer_message") self.limits_reaction = self.config.get("points_reaction") self.timer_reaction = self.config.get("timer_reaction") self.stats_message = {} self.stats_reaction = {} self.cleanup.start() ## ## Commands ## @commands.group(name="points", aliases=["body"]) async def points(self, ctx): """Points? Points!""" await utils.send_help(ctx) @points.command(name="get", aliases=["gde", "me", "stalk"]) async def points_get(self, ctx, member: discord.Member = None): """Get user points""" if member is None: member = ctx.author result = repo_p.get(member.id) position = repo_p.getPosition(result.points) if member.id == ctx.author.id: text = self.text.get("me", points=result.points, position=position) else: text = self.text.get( "stalk", display_name=self.sanitise(member.display_name), points=result.points, position=position, ) await ctx.send(text) await utils.room_check(ctx) @points.command(name="leaderboard", aliases=["🏆"]) async def points_leaderboard(self, ctx): """Points leaderboard""" embed = self.embed( ctx=ctx, title=self.text.get("embed", "title") + self.text.get("embed", "desc_suffix"), description=self.text.get("embed", "desc_description"), ) users = repo_p.getUsers("desc", limit=self.config.get("board"), offset=0) value = self._getBoard(ctx.author, users) embed.add_field( name=self.text.get("embed", "desc_0", num=self.config.get("board")), value=value, inline=False, ) # if the user is not present, add them to second field if ctx.author.id not in [u.user_id for u in users]: author = repo_p.get(ctx.author.id) embed.add_field( name=self.text.get("embed", "user"), value="`{points:>8}` … {name}".format( points=author.points, name="**" + self.sanitise(ctx.author.display_name) + "**"), inline=False, ) message = await ctx.send(embed=embed) await message.add_reaction("⏪") await message.add_reaction("◀") await message.add_reaction("▶") await utils.room_check(ctx) @points.command(name="loserboard", aliases=["💩"]) async def points_loserboard(self, ctx): """Points loserboard""" embed = self.embed( ctx=ctx, title=self.text.get("embed", "title") + self.text.get("embed", "asc_suffix"), description=self.text.get("embed", "asc_description"), ) users = repo_p.getUsers("asc", limit=self.config.get("board"), offset=0) value = self._getBoard(ctx.author, users) embed.add_field(name=self.text.get("embed", "asc_0", num=self.config.get("board")), value=value) # if the user is not present, add them to second field if ctx.author.id not in [u.user_id for u in users]: author = repo_p.get(ctx.author.id) embed.add_field( name=self.text.get("embed", "user"), value="`{points:>8}` … {name}".format( points=author.points, name="**" + self.sanitise(ctx.author.display_name) + "**"), inline=False, ) message = await ctx.send(embed=embed) await message.add_reaction("⏪") await message.add_reaction("◀") await message.add_reaction("▶") await utils.room_check(ctx) ## ## Listeners ## @commands.Cog.listener() async def on_message(self, message): """Add points on message""" if message.author.bot: return # Before the database is updated, only count primary guild if message.guild.id not in (config.guild_id, config.slave_id): return now = datetime.datetime.now() if (str(message.author.id) in self.stats_message and (now - self.stats_message[str(message.author.id)]).total_seconds() < self.timer_message): return value = random.randint(self.limits_message[0], self.limits_message[1]) self.stats_message[str(message.author.id)] = now repo_p.increment(message.author.id, value) @commands.Cog.listener() async def on_reaction_add(self, reaction, user): """Handle board scrolling""" if user.bot: return # add points now = datetime.datetime.now() if (str(user.id) not in self.stats_reaction or (now - self.stats_reaction[str(user.id)]).total_seconds() >= self.timer_reaction): value = random.randint(self.limits_reaction[0], self.limits_reaction[1]) self.stats_reaction[str(user.id)] = now repo_p.increment(user.id, value) if str(reaction) not in ("⏪", "◀", "▶"): return # fmt: off if len(reaction.message.embeds) != 1 \ or type(reaction.message.embeds[0].title) != str \ or not reaction.message.embeds[0].title.startswith(self.text.get("embed", "title")): return # fmt: on embed = reaction.message.embeds[0] # get ordering if embed.title.endswith(self.text.get("embed", "desc_suffix")): order = "desc" else: order = "asc" # get current offset if ", " in embed.fields[0].name: offset = int(embed.fields[0].name.split(" ")[-1]) - 1 else: offset = 0 # get new offset if str(reaction) == "⏪": offset = 0 elif str(reaction) == "◀": offset -= self.config.get("board") elif str(reaction) == "▶": offset += self.config.get("board") if offset < 0: return await utils.remove_reaction(reaction, user) users = repo_p.getUsers(order, limit=self.config.get("board"), offset=offset) value = self._getBoard(user, users) if not value: # offset too big return await utils.remove_reaction(reaction, user) if offset: name = self.text.get("embed", order + "_n", num=self.config.get("board"), offset=offset + 1) else: name = self.text.get("embed", order + "_0", num=self.config.get("board")) embed.clear_fields() embed.add_field(name=name, value=value, inline=False) # if the user is not present, add them to second field if user.id not in [u.user_id for u in users]: author = repo_p.get(user.id) embed.add_field( name=self.text.get("embed", "user"), value="`{points:>8}` … {name}".format( points=author.points, name="**" + self.sanitise(user.display_name) + "**"), inline=False, ) await reaction.message.edit(embed=embed) await utils.remove_reaction(reaction, user) ## ## Helper functions ## def _getBoard(self, author: Union[discord.User, discord.Member], users: list, offset: int = 0) -> str: result = [] template = "`{points:>8}` … {name}" for db_user in users: user = self.bot.get_user(db_user.user_id) if user and user.display_name: name = discord.utils.escape_markdown(user.display_name) else: name = self.text.get("unknown") if db_user.user_id == author.id: name = "**" + name + "**" result.append(template.format(points=db_user.points, name=name)) return "\n".join(result) ## ## Tasks ## @tasks.loop(seconds=120.0) async def cleanup(self): delete = [] for uid, time in self.stats_message.items(): if (datetime.datetime.now() - time).total_seconds() >= self.timer_message: delete.append(uid) for uid in delete: self.stats_message.pop(uid) delete = [] for uid, time in self.stats_reaction.items(): if (datetime.datetime.now() - time).total_seconds() >= self.timer_reaction: delete.append(uid) for uid in delete: self.stats_reaction.pop(uid)
class Random(rubbercog.Rubbercog): """Pick, flip, roll dice""" def __init__(self, bot): super().__init__(bot) self.text = CogText("random") @commands.cooldown(rate=3, per=20.0, type=commands.BucketType.user) @commands.check(check.is_verified) @commands.command() async def pick(self, ctx, *args): """"Pick an option""" for i, arg in enumerate(args): if arg.endswith("?"): args = args[i + 1:] break if not len(args): return option = self.sanitise(random.choice(args), limit=50) if option is not None: await ctx.send( self.text.get("answer", mention=ctx.author.mention, option=option)) await utils.room_check(ctx) @commands.cooldown(rate=3, per=20.0, type=commands.BucketType.user) @commands.check(check.is_verified) @commands.command() async def flip(self, ctx): """Yes/No""" option = random.choice(self.text.get("flip")) await ctx.send( self.text.get("answer", mention=ctx.author.mention, option=option)) await utils.room_check(ctx) @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user) @commands.check(check.is_verified) @commands.command() async def random(self, ctx, first: int, second: int = None): """Pick number from interval""" if second is None: second = 0 if first > second: first, second = second, first option = random.randint(first, second) await ctx.send( self.text.get("answer", mention=ctx.author.mention, option=option)) await utils.room_check(ctx) @commands.cooldown(rate=5, per=20, type=commands.BucketType.channel) @commands.check(check.is_verified) @commands.command(aliases=["unsplash"]) async def picsum(self, ctx, *, seed: str = None): """Get random image from picsum.photos""" size = "900/600" url = "https://picsum.photos/" if seed: url += "seed/" + seed + "/" url += f"{size}.jpg?random={ctx.message.id}" # we cannot use the URL directly, because embed will contain other image than its thumbnail image = requests.get(url) if image.status_code != 200: return await ctx.send(f"E{image.status_code}") # get image info # example url: https://i.picsum.photos/id/857/600/360.jpg?hmac=..... image_id = image.url.split("/id/", 1)[1].split("/")[0] image_info = requests.get(f"https://picsum.photos/id/{image_id}/info") try: image_url = image_info.json()["url"] except: image_url = discord.Embed.Empty embed = self.embed(ctx=ctx, title=discord.Embed.Empty, description=image_url, footer=seed) embed.set_image(url=image.url) await ctx.send(embed=embed) await utils.room_check(ctx)
class Animals(rubbercog.Rubbercog): """Private zone""" def __init__(self, bot): super().__init__(bot) self.config = CogConfig("animals") self.text = CogText("animals") self.channel = None self.role = None def getChannel(self): if self.channel is None: self.channel = self.bot.get_channel(self.config.get("channel")) return self.channel def getRole(self): if self.role is None: self.role = self.getChannel().guild.get_role( self.config.get("role")) return self.role ## ## Commands ## @commands.check(acl.check) @commands.command() async def animal(self, ctx, member: discord.Member): """Send vote embed""" await self.check(member, "manual") ## ## Listeners ## @commands.Cog.listener() async def on_user_update(self, before: discord.User, after: discord.User): # only act if user is verified member = self.getGuild().get_member(after.id) if member is None: return # only act if user is verified if self.getVerifyRole() not in member.roles: return # only act if user has changed their avatar if before.avatar_url == after.avatar_url: await self.event.user(f"{after} updated", "Not an animal (default avatar).") return await self.check(after, "on_user_update") @commands.Cog.listener() async def on_member_update(self, before: discord.Member, after: discord.Member): # only act if the user has been verified verify = self.getVerifyRole() if not (verify not in before.roles and verify in after.roles): return # only act if their avatar is not default if after.avatar_url == after.default_avatar_url: await self.event.user(f"{after} verified", "Not an animal (default avatar).") return # lookup user timestamp, only allow new verifications db_user = repo_u.get(after.id) if db_user is not None and db_user.status == "verified": db_user = repo_u.get(after.id) timestamp = datetime.strptime(db_user.changed, "%Y-%m-%d %H:%M:%S") now = datetime.now() if (now - timestamp).total_seconds() > 5: # this was probably temporary unverify, they have been checked before await self.event.user(f"{after} reverified", "Skipping (unverify).") return await self.check(after, "on_member_update") @commands.Cog.listener() async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): """Vote""" if payload.channel_id != self.getChannel().id: return if payload.member.bot: return message = await self.getChannel().fetch_message(payload.message_id) # fmt: off if not message or len(message.embeds) != 1 \ or message.embeds[0].title != self.text.get("title"): return # fmt: on if str(payload.emoji) not in ("☑️", "❎"): return await message.remove_reaction(payload.emoji, payload.member) animal_id = int(message.embeds[0].description.split(" | ")[1]) if animal_id == payload.member.id: return await message.remove_reaction(payload.emoji, payload.member) animal = self.getChannel().guild.get_member(animal_id) if animal is None: await self.console.error( "animals", f"Could not find member with ID {animal_id}. Vote aborted.") await self.event.user( "animals", f"Could not find user {animal_id}, vote aborted.") return await utils.delete(message) # delete if the user has changed their avatar since the embed creation if str(message.embeds[0].image.url) != str(animal.avatar_url): await self.console.info(animal, "Avatar has changed since. Vote aborted.") return await utils.delete(message) for r in message.reactions: if r.emoji == "☑️" and r.count > self.config.get("limit"): if self.getRole() in animal.roles: # member is an animal and has been before await self.getChannel().send( self.text.get( "result", "yes_yes", nickname=self.sanitise(animal.display_name), )) else: # member is an animal and has not been before try: await animal.add_roles(self.getRole()) await self.event.user(animal, "New animal!") await self.getChannel().send( self.text.get("result", "no_yes", mention=animal.mention)) except Exception as e: await self.console.error(message, "Could not add animal", e) break elif r.emoji == "❎" and r.count > self.config.get("limit"): if self.getRole() in animal.roles: # member is not an animal and has been before try: await animal.remove_roles(self.getRole()) await self.event.user(animal, "Animal left.") await self.getChannel().send( self.text.get("result", "yes_no", mention=animal.mention)) except Exception as e: await self.console.error(message, "Could not remove animal", e) else: # member is not an animal and has not been before await self.getChannel().send( self.text.get("result", "no_no", mention=animal.mention)) break else: return await utils.delete(message) ## ## Logic ## async def check(self, member: discord.Member, source: str): """Create vote embed""" embed = self.embed( title=self.text.get("title"), description=f"{self.sanitise(str(member))} | {member.id}", ) embed.add_field( name=self.text.get("source", source), value=self.text.get("required", limit=self.config.get("limit")), inline=False, ) embed.set_image(url=member.avatar_url) message = await self.getChannel().send(embed=embed) await message.add_reaction("☑️") await message.add_reaction("❎") try: await message.pin() except Exception as e: await self.event.user(member, "Could not pin Animal check embed.", e) await asyncio.sleep(0.5) messages = await message.channel.history(limit=5, after=message).flatten() for m in messages: if m.type == discord.MessageType.pins_add: await utils.delete(m) break
class Mover(rubbercog.Rubbercog): """Move database objects""" def __init__(self, bot): super().__init__(bot) self.text = CogText("mover") ## ## Commands ## @commands.check(acl.check) @commands.group(name="move") async def move(self, ctx): """Move stuff""" await utils.send_help(ctx) @commands.check(acl.check) @move.command(name="member") async def move_member(self, ctx, before: discord.Member, after: discord.Member): """Move old member data to new one Roles from the `before` member are moved to the `after` one. """ async with ctx.typing(): result = self.move_user_data(before.id, after.id) try: await self.move_member_roles(before, after) except Exception as e: await self.console.error(ctx, "Could not migrate member roles", e) embed = self.move_user_embed(ctx, after, result) await ctx.send(embed=embed) @commands.check(acl.check) @move.command(name="user") async def move_user(self, ctx, before: int, after: int): """Move old user data to new one This is useful when the member is not on the server anymore. """ async with ctx.typing(): result = self.move_user_data(before, after) embed = self.move_user_embed(ctx, after, result) await ctx.send(embed=embed) ## ## Logic ## def move_user_data(self, before_id: int, after_id: int) -> Dict[str, int]: """Move user data Arguments --------- before_id: `int` Old user ID after_id: `int` New user ID Returns ------- `dict`: mapping from table name to change counter """ result = {} result["interaction"] = repo_interaction.move_user(before_id, after_id) result["karma"] = repo_karma.move_user(before_id, after_id) result["points"] = repo_points.move_user(before_id, after_id) result["user"] = repo_user.move_user(before_id, after_id) return result async def move_member_roles(self, before: discord.Member, after: discord.Member): """Move roles from the before member to the after one.""" roles = before.roles[1:] await after.add_roles(*roles, reason="Member migration") await before.remove_roles(*roles, reason="Member migration") ## ## Helper functions ## def move_user_embed( self, ctx, after: Union[discord.Member, int], result: Dict[str, int], ) -> discord.Embed: """Create embed for move_member and move_user Arguments --------- after: `Member` or `int` representing new user result: `Dict` mapping table - number of affected rows Returns ------- `discord.Embed` """ embed = self.embed(ctx=ctx, title=self.text.get("user", "move"), description=str(after)) result_items = [] for key, value in result.items(): result_items.append(f"{key}: `{value}`") embed.add_field( name=self.text.get("user", "result"), value="\n".join(result_items), ) return embed
class Voice(rubbercog.Rubbercog): """Manage voice channels""" def __init__(self, bot): super().__init__(bot) self.config = CogConfig("voice") self.text = CogText("voice") self.locked = [] ## ## Commands ## @commands.guild_only() @commands.check(check.is_in_voice) @commands.bot_has_permissions(manage_channels=True, manage_messages=True) @commands.group(name="voice") async def voice(self, ctx): """Manage voice channels""" await utils.send_help(ctx) @voice.command(name="lock", aliases=["close"]) async def voice_lock(self, ctx): """Make current voice channel invisible""" voice = self.users_voice_channel(ctx) # Lock await voice.set_permissions(self.getVerifyRole(), overwrite=None) if ctx.channel.id not in self.locked: self.locked.append(ctx.channel.id) # Report await ctx.send(self.text.get("locked", name=self.sanitise(voice.name)), delete_after=10) await utils.delete(ctx.message) await self.event.user(ctx, f"Channel locked: **{voice.name}**.") @voice.command(name="unlock", aliases=["open"]) async def voice_unlock(self, ctx): """Make current voice channel visible""" voice = self.users_voice_channel(ctx) # Unlock await voice.set_permissions(self.getVerifyRole(), view_channel=True) if ctx.channel.id in self.locked: self.locked.remove(ctx.channel.id) # Report await ctx.send(self.text.get("unlocked", name=self.sanitise(voice.name)), delete_after=10) await utils.delete(ctx.message) await self.event.user(ctx, f"Channel unlocked: **{voice.name}**.") ## ## Listeners ## @commands.Cog.listener() async def on_voice_state_update(self, member, beforeState, afterState): """Detect changes in voice channels""" # get voice objects before = beforeState.channel after = afterState.channel # Do not act if no one has left or joined if before == after or (before is None and after is None): return # Do not act if the action is not on main guild if before not in self.getGuild( ).channels and after not in self.getGuild().channels: return # Get voice-no-mic channel nomic = self.getGuild().get_channel(config.channel_nomic) # Manage channel overwrites if before is None: # User joined await nomic.set_permissions(member, view_channel=True) try: await after.set_permissions(member, view_channel=True) except Exception as e: await self.console.warning( member, f"Could not add overwrite to {after}.", e) # Send welcome message await nomic.send( self.text.get("welcome", nickname=self.sanitise(member.display_name), prefix=config.prefix), delete_after=30, ) elif after is None: # User left await nomic.set_permissions(member, overwrite=None) try: await before.set_permissions(member, overwrite=None) except Exception as e: await self.console.warning( member, f"Could not remove overwrite from {before}.", e) else: # User moved try: await before.set_permissions(member, overwrite=None) except Exception as e: await self.console.warning( member, f"Could not remove overwrite from {before}.", e) try: await after.set_permissions(member, view_channel=True) except Exception as e: await self.console.warning( member, f"Could not add overwrite to {before}.", e) # Do cleanup await self.cleanup_channels() ## ## Logic ## async def cleanup_channels(self): """Remove empty voice channels""" category = self.getGuild().get_channel(config.channel_voices) nomic = self.getGuild().get_channel(config.channel_nomic) empty = [] for voice in category.voice_channels: if len(voice.members) == 0: empty.append(voice) if len(empty) == 0: # Need to add one voice = await self.getGuild().create_voice_channel( name=self.gen_channel_name(), category=category) await voice.set_permissions(self.getVerifyRole(), view_channel=True) return # Delete all except the last one for voice in empty[:-1]: try: await voice.delete() except discord.NotFound as e: await self.console.warning("Voice cleanup", f"Channel {voice} not found.", e) # Make sure the empty is writable await empty[-1].set_permissions(self.getVerifyRole(), view_channel=True) if len(category.voice_channels) == 1: # No one inside, can safely wipe no-mic channel await nomic.purge() # Just to make sure self.locked = [] ## ## Helper functions ## def users_voice_channel(self, ctx: commands.Context): return ctx.author.voice.channel def gen_channel_name(self) -> str: adjectives = self.config.get("adjectives") nouns = self.config.get("nouns") return "{} {}".format(random.choice(adjectives), random.choice(nouns))
class Verify(rubbercog.Rubbercog): """Verify your account""" def __init__(self, bot): super().__init__(bot) self.text = CogText("verify") self.config = CogConfig("verify") ## ## Commands ## @commands.check(acl.check) @commands.cooldown(rate=5, per=120, type=commands.BucketType.user) @commands.command() async def verify(self, ctx, email: str): """Ask for verification code""" await utils.delete(ctx) if email.count("@") != 1: raise NotAnEmail() if self.config.get("placeholder") in email: raise PlaceholderEmail() # check the database for member ID if repo_u.get(ctx.author.id) is not None: raise IDAlreadyInDatabase() # check the database for email if repo_u.getByLogin(email) is not None: raise EmailAlreadyInDatabase() # check e-mail format role = await self._email_to_role(ctx, email) # generate code code = await self._add_user(ctx.author, login=email, role=role) # send mail try: await self._send_verification_email(ctx.author, email, code) except smtplib.SMTPException as e: await self.console.warning(ctx, type(e).__name__) await self._send_verification_email(ctx.author, email, code) anonymised = "[redacted]@" + email.split("@")[1] await ctx.send( self.text.get( "verify successful", mention=ctx.author.mention, email=anonymised, prefix=config.prefix, ), delete_after=config.get("delay", "verify"), ) @commands.check(acl.check) @commands.cooldown(rate=3, per=120, type=commands.BucketType.user) @commands.command() async def submit(self, ctx, code: str): """Submit verification code""" await utils.delete(ctx) db_user = repo_u.get(ctx.author.id) if db_user is None or db_user.status in ( "unknown", "unverified") or db_user.code is None: raise SubmitWithoutCode() if db_user.status != "pending": raise ProblematicVerification(status=db_user.status, login=db_user.login) # repair the code code = code.replace("I", "1").replace("O", "0").upper() if code != db_user.code: raise WrongVerificationCode(ctx.author, code, db_user.code) # user is verified now repo_u.save_verified(ctx.author.id) # add role await self._add_verify_roles(ctx.author, db_user) # send messages for role_id in config.get("roles", "native"): if role_id in [x.id for x in ctx.author.roles]: await ctx.author.send(self.text.get("verification DM native")) break else: await ctx.author.send(self.text.get("verification DM guest")) # fmt: off # announce the verification await ctx.channel.send(self.text.get( "verification public", mention=ctx.author.mention, role=db_user.group, ), delete_after=config.get("delay", "verify")) # fmt: on await self.event.user( ctx, f"User {ctx.author.id} verified with group **{db_user.group}**.", ) ## ## Listeners ## @commands.Cog.listener() async def on_member_join(self, member: discord.Member): """Add them their roles back, if they have been verified before""" if member.guild.id != config.guild_id: return db_user = repo_u.get(member.id) if db_user is None or db_user.status != "verified": return # user has been verified, give them their main roles back await self._add_verify_roles(member, db_user) await self.event.user(member, f"Verification skipped (**{db_user.group}**)") ## ## Helper functions ## async def _email_to_role(self, ctx, email: str) -> discord.Role: """Get role from email address""" registered = self.config.get("suffixes") constraints = self.config.get("constraints") username = email.split("@")[0] for domain, role_id in list(registered.items())[:-1]: if not email.endswith(domain): continue # found corresponding domain, check constraint if domain in constraints: constraint = constraints[domain] else: constraint = list(constraints.values())[-1] match = re.fullmatch(constraint, username) # return if match is not None: return self.getGuild().get_role(role_id) else: await self.event.user( ctx, f"Rejecting e-mail: {self.sanitise(email)}") raise BadEmail(constraint=constraint) # domain not found, fallback to basic guest role role_id = registered.get(".") constraint = list(constraints.values())[-1] match = re.fullmatch(constraint, username) # return if match is not None: return self.getGuild().get_role(role_id) else: await self.event.user(ctx, f"Rejecting e-mail: {self.sanitise(email)}") raise BadEmail(constraint=constraint) async def _add_user(self, member: discord.Member, login: str, role: discord.Role) -> str: code_source = string.ascii_uppercase.replace("O", "").replace( "I", "") + string.digits code = "".join(random.choices(code_source, k=8)) repo_u.add(discord_id=member.id, login=login, group=role.name, code=code) await self.event.user( member, f"Adding {member.id} to database (**{role.name}**, code `{code}`)." ) return code async def _update_user(self, member: discord.Member) -> str: code_source = string.ascii_uppercase.replace("O", "").replace( "I", "") + string.digits code = "".join(random.choices(code_source, k=8)) repo_u.update(discord_id=member.id, code=code, status="pending") await self.event.user(member, f"{member.id} updated with code `{code}`") return code async def _send_verification_email(self, member: discord.Member, email: str, code: str) -> bool: cleartext = self.text.get("plaintext mail").format( guild_name=self.getGuild().name, code=code, bot_name=self.bot.user.name, git_hash=utils.git_get_hash()[:7], prefix=config.prefix, ) richtext = self.text.get( "html mail", # styling color_bg="#54355F", color_fg="white", font_family="Arial,Verdana,sans-serif", # names guild_name=self.getGuild().name, bot_name=self.bot.user.name, user_name=member.name, # codes code=code, git_hash=utils.git_get_hash()[:7], prefix=config.prefix, # images bot_avatar=self.bot.user.avatar_url_as(static_format="png", size=128), bot_avatar_size="120px", user_avatar=member.avatar_url_as(static_format="png", size=32), user_avatar_size="20px", ) msg = MIMEMultipart("alternative") msg["Subject"] = self.text.get("mail subject", guild_name=self.getGuild().name, user_name=member.name) msg["From"] = self.config.get("email", "address") msg["To"] = email msg["Bcc"] = self.config.get("email", "address") msg.attach(MIMEText(cleartext, "plain")) msg.attach(MIMEText(richtext, "html")) with smtplib.SMTP(self.config.get("email", "server"), self.config.get("email", "port")) as server: server.starttls() server.ehlo() server.login(self.config.get("email", "address"), self.config.get("email", "password")) server.send_message(msg) async def _add_verify_roles(self, member: discord.Member, db_user: object): """Return True if reverified""" verify = self.getVerifyRole() group = discord.utils.get(self.getGuild().roles, name=db_user.group) await member.add_roles(verify, group, reason="Verification") ## ## Error catching ## @commands.Cog.listener() async def on_command_error(self, ctx: commands.Context, error): # try to get original error if hasattr(ctx.command, "on_error") or hasattr(ctx.command, "on_command_error"): return error = getattr(error, "original", error) # non-rubbergoddess exceptions are handled globally if not isinstance(error, rubbercog.RubbercogException): return # fmt: off # exceptions with parameters if isinstance(error, ProblematicVerification): await self.output.warning( ctx, self.text.get("ProblematicVerification", status=error.status)) await self.event.user( ctx, f"Problem with verification: {error.login}: {error.status}") elif isinstance(error, BadEmail): await self.output.warning( ctx, self.text.get("BadEmail", constraint=error.constraint)) elif isinstance(error, WrongVerificationCode): await self.output.warning( ctx, self.text.get("WrongVerificationCode", mention=ctx.author.mention)) await self.event.user( ctx, f"User ({error.login}) code mismatch: `{error.their}` != `{error.database}`" ) # exceptions without parameters elif isinstance(error, VerificationException): await self.output.error(ctx, self.text.get(type(error).__name__))
class Meme(rubbercog.Rubbercog): """Interact with users""" def __init__(self, bot): super().__init__(bot) self.text = CogText("meme") self.config = CogConfig("meme") self.fishing_pool = self.config.get("_fishing") @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user) @commands.command() async def hug(self, ctx, user: discord.Member = None): """Hug someone! user: Discord user. If none, the bot will hug yourself. """ if user is None: user = ctx.author elif user == self.bot.user: await ctx.send(emote.hug_left) return await ctx.send(emote.hug_right + f" **{self.sanitise(user.display_name)}**") @hug.error async def hugError(self, ctx, error): if isinstance(error, commands.BadArgument): await ctx.send(self.text.get("cannot_hug")) @commands.cooldown(rate=5, per=120, type=commands.BucketType.user) @commands.command(aliases=["owo"]) async def uwu(self, ctx, *, message: str = None): """UWUize message""" if message is None: text = "OwO!" else: text = self.sanitise(self.uwuize(message), limit=1900, markdown=True) await ctx.send(f"**{ctx.author.display_name}**\n>>> " + text) @commands.cooldown(rate=5, per=120, type=commands.BucketType.user) @commands.command(aliases=["rcase", "randomise"]) async def randomcase(self, ctx, *, message: str = None): """raNdOMisE cAsInG""" if message is None: text = "O.o" else: text = "" for letter in message: if letter.isalpha(): text += letter.upper() if random.choice( (True, False)) else letter.lower() else: text += letter await ctx.send(f"**{ctx.author.display_name}**\n>>> " + text[:1900]) @commands.cooldown(rate=3, per=10, type=commands.BucketType.user) @commands.command() async def fish(self, ctx): """Go fishing!""" roll = random.uniform(0, 1) options = None for probabilty, harvest in self.fishing_pool.items(): if roll >= float(probabilty): options = harvest break else: return await ctx.send( self.text.get("fishing_fail", mention=ctx.author.mention)) await ctx.send(random.choice(options)) ## ## Logic ## @staticmethod def uwuize(string: str) -> str: # Adapted from https://github.com/PhasecoreX/PCXCogs/blob/master/uwu/uwu.py result = [] def uwuize_word(string: str) -> str: try: if string.lower()[0] == "m" and len(string) > 2: w = "W" if string[1].isupper() else "w" string = string[0] + w + string[1:] except: # this is how we handle emojis pass string = string.replace("r", "w").replace("R", "W") string = string.replace("ř", "w").replace("Ř", "W") string = string.replace("l", "w").replace("L", "W") string = string.replace("?", "?" * random.randint(1, 3)) string = string.replace("'", ";" * random.randint(1, 3)) if string[-1] == ",": string = string[:-1] + "." * random.randint(2, 3) return string result = " ".join([uwuize_word(s) for s in string.split(" ")]) if result[-1] == "?": result += " UwU" if result[-1] == "!": result += " OwO" if result[-1] == ".": result = result[:-1] + "," * random.randint(2, 4) return result
class Errors(rubbercog.Rubbercog): def __init__(self, bot): super().__init__(bot) self.text = CogText("errors") @commands.Cog.listener() async def on_command_error(self, ctx, error): # noqa: C901 """Handle errors""" if hasattr(ctx.command, "on_error") or hasattr(ctx.command, "on_command_error"): return error = getattr(error, "original", error) throw_error = await self._send_exception_message(ctx, error) if not throw_error: return # display error message await self.output.error(ctx, "", error) output = self._format_output(ctx, error) print(output) # send traceback to dedicated channel channel_stdout = self.bot.get_channel(config.get("channels", "stdout")) output = list(output[0 + i:1960 + i] for i in range(0, len(output), 1960)) sent = [] for message in output: m = await channel_stdout.send("```\n{}```".format(message)) sent.append(m) # send notification to botdev await self._send_notification(ctx, output, error, sent[0].jump_url) ## ## Logic ## async def _send_exception_message(self, ctx: commands.Context, error: Exception) -> bool: """Return True if error should be thrown""" # fmt: off if isinstance(error, acl_repo.ACLException): await self.output.error( ctx, self.text.get("acl", type(error).__name__, error=str(error))) return False # cog exceptions are handled in their cogs if isinstance(error, rubbercog.RubbercogException): if type(error) is not rubbercog.RubbercogException: return False await self.output.error(ctx, self.text.get("RubbercogException"), error) return False if type(error) == commands.CommandNotFound: return # Exceptions with parameters if type(error) == commands.MissingRequiredArgument: await self.output.warning( ctx, self.text.get("MissingRequiredArgument", param=error.param.name)) return False if type(error) == commands.CommandOnCooldown: time = utils.seconds2str(error.retry_after) await self.output.warning( ctx, self.text.get("CommandOnCooldown", time=time)) return False if type(error) == commands.MaxConcurrencyReached: await self.output.warning( ctx, self.text.get("MaxConcurrencyReached", num=error.number, per=error.per.name)) return False if type(error) == commands.MissingRole: # TODO Is !r OK, or should we use error.missing_role.name? role = f"`{error.missing_role!r}`" await self.output.warning(ctx, self.text.get("MissingRole", role=role)) return False if type(error) == commands.BotMissingRole: role = f"`{error.missing_role!r}`" await self.output.error(ctx, self.text.get("BotMissingRole", role=role)) return False if type(error) == commands.MissingAnyRole: roles = ", ".join(f"`{r!r}`" for r in error.missing_roles) await self.output.warning( ctx, self.text.get("MissingAnyRole", roles=roles)) return False if type(error) == commands.BotMissingAnyRole: roles = ", ".join(f"`{r!r}`" for r in error.missing_roles) await self.output.error( ctx, self.text.get("BotMissingAnyRole", roles=roles)) return False if type(error) == commands.MissingPermissions: perms = ", ".join(f"`{p}`" for p in error.missing_perms) await self.output.warning( ctx, self.text.get("MissingPermissions", perms=perms)) return False if type(error) == commands.BotMissingPermissions: perms = ", ".join(f"`{p}`" for p in error.missing_perms) await self.output.error( ctx, self.text.get("BotMissingPermissions", perms=perms)) return False if type(error) == commands.BadUnionArgument: await self.output.warning( ctx, self.text.get("BadUnionArgument", param=error.param.name)) return False if type(error) == commands.BadBoolArgument: await self.output.warning( ctx, self.text.get("BadBoolArgument", arg=error.argument)) return False # All cog-related errors if isinstance(error, smtplib.SMTPException): await self.console.error(ctx, "Could not send e-mail", error) await ctx.send( self.text.get("SMTPException", name=type(error).__name__)) return False if type(error) == commands.ExtensionFailed: await self.output.error( ctx, self.text.get( type(error).__name__, extension=f"{error.name!r}", error_name=error.original.__class__.__name__, error=str(error.original), )) return False if isinstance(error, commands.ExtensionError): await self.output.critical( ctx, self.text.get(type(error).__name__, extension=f"{error.name!r}")) return False # The rest of client exceptions if isinstance(error, commands.CommandError) or isinstance( error, discord.ClientException): await self.output.warning(ctx, self.text.get(type(error).__name__)) return False # DiscordException, non-critical errors if type(error) in ( discord.errors.NoMoreItems, discord.errors.HTTPException, discord.errors.Forbidden, discord.errors.NotFound, ): await self.output.error(ctx, self.text.get(type(error).__name__)) await self.console.error(ctx, type(error).__name__, error) return False # DiscordException, critical errors if type(error) in (discord.errors.DiscordException, discord.errors.GatewayNotFound): await self.output.error(ctx, self.text.get(type(error).__name__)) # Database if isinstance(error, sqlalchemy.exc.SQLAlchemyError): error_name = ".".join( [type(error).__module__, type(error).__name__]) await self.output.critical(ctx, error_name) await self.console.critical(ctx, "Database error", error) await self.event.user( ctx, f"Database reported`{error_name}`. The session may be invalidated <@{config.admin_id}>", escape_markdown=False, ) return False # fmt: on return True def _format_output(self, ctx: commands.Context, error) -> str: if isinstance(ctx.channel, discord.TextChannel): location = f"{ctx.guild.name}/{ctx.channel.name} ({ctx.channel.id})" else: location = type(ctx.channel).__name__ output = "{command} by {user} in {location}\n".format( command=config.prefix + ctx.command.qualified_name, user=str(ctx.author), location=location, ) output += "".join( traceback.format_exception(type(error), error, error.__traceback__)) return output async def _send_notification(self, ctx: commands.Context, output: str, error: Exception, traceback_url: str): channel = self.bot.get_channel(config.channel_botdev) embed = self.embed(ctx=ctx, color=discord.Color.from_rgb(255, 0, 0)) # fmt: off footer = "{user} in {channel}".format( user=str(ctx.author), channel=ctx.channel.name if isinstance(ctx.channel, discord.TextChannel) else type( ctx.channel).__name__) embed.set_footer(text=footer, icon_url=ctx.author.avatar_url) stack = output[-1] if len(stack) > 255: stack = "…" + stack[-255:] embed.add_field( name=type(error).__name__, value=f"```{stack}```", inline=False, ) embed.add_field(name="Traceback", value=traceback_url, inline=False) await channel.send(embed=embed)
class Backup(commands.Cog): def __init__(self, bot): self.bot = bot self.text = CogText("backup") @commands.check(acl.check) @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) @commands.group(name="backup") async def backup(self, ctx): """Backup various kinds of data""" await utils.send_help(ctx) @commands.check(acl.check) @backup.command(name="channel") async def backup_channel(self, ctx, channel: discord.TextChannel, limit: int = None): """Backup contents of text channel. channel: Text channel to be backed up. limit: Optional. Number of messages to back up. This can only be run once at a time. """ now = datetime.datetime.now() database.Information.add( id=channel.guild.id, channel_id=channel.id, author_id=ctx.author.id, ) users = set() # download messages total = str(limit) if limit is not None else "???" status = await ctx.send( self.text.get("channel", "messages", count=0, total=total)) i = 0 async for message in channel.history(limit=limit, oldest_first=False): try: attachments = (",".join([a.url for a in message.attachments]) if len(message.attachments) else None) embeds = (json.dumps([e.to_dict() for e in message.embeds]) if len(message.embeds) else None) database.Message.add( id=message.id, author_id=message.author.id, text=message.content, attachments=attachments, embeds=embeds, ) users.add(message.author) i += 1 except Exception as exc: await ctx.send(exc) if i > 0 and i % 500 == 0: await status.edit(content=self.text.get( "channel", "messages", count=i, total=total)) # download user information await status.edit(content=status.content + " " + self.text.get("channel", "users")) for user in users: try: database.User.add( id=user.id, name=user.name, avatar=await user.avatar_url_as(format="png", size=256).read(), ) except Exception as exc: await ctx.send(f"`{exc}`") # done await status.edit(content=status.content + " " + self.text.get("channel", "done")) # get file size in bytes dbsize = os.path.getsize("data/backup/default.db") if ctx.guild is not None: limit = ctx.guild.filesize_limit else: # exactly 8MB is refused, this is ok limit = 8 * 1024**2 - 1024 # copy fname = now.strftime(f"backup_%Y%m%d-%H%M%S_channel-{channel.name}.db") fpath = "data/backup/" + fname shutil.copy("data/backup/default.db", fpath) text = self.text.get("saved_as", file=fpath) if dbsize > limit: await ctx.send(text + " " + self.text.get("too_big", size=int(dbsize / 1000))) else: await ctx.send( text, file=discord.File(fp=fpath, filename=fname), ) database.wipe() delta = datetime.datetime.now() - now await ctx.send( self.text.get("time", seconds=int(delta.total_seconds()))) @commands.guild_only() @commands.check(acl.check) @backup.command(name="emojis") async def backup_emojis(self, ctx): """Backup guild emojis. This can only be run once at a time. """ now = datetime.datetime.now() database.Information.add( id=ctx.guild.id, channel_id=ctx.channel.id, author_id=ctx.author.id, ) database.User.add( id=ctx.author.id, name=ctx.author.name, avatar=await ctx.author.avatar_url_as(format="png", size=256).read(), ) status = await ctx.send( self.text.get("emojis", "downloading", count=len(ctx.guild.emojis))) for emoji in ctx.guild.emojis: try: database.Emoji.add( id=emoji.id, name=emoji.name, data=await emoji.url.read(), ) except Exception as exc: await ctx.send(f"`{exc}`") await status.edit(content=status.content + " " + self.text.get("emojis", "done")) # get file size in bytes dbsize = os.path.getsize("data/backup/default.db") # copy fname = now.strftime( f"backup_%Y%m%d-%H%M%S_emojis-{ctx.guild.name}.db") fpath = "data/backup/" + fname shutil.copy("data/backup/default.db", fpath) text = self.text.get("saved_as", file=fpath) if dbsize > ctx.guild.filesize_limit: await ctx.send(text + " " + self.text.get("too_big", size=int(dbsize / 1000))) else: await ctx.send( text, file=discord.File(fp=fpath, filename=fname), ) database.wipe() delta = datetime.datetime.now() - now await ctx.send( self.text.get("time", seconds=int(delta.total_seconds())))