diff --git a/.gitignore b/.gitignore index bf8bf731ab1..0de90971986 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,6 @@ Pipfile.lock .vscode/ *.sublime-project *.sublime-workspace - ## Plugin-specific files: # IntelliJ diff --git a/.vs/Repo/FileContentIndex/0e55f6bc-90b2-4384-b31e-0f5e912016dd.vsidx b/.vs/Repo/FileContentIndex/0e55f6bc-90b2-4384-b31e-0f5e912016dd.vsidx new file mode 100644 index 00000000000..ebe8df26411 Binary files /dev/null and b/.vs/Repo/FileContentIndex/0e55f6bc-90b2-4384-b31e-0f5e912016dd.vsidx differ diff --git a/.vs/Repo/FileContentIndex/1e3aedbe-63e9-4c05-9f49-2a4b8e07f5ef.vsidx b/.vs/Repo/FileContentIndex/1e3aedbe-63e9-4c05-9f49-2a4b8e07f5ef.vsidx new file mode 100644 index 00000000000..e9163de07fe Binary files /dev/null and b/.vs/Repo/FileContentIndex/1e3aedbe-63e9-4c05-9f49-2a4b8e07f5ef.vsidx differ diff --git a/.vs/Repo/FileContentIndex/300d4243-368c-4679-84c4-f975bc660c83.vsidx b/.vs/Repo/FileContentIndex/300d4243-368c-4679-84c4-f975bc660c83.vsidx new file mode 100644 index 00000000000..e2e3153a92b Binary files /dev/null and b/.vs/Repo/FileContentIndex/300d4243-368c-4679-84c4-f975bc660c83.vsidx differ diff --git a/.vs/Repo/FileContentIndex/935c11a2-5695-449c-ab0f-08ef0ff61f1a.vsidx b/.vs/Repo/FileContentIndex/935c11a2-5695-449c-ab0f-08ef0ff61f1a.vsidx new file mode 100644 index 00000000000..e2fd6502ea4 Binary files /dev/null and b/.vs/Repo/FileContentIndex/935c11a2-5695-449c-ab0f-08ef0ff61f1a.vsidx differ diff --git a/.vs/Repo/FileContentIndex/a0281b3a-e7ca-411f-97f3-a25201d3d18c.vsidx b/.vs/Repo/FileContentIndex/a0281b3a-e7ca-411f-97f3-a25201d3d18c.vsidx new file mode 100644 index 00000000000..71a170609ab Binary files /dev/null and b/.vs/Repo/FileContentIndex/a0281b3a-e7ca-411f-97f3-a25201d3d18c.vsidx differ diff --git a/.vs/Repo/FileContentIndex/read.lock b/.vs/Repo/FileContentIndex/read.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.vs/Repo/v17/.suo b/.vs/Repo/v17/.suo new file mode 100644 index 00000000000..a88a39ac2b8 Binary files /dev/null and b/.vs/Repo/v17/.suo differ diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json new file mode 100644 index 00000000000..2404ce1e2da --- /dev/null +++ b/.vs/VSWorkspaceState.json @@ -0,0 +1,9 @@ +{ + "ExpandedNodes": [ + "", + "\\cogs", + "\\cogs\\welcome" + ], + "SelectedNode": "\\cogs\\welcome\\welcome.py", + "PreviewInSolutionExplorer": false +} \ No newline at end of file diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite new file mode 100644 index 00000000000..3d287e9ce70 Binary files /dev/null and b/.vs/slnx.sqlite differ diff --git a/cogs/.vs/VSWorkspaceState.json b/cogs/.vs/VSWorkspaceState.json new file mode 100644 index 00000000000..33376ae5e13 --- /dev/null +++ b/cogs/.vs/VSWorkspaceState.json @@ -0,0 +1,6 @@ +{ + "ExpandedNodes": [ + "\\heist" + ], + "PreviewInSolutionExplorer": false +} \ No newline at end of file diff --git a/cogs/.vs/cogs/FileContentIndex/00ee2c40-4b90-45e4-b94f-bf3b6e724781.vsidx b/cogs/.vs/cogs/FileContentIndex/00ee2c40-4b90-45e4-b94f-bf3b6e724781.vsidx new file mode 100644 index 00000000000..6d7f497eeb7 Binary files /dev/null and b/cogs/.vs/cogs/FileContentIndex/00ee2c40-4b90-45e4-b94f-bf3b6e724781.vsidx differ diff --git a/cogs/.vs/cogs/FileContentIndex/5af01e3b-383a-48b9-ab1c-c7d6dd282aac.vsidx b/cogs/.vs/cogs/FileContentIndex/5af01e3b-383a-48b9-ab1c-c7d6dd282aac.vsidx new file mode 100644 index 00000000000..7c6fa3dd025 Binary files /dev/null and b/cogs/.vs/cogs/FileContentIndex/5af01e3b-383a-48b9-ab1c-c7d6dd282aac.vsidx differ diff --git a/cogs/.vs/cogs/FileContentIndex/7ae3e1e8-8be2-4f31-b418-dcb3239e6015.vsidx b/cogs/.vs/cogs/FileContentIndex/7ae3e1e8-8be2-4f31-b418-dcb3239e6015.vsidx new file mode 100644 index 00000000000..7c6fa3dd025 Binary files /dev/null and b/cogs/.vs/cogs/FileContentIndex/7ae3e1e8-8be2-4f31-b418-dcb3239e6015.vsidx differ diff --git a/cogs/.vs/cogs/FileContentIndex/88dc1ec0-ec2d-40b6-9464-1c268950fcc6.vsidx b/cogs/.vs/cogs/FileContentIndex/88dc1ec0-ec2d-40b6-9464-1c268950fcc6.vsidx new file mode 100644 index 00000000000..7c6fa3dd025 Binary files /dev/null and b/cogs/.vs/cogs/FileContentIndex/88dc1ec0-ec2d-40b6-9464-1c268950fcc6.vsidx differ diff --git a/cogs/.vs/cogs/FileContentIndex/read.lock b/cogs/.vs/cogs/FileContentIndex/read.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cogs/.vs/cogs/v17/.suo b/cogs/.vs/cogs/v17/.suo new file mode 100644 index 00000000000..fe80741d2d7 Binary files /dev/null and b/cogs/.vs/cogs/v17/.suo differ diff --git a/cogs/.vs/slnx.sqlite b/cogs/.vs/slnx.sqlite new file mode 100644 index 00000000000..c0543625776 Binary files /dev/null and b/cogs/.vs/slnx.sqlite differ diff --git a/cogs/welcome/constants.py b/cogs/welcome/constants.py index e2b1afdcbf3..cd89b3a80b4 100644 --- a/cogs/welcome/constants.py +++ b/cogs/welcome/constants.py @@ -16,10 +16,18 @@ KEY_WELCOME_CHANNEL_SETTINGS = "welcomeChannelSettings" KEY_POST_FAILED_DM = "postFailedDm" KEY_JOINED_USER_IDS = "joinedUserIds" +KEY_TOGGLE_RANDOM_IMG = "toggleImg" +WELCOME_IMG_FOLDER = "welcomeImgs" +WELCOME_IMG_TEMPLATE = "welcome_template.png" MAX_MESSAGE_LENGTH = 2000 MAX_DESCRIPTION_LENGTH = 500 +IMAGE_DIMENSION_HEIGHT = 1193 +IMAGE_DIMENSION_WIDTH = 671 +IMAGE_RESAMPLE_TYPE = 2 +IMAGE_DPI = 72 + DEFAULT_GUILD = { KEY_DM_ENABLED: False, KEY_LOG_JOIN_ENABLED: False, @@ -38,6 +46,7 @@ KEY_POST_FAILED_DM: False, }, KEY_JOINED_USER_IDS: [], + KEY_TOGGLE_RANDOM_IMG: False, } diff --git a/cogs/welcome/data/BORDER.png b/cogs/welcome/data/BORDER.png new file mode 100644 index 00000000000..e24408554c8 Binary files /dev/null and b/cogs/welcome/data/BORDER.png differ diff --git a/cogs/welcome/data/BORDER_mask.png b/cogs/welcome/data/BORDER_mask.png new file mode 100644 index 00000000000..e263ed43895 Binary files /dev/null and b/cogs/welcome/data/BORDER_mask.png differ diff --git a/cogs/welcome/data/MASK.png b/cogs/welcome/data/MASK.png new file mode 100644 index 00000000000..36cececabe6 Binary files /dev/null and b/cogs/welcome/data/MASK.png differ diff --git a/cogs/welcome/data/welcome_template.png b/cogs/welcome/data/welcome_template.png new file mode 100644 index 00000000000..72224690e0d Binary files /dev/null and b/cogs/welcome/data/welcome_template.png differ diff --git a/cogs/welcome/welcome.py b/cogs/welcome/welcome.py index b2da48e671c..843307ea25e 100644 --- a/cogs/welcome/welcome.py +++ b/cogs/welcome/welcome.py @@ -1,20 +1,25 @@ """Welcome cog Sends welcome DMs to users that join the server. """ - import asyncio -import discord +import io import logging +import os import random +import pathlib -from redbot.core import Config, checks, commands +import discord +import aiohttp +from PIL import Image, ImageChops, ImageOps + +from redbot.core import Config, checks, commands, data_manager from redbot.core.bot import Red from redbot.core.commands.context import Context from redbot.core.utils.chat_formatting import box, info, pagify, warning from redbot.core.utils.menus import DEFAULT_CONTROLS, menu from redbot.core.utils import AsyncIter -from typing import Optional +from typing import Optional from .constants import * from .helpers import createTagListPages @@ -28,8 +33,19 @@ class Welcome(commands.Cog): # pylint: disable=too-many-instance-attributes def __init__(self, bot: Red): self.bot = bot self.config = Config.get_conf(self, identifier=5842647, force_registration=True) + self.config.register_guild(**DEFAULT_GUILD) + self.dataDir = data_manager.cog_data_path(cog_instance=self) + self.imgDir = self.dataDir / WELCOME_IMG_FOLDER + # create folder to hold welcome images + try: + self.imgDir.mkdir(parents=True, exist_ok=True) + except OSError as error: + errorMessage = "Could not create folder for images!" + LOGGER.error(errorMessage, exc_info=True) + raise RuntimeError(errorMessage) from error + async def getRandomMessage(self, guild: discord.Guild, pool: Optional[GreetingPools] = None): """Gets a random message from a greeting pool. @@ -149,6 +165,10 @@ async def sendWelcomeMessageChannel(self, newUser: discord.Member): isSet = await self.config.guild(guild).get_attr(KEY_WELCOME_CHANNEL_ENABLED)() # if channel isn't set if not isSet: + LOGGER.error( + "Could not send welcome message because no welcome channel is set. Use command [p]welcomeset greetings channelset channel to set one.", + exc_info=True, + ) return channel = discord.utils.get(guild.channels, id=channelID) @@ -161,7 +181,14 @@ async def sendWelcomeMessageChannel(self, newUser: discord.Member): message = rawMessage.replace("{USER}", newUser.mention) try: - await channel.send(message) + if await self.config.guild(guild).get_attr(KEY_TOGGLE_RANDOM_IMG)(): + img = await self.generateRandWelcomeImg(newUser, newUser.guild) + if img == None: + raise TypeError + await channel.send(message, file=discord.File(img, filename="generated.png")) + img.close() + else: + await channel.send(message) except (discord.Forbidden, discord.HTTPException) as errorMsg: LOGGER.error( "Could not send message, please make sure the bot " @@ -170,6 +197,9 @@ async def sendWelcomeMessageChannel(self, newUser: discord.Member): exc_info=True, ) LOGGER.error(errorMsg) + except TypeError as errorMsg: + LOGGER.error("Image could not be generated.", exc_info=True) + LOGGER.error(errorMsg) else: LOGGER.info( "User %s#%s (%s) has joined. Posted welcome message.", @@ -318,6 +348,68 @@ async def logServerLeave(self, leaveUser: discord.Member): leaveUser.id, ) + async def generateRandWelcomeImg(self, user: discord.member, guild: discord.guild): + """create an image for the specific player using their avatar and an image from the random image pool, then returns it""" + base = Image.open( + self.imgDir / str(guild.id) / random.choice(os.listdir(self.imgDir / str(guild.id))) + ) + mask = Image.open(data_manager.bundled_data_path(self) / "MASK.png") + borderOverlay = Image.open(data_manager.bundled_data_path(self) / "BORDER.png") + borderOverlayMask = Image.open(data_manager.bundled_data_path(self) / "BORDER_mask.png") + # get avatar from User + avatar: bytes + retrievedAvatar: Image + session = aiohttp.ClientSession() + # a header to successfully download user avatars for use + usedHeader = {"User-agent": "Mozilla/5.0"} + + try: + async with session.get(str(user.avatar.url), headers=usedHeader) as webp: + avatar = await webp.read() + retrievedAvatar = Image.open(io.BytesIO(avatar)) + if not retrievedAvatar: + base.close() + mask.close() + borderOverlay.close() + borderOverlayMask.close() + LOGGER.error( + "Could not retrieve user profile picture from discord servers.", exc_info=1 + ) + return + except aiohttp.ClientResponseError: + pass + + retrievedAvatar = retrievedAvatar.resize((325, 325), 1) + base.paste(borderOverlay, (434, 0), borderOverlayMask) + base.paste(retrievedAvatar, (434, 0), mask) + generated = io.BytesIO() + base.save(generated, format="png") + generated.seek(0) + base.close() + mask.close() + borderOverlay.close() + borderOverlayMask.close() + return generated + + async def ensureCurrentServerHasImgCache(self, channel: discord.channel): + """ + Check if there is a folder in the image cache for the associated server. If one doesn't exist, creates it. + """ + idStr = str(channel.guild.id) + fp = self.imgDir / idStr + if os.path.exists(fp): + return + + try: + os.makedirs(fp, exist_ok=True) + await channel.send("No image cache folder found for this server! Created one") + + except OSError as info: + LOGGER.info( + "Could not create folder for server: %s" % idStr, + exc_info=True, + ) + #################### # MESSAGE COMMANDS # #################### @@ -470,6 +562,24 @@ async def greetings(self, ctx: Context): - `returning`: pool of greetings that are sent to returning users """ + # [p]welcomeset greetings toggleimg + @greetings.command(name="toggleimg") + async def toggleImg(self, ctx: Context): + """Toggle the random image on and off""" + await self.ensureCurrentServerHasImgCache(ctx.channel) + toggleImgConfig = self.config.guild(ctx.guild).get_attr(KEY_TOGGLE_RANDOM_IMG) + randomImageEnabled = await toggleImgConfig() + + if randomImageEnabled: + await toggleImgConfig.set(False) + else: + if len(os.listdir(self.imgDir / str(ctx.guild.id))) < 1: + await ctx.send("Add at least one image before turning the randomiser on") + return + await toggleImgConfig.set(True) + + await ctx.send(f"Sending randomised welcome image: {await toggleImgConfig()}") + # [p]welcomeset greetings add @greetings.command(name="add") async def greetAdd(self, ctx: Context, name: str, pool: Optional[str] = None): @@ -537,6 +647,106 @@ def check(message: discord.Message): await self.config.guild(ctx.guild).get_attr(key).set(greetings) return + # [p]welcomeset greetings image + @greetings.group(name="image") + async def image(self, ctx: Context): + """Base command for the image command group""" + + # [p]welcomeset greetings image template + @image.command(name="template") + async def imageTemplate(self, ctx: Context): + await ctx.send( + "Here is the welcome image template so you can make your own! For best results please render the image at IMAGE_DPI dpi, IMAGE_DIMENSION_HEIGHT x IMAGE_DIMENSION_WIDTH. The bot will try to make it conform automatically but mileage may vary.", + file=discord.File(data_manager.bundled_data_path(self) / WELCOME_IMG_TEMPLATE), + ) + + # [p]welcomeset greetings image add + @image.command(name="add") + async def imgAdd(self, ctx: Context, name: str): + """Add the attached image to the pool of random based images used to generate custom welcome images. Attaches only the first image attached. + + + Additionally automatically makes the sent image conform to the dimensions and dpi that's been tested for: 72dpi, 1193x671. Mileage may vary + + """ + await self.ensureCurrentServerHasImgCache(ctx.channel) + file_name = f"{name}.png" + fp = self.imgDir / str(ctx.guild.id) / file_name + + if os.path.exists(fp): + await ctx.reply( + "This name is already in use! For ease of management, please use another name." + ) + return + + image = None + if len(ctx.message.attachments) == 1: + image = ctx.message.attachments[0] + + else: + await ctx.reply( + "You need to attach exactly 1 image in the message that uses this command" + ) + return + + await image.save(fp) + + # Performing necessary checks to ensure that this base can produce a good generated image + temp = Image.open(fp) + temp_resize = temp.resize( + (IMAGE_DIMENSION_HEIGHT, IMAGE_DIMENSION_WIDTH), IMAGE_RESAMPLE_TYPE + ) + temp_resize.save(fp, dpi=(IMAGE_DPI, IMAGE_DPI)) + + # alert user that their image has been added + await ctx.reply("Image added to this server's image cache") + + # [p]welcomeset greetings image remove + @image.command(name="remove") + async def imgRemove(self, ctx: Context, imgName: str): + """Removes the specified image from the pool""" + await self.ensureCurrentServerHasImgCache(ctx.channel) + fileName = f"{imgName}.png" + try: + os.remove(self.imgDir / str(ctx.guild.id) / fileName) + except: + await ctx.reply("The named image doesn't exist") + return + + if len(os.listdir(self.imgDir / str(ctx.channel.guild.id))) == 0: + self.config.guild(ctx.guild).get_attr(KEY_TOGGLE_RANDOM_IMG).set(False) + await ctx.reply("Last image deleted. Image randomiser turned off.") + + await ctx.reply("Image sucessfully removed") + + # [p]welcomeset greetings image view + @image.command(name="view") + async def showImg(self, ctx: Context, imgName: str): + """Show the named image from the image pool if it exists""" + await self.ensureCurrentServerHasImgCache(ctx.channel) + fileName = f"{imgName}.png" + try: + await ctx.send( + imgName + ":", + file=discord.File(self.imgDir / str(ctx.channel.guild.id) / fileName), + ) + except: + await ctx.reply("The named image doesn't exist") + + # [p]welcomeset greetings image list + @image.command(name="list") + async def listImg(self, ctx: Context): + """Display a list of all the images in this server's image cache""" + await self.ensureCurrentServerHasImgCache(ctx.channel) + listOfImages = "\n".join( + imagePath.stem + for imagePath in pathlib.Path(self.imgDir / str(ctx.channel.guild.id)).iterdir() + ) + if len(listOfImages) == 0: + await ctx.reply("No images added yet.") + return + await ctx.reply(listOfImages) + # [p]welcomeset greetings channelset @greetings.group(name="channelset", aliases=["channelconfig", "chconfig", "chset"]) async def greetChannelConfig(self, ctx: Context):