|
|
|
@ -0,0 +1,456 @@ |
|
|
|
|
import config |
|
|
|
|
import discord |
|
|
|
|
from discord.ext import commands |
|
|
|
|
from discord.ext.commands import Cog |
|
|
|
|
from helpers.checks import check_if_staff, check_if_target_is_staff |
|
|
|
|
import io |
|
|
|
|
import os |
|
|
|
|
import urllib.parse |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Lists(Cog): |
|
|
|
|
""" |
|
|
|
|
Manages channels that are dedicated to lists. |
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
def __init__(self, bot): |
|
|
|
|
self.bot = bot |
|
|
|
|
|
|
|
|
|
# Helpers |
|
|
|
|
|
|
|
|
|
def is_edit(self, emoji): |
|
|
|
|
return str(emoji)[0] == "โ" or str(emoji)[0] == "๐" |
|
|
|
|
|
|
|
|
|
def is_delete(self, emoji): |
|
|
|
|
return str(emoji)[0] == "โ" or str(emoji)[0] == "โ" |
|
|
|
|
|
|
|
|
|
def is_recycle(self, emoji): |
|
|
|
|
return str(emoji)[0] == "โป" |
|
|
|
|
|
|
|
|
|
def is_insert_above(self, emoji): |
|
|
|
|
return str(emoji)[0] == "โคด๏ธ" or str(emoji)[0] == "โฌ" |
|
|
|
|
|
|
|
|
|
def is_insert_below(self, emoji): |
|
|
|
|
return str(emoji)[0] == "โคต๏ธ" or str(emoji)[0] == "โฌ" |
|
|
|
|
|
|
|
|
|
def is_reaction_valid(self, reaction): |
|
|
|
|
allowed_reactions = [ |
|
|
|
|
"โ", |
|
|
|
|
"๐", |
|
|
|
|
"โ", |
|
|
|
|
"โ", |
|
|
|
|
"โป", |
|
|
|
|
"โคด๏ธ", |
|
|
|
|
"โฌ", |
|
|
|
|
"โฌ", |
|
|
|
|
"โคต๏ธ", |
|
|
|
|
] |
|
|
|
|
return str(reaction.emoji)[0] in allowed_reactions |
|
|
|
|
|
|
|
|
|
async def find_reactions(self, user_id, channel_id, limit=None): |
|
|
|
|
reactions = [] |
|
|
|
|
channel = self.bot.get_channel(channel_id) |
|
|
|
|
async for message in channel.history(limit=limit): |
|
|
|
|
if len(message.reactions) == 0: |
|
|
|
|
continue |
|
|
|
|
|
|
|
|
|
for reaction in message.reactions: |
|
|
|
|
users = await reaction.users().flatten() |
|
|
|
|
user_ids = map(lambda user: user.id, users) |
|
|
|
|
if user_id in user_ids: |
|
|
|
|
reactions.append(reaction) |
|
|
|
|
|
|
|
|
|
return reactions |
|
|
|
|
|
|
|
|
|
def create_log_message(self, emoji, action, user, channel, reason=""): |
|
|
|
|
msg = ( |
|
|
|
|
f"{emoji} **{action}** \n" |
|
|
|
|
f"from {self.bot.escape_message(user.name)} ({user.id}), in {channel.mention}" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
if reason != "": |
|
|
|
|
msg += f":\n`{reason}`" |
|
|
|
|
|
|
|
|
|
return msg |
|
|
|
|
|
|
|
|
|
async def clean_up_raw_text_file_message(self, guild_id, message): |
|
|
|
|
embeds = message.embeds |
|
|
|
|
if len(embeds) == 0: |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
fields = embeds[0].fields |
|
|
|
|
for field in fields: |
|
|
|
|
if field.name == "Message ID": |
|
|
|
|
files_channel = self.bot.get_channel( |
|
|
|
|
config.list_files_channel[guild_id] |
|
|
|
|
) |
|
|
|
|
file_message = await files_channel.fetch_message(int(field.value)) |
|
|
|
|
await file_message.delete() |
|
|
|
|
|
|
|
|
|
await message.edit(embed=None) |
|
|
|
|
|
|
|
|
|
async def link_list_item(self, ctx, channel: discord.TextChannel, number: int): |
|
|
|
|
if number <= 0: |
|
|
|
|
await ctx.send(f"Number must be greater than 0.") |
|
|
|
|
return False |
|
|
|
|
|
|
|
|
|
if channel.id not in config.list_channels[ctx.guild.id]: |
|
|
|
|
await ctx.send(f"{channel.mention} is not a list channel.") |
|
|
|
|
return False |
|
|
|
|
|
|
|
|
|
counter = 0 |
|
|
|
|
async for message in channel.history(limit=None, oldest_first=True): |
|
|
|
|
if message.content.strip(): |
|
|
|
|
counter += 1 |
|
|
|
|
|
|
|
|
|
if counter == number: |
|
|
|
|
embed = discord.Embed( |
|
|
|
|
title=f"Item #{number} in #{channel.name}", |
|
|
|
|
description=message.content.replace("โ", "").strip(), |
|
|
|
|
) |
|
|
|
|
embed.add_field(name="Jump URL", value=f"[Jump!]({message.jump_url})") |
|
|
|
|
|
|
|
|
|
await ctx.send(content="", embed=embed) |
|
|
|
|
return True |
|
|
|
|
|
|
|
|
|
await ctx.send(f"Unable to find item #{number} in {channel.mention}.") |
|
|
|
|
return False |
|
|
|
|
|
|
|
|
|
async def cache_message(self, message): |
|
|
|
|
msg = { |
|
|
|
|
"has_attachment": False, |
|
|
|
|
"attachment_filename": "", |
|
|
|
|
"attachment_data": b"", |
|
|
|
|
"content": message.content, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if len(message.attachments) != 0: |
|
|
|
|
attachment = next( |
|
|
|
|
( |
|
|
|
|
a |
|
|
|
|
for a in message.attachments |
|
|
|
|
if os.path.splitext(a.filename)[1] in [".png", ".jpg", ".jpeg"] |
|
|
|
|
), |
|
|
|
|
None, |
|
|
|
|
) |
|
|
|
|
if attachment is not None: |
|
|
|
|
msg["has_attachment"] = True |
|
|
|
|
msg["attachment_filename"] = attachment.filename |
|
|
|
|
msg["attachment_data"] = await attachment.read() |
|
|
|
|
|
|
|
|
|
return msg |
|
|
|
|
|
|
|
|
|
async def send_cached_message(self, channel, message): |
|
|
|
|
if message["has_attachment"] == True: |
|
|
|
|
file = discord.File( |
|
|
|
|
io.BytesIO(message["attachment_data"]), |
|
|
|
|
filename=message["attachment_filename"], |
|
|
|
|
) |
|
|
|
|
await channel.send(content=message["content"], file=file) |
|
|
|
|
else: |
|
|
|
|
await channel.send(content=message["content"]) |
|
|
|
|
|
|
|
|
|
# Commands |
|
|
|
|
|
|
|
|
|
@commands.guild_only() |
|
|
|
|
@commands.command(aliases=["list"]) |
|
|
|
|
async def listitem(self, ctx, channel: discord.TextChannel, number: int): |
|
|
|
|
"""Link to a specific list item.""" |
|
|
|
|
await self.link_list_item(ctx, channel, number) |
|
|
|
|
|
|
|
|
|
@commands.guild_only() |
|
|
|
|
@commands.command(aliases=["rule"]) |
|
|
|
|
async def rules(self, ctx, number: int): |
|
|
|
|
"""Link to a specific list item in #rules""" |
|
|
|
|
channel = ctx.guild.get_channel(config.rules_channel[ctx.guild.id]) |
|
|
|
|
await self.link_list_item(ctx, channel, number) |
|
|
|
|
|
|
|
|
|
@commands.guild_only() |
|
|
|
|
@commands.check(check_if_staff) |
|
|
|
|
@commands.command(aliases=["warnrule", "ruleswarn", "warnrules"]) |
|
|
|
|
async def rulewarn( |
|
|
|
|
self, ctx, target: discord.Member, number: int, *, reason: str = "" |
|
|
|
|
): |
|
|
|
|
if "Mod" not in self.bot.cogs: |
|
|
|
|
await ctx.send("Mod cog must be loaded to run this command.") |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
mod_cog = self.bot.cogs["Mod"] |
|
|
|
|
warn_command = None |
|
|
|
|
for command in mod_cog.get_commands(): |
|
|
|
|
if command.name == "warn": |
|
|
|
|
warn_command = command |
|
|
|
|
break |
|
|
|
|
|
|
|
|
|
if warn_command is None: |
|
|
|
|
await ctx.send("Unable to find the warn command from the Mod cog.") |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
if not await warn_command.can_run(ctx): |
|
|
|
|
await ctx.send("Unable to run the warn command from the Mod cog.") |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
if ( |
|
|
|
|
len(warn_command.params) != 4 |
|
|
|
|
or "self" not in warn_command.params |
|
|
|
|
or "ctx" not in warn_command.params |
|
|
|
|
or "target" not in warn_command.params |
|
|
|
|
or "reason" not in warn_command.params |
|
|
|
|
): |
|
|
|
|
await ctx.send("Warn's signature has changed please update the Lists cog.") |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
channel = ctx.guild.get_channel(config.rules_channel[ctx.guild.id]) |
|
|
|
|
if await self.link_list_item(ctx, channel, number): |
|
|
|
|
await warn_command.callback( |
|
|
|
|
mod_cog, ctx=ctx, target=target, reason=f"Rule {number} - {reason}" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
# Listeners |
|
|
|
|
|
|
|
|
|
@Cog.listener() |
|
|
|
|
async def on_raw_reaction_add(self, payload): |
|
|
|
|
await self.bot.wait_until_ready() |
|
|
|
|
|
|
|
|
|
# We only care about reactions in Rules, and Support FAQ |
|
|
|
|
if payload.channel_id not in config.list_channels[payload.guild_id]: |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
channel = self.bot.get_channel(payload.channel_id) |
|
|
|
|
message = await channel.fetch_message(payload.message_id) |
|
|
|
|
member = channel.guild.get_member(payload.user_id) |
|
|
|
|
user = self.bot.get_user(payload.user_id) |
|
|
|
|
reaction = next( |
|
|
|
|
( |
|
|
|
|
reaction |
|
|
|
|
for reaction in message.reactions |
|
|
|
|
if str(reaction.emoji) == str(payload.emoji) |
|
|
|
|
), |
|
|
|
|
None, |
|
|
|
|
) |
|
|
|
|
if reaction is None: |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
# Only staff can add reactions in these channels. |
|
|
|
|
if not check_if_target_is_staff(payload.guild_id, member): |
|
|
|
|
await reaction.remove(user) |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
# Reactions are only allowed on messages from the bot. |
|
|
|
|
if not message.author.bot: |
|
|
|
|
await reaction.remove(user) |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
# Only certain reactions are allowed. |
|
|
|
|
if not self.is_reaction_valid(reaction): |
|
|
|
|
await reaction.remove(user) |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
# Remove all other reactions from user in this channel. |
|
|
|
|
for r in await self.find_reactions(payload.user_id, payload.channel_id): |
|
|
|
|
if r.message.id != message.id or ( |
|
|
|
|
r.message.id == message.id and str(r.emoji) != str(reaction.emoji) |
|
|
|
|
): |
|
|
|
|
await r.remove(user) |
|
|
|
|
|
|
|
|
|
# When editing we want to provide the user a copy of the raw text. |
|
|
|
|
if ( |
|
|
|
|
self.is_edit(reaction.emoji) |
|
|
|
|
and config.list_files_channel[payload.guild_id] != 0 |
|
|
|
|
): |
|
|
|
|
files_channel = self.bot.get_channel( |
|
|
|
|
config.list_files_channel[payload.guild_id] |
|
|
|
|
) |
|
|
|
|
file = discord.File( |
|
|
|
|
io.BytesIO(message.content.encode("utf-8")), |
|
|
|
|
filename=f"{message.id}.txt", |
|
|
|
|
) |
|
|
|
|
file_message = await files_channel.send(file=file) |
|
|
|
|
|
|
|
|
|
embed = discord.Embed( |
|
|
|
|
title="Click here to get the raw text to modify.", |
|
|
|
|
url=f"{file_message.attachments[0].url}?", |
|
|
|
|
) |
|
|
|
|
embed.add_field(name="Message ID", value=file_message.id, inline=False) |
|
|
|
|
await message.edit(embed=embed) |
|
|
|
|
|
|
|
|
|
@Cog.listener() |
|
|
|
|
async def on_raw_reaction_remove(self, payload): |
|
|
|
|
await self.bot.wait_until_ready() |
|
|
|
|
|
|
|
|
|
# We only care about reactions in Rules, and Support FAQ |
|
|
|
|
if payload.channel_id not in config.list_channels[payload.guild_id]: |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
channel = self.bot.get_channel(payload.channel_id) |
|
|
|
|
message = await channel.fetch_message(payload.message_id) |
|
|
|
|
|
|
|
|
|
# Reaction was removed from a message we don't care about. |
|
|
|
|
if not message.author.bot: |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
# We want to remove the embed we added. |
|
|
|
|
if ( |
|
|
|
|
self.is_edit(payload.emoji) |
|
|
|
|
and config.list_files_channel[payload.guild_id] != 0 |
|
|
|
|
): |
|
|
|
|
await self.clean_up_raw_text_file_message(payload.guild_id, message) |
|
|
|
|
|
|
|
|
|
@Cog.listener() |
|
|
|
|
async def on_message(self, message): |
|
|
|
|
await self.bot.wait_until_ready() |
|
|
|
|
|
|
|
|
|
# We only care about messages in Rules, and Support FAQ |
|
|
|
|
if message.channel.id not in config.list_channels[message.guild.id]: |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
# We don't care about messages from bots. |
|
|
|
|
if message.author.bot: |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
# Only staff can modify lists. |
|
|
|
|
if not check_if_target_is_staff(message.guild.id, message.author): |
|
|
|
|
await message.delete() |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
channel = message.channel |
|
|
|
|
content = message.content |
|
|
|
|
user = message.author |
|
|
|
|
|
|
|
|
|
attachment_filename = None |
|
|
|
|
attachment_data = None |
|
|
|
|
if len(message.attachments) != 0: |
|
|
|
|
# Lists will only reupload the first image. |
|
|
|
|
attachment = next( |
|
|
|
|
( |
|
|
|
|
a |
|
|
|
|
for a in message.attachments |
|
|
|
|
if os.path.splitext(a.filename)[1] in [".png", ".jpg", ".jpeg"] |
|
|
|
|
), |
|
|
|
|
None, |
|
|
|
|
) |
|
|
|
|
if attachment is not None: |
|
|
|
|
attachment_filename = attachment.filename |
|
|
|
|
attachment_data = await attachment.read() |
|
|
|
|
|
|
|
|
|
await message.delete() |
|
|
|
|
|
|
|
|
|
reactions = await self.find_reactions(user.id, channel.id) |
|
|
|
|
|
|
|
|
|
# Add to the end of the list if there is no reactions or somehow more |
|
|
|
|
# than one. |
|
|
|
|
if len(reactions) != 1: |
|
|
|
|
if attachment_filename is not None and attachment_data is not None: |
|
|
|
|
file = discord.File( |
|
|
|
|
io.BytesIO(attachment_data), filename=attachment_filename |
|
|
|
|
) |
|
|
|
|
await channel.send(content=content, file=file) |
|
|
|
|
else: |
|
|
|
|
await channel.send(content) |
|
|
|
|
|
|
|
|
|
for reaction in reactions: |
|
|
|
|
await reaction.remove(user) |
|
|
|
|
|
|
|
|
|
await self.bot.botlog_channels[message.guild.id].send( |
|
|
|
|
self.create_log_message("๐ฌ", "List item added:", user, channel) |
|
|
|
|
) |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
targeted_reaction = reactions[0] |
|
|
|
|
targeted_message = targeted_reaction.message |
|
|
|
|
|
|
|
|
|
if self.is_edit(targeted_reaction): |
|
|
|
|
if config.list_files_channel[message.guild.id] != 0: |
|
|
|
|
await self.clean_up_raw_text_file_message( |
|
|
|
|
message.guild.id, targeted_message |
|
|
|
|
) |
|
|
|
|
await targeted_message.edit(content=content) |
|
|
|
|
await targeted_reaction.remove(user) |
|
|
|
|
|
|
|
|
|
await self.bot.botlog_channels[message.guild.id].send( |
|
|
|
|
self.create_log_message("๐", "List item edited:", user, channel) |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
elif self.is_delete(targeted_reaction): |
|
|
|
|
await targeted_message.delete() |
|
|
|
|
|
|
|
|
|
await self.bot.botlog_channels[message.guild.id].send( |
|
|
|
|
self.create_log_message( |
|
|
|
|
"โ", "List item deleted:", user, channel, content |
|
|
|
|
) |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
elif self.is_recycle(targeted_reaction): |
|
|
|
|
messages = [await self.cache_message(targeted_message)] |
|
|
|
|
|
|
|
|
|
for message in await channel.history( |
|
|
|
|
limit=None, after=targeted_message, oldest_first=True |
|
|
|
|
).flatten(): |
|
|
|
|
messages.append(await self.cache_message(message)) |
|
|
|
|
|
|
|
|
|
await channel.purge(limit=len(messages) + 1, bulk=True) |
|
|
|
|
|
|
|
|
|
for message in messages: |
|
|
|
|
await self.send_cached_message(channel, message) |
|
|
|
|
|
|
|
|
|
await self.bot.botlog_channels[message.guild.id].send( |
|
|
|
|
self.create_log_message( |
|
|
|
|
"โป", "List item recycled:", user, channel, content |
|
|
|
|
) |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
elif self.is_insert_above(targeted_reaction): |
|
|
|
|
messages = [await self.cache_message(targeted_message)] |
|
|
|
|
|
|
|
|
|
for message in await channel.history( |
|
|
|
|
limit=None, after=targeted_message, oldest_first=True |
|
|
|
|
).flatten(): |
|
|
|
|
messages.append(await self.cache_message(message)) |
|
|
|
|
|
|
|
|
|
await channel.purge(limit=len(messages) + 1, bulk=True) |
|
|
|
|
|
|
|
|
|
if attachment_filename is not None and attachment_data is not None: |
|
|
|
|
file = discord.File( |
|
|
|
|
io.BytesIO(attachment_data), filename=attachment_filename |
|
|
|
|
) |
|
|
|
|
await channel.send(content=content, file=file) |
|
|
|
|
else: |
|
|
|
|
await channel.send(content) |
|
|
|
|
|
|
|
|
|
for message in messages: |
|
|
|
|
await self.send_cached_message(channel, message) |
|
|
|
|
|
|
|
|
|
await self.bot.botlog_channels[message.guild.id].send( |
|
|
|
|
self.create_log_message("๐ฌ", "List item added:", user, channel) |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
elif self.is_insert_below(targeted_reaction): |
|
|
|
|
await targeted_reaction.remove(user) |
|
|
|
|
|
|
|
|
|
messages = [] |
|
|
|
|
|
|
|
|
|
for message in await channel.history( |
|
|
|
|
limit=None, after=targeted_message, oldest_first=True |
|
|
|
|
).flatten(): |
|
|
|
|
messages.append(await self.cache_message(message)) |
|
|
|
|
|
|
|
|
|
await channel.purge(limit=len(messages), bulk=True) |
|
|
|
|
|
|
|
|
|
if attachment_filename is not None and attachment_data is not None: |
|
|
|
|
file = discord.File( |
|
|
|
|
io.BytesIO(attachment_data), filename=attachment_filename |
|
|
|
|
) |
|
|
|
|
await channel.send(content=content, file=file) |
|
|
|
|
else: |
|
|
|
|
await channel.send(content) |
|
|
|
|
|
|
|
|
|
for message in messages: |
|
|
|
|
await self.send_cached_message(channel, message) |
|
|
|
|
|
|
|
|
|
await self.bot.botlog_channels[message.guild.id].send( |
|
|
|
|
self.create_log_message("๐ฌ", "List item added:", user, channel) |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def setup(bot): |
|
|
|
|
bot.add_cog(Lists(bot)) |