async def cmyk_float(self, ctx, c, m, y, k): """ Displays info on a colour given in the CMYK (cyan-magenta-yellow-key) colour space. "Key" may also be defined as "black". """ try: r, g, b = utils.from_cmyk(c, m, y, k) except (ValueError, TypeError) as ex: raise neko.NekoCommandError(str(ex)) else: await single_colour_response(ctx, r, g, b)
async def lookup(self, ctx, *, string: str): """ This is the opposite of `charinfo`. It will look up a given string as a unicode character name and if it finds a character matching the input, it will show unicode information about it. """ try: character = unicodedata.lookup(string.upper()) await ctx.send('\n'.join(_unicode_table(character))) except KeyError: raise neko.NekoCommandError('No character exists for that ' 'description.')
async def rgba_byte(self, ctx, r, g, b, a='255'): """ If alpha is omitted, then it gets the value of 255 by default. All values must be in the range 0 ≤ x < 256. """ try: r, g, b, a = (utils.ensure_int_in_0_255(x) for x in (r, g, b, a)) for x in (r, g, b, a): if not 0 <= x < 256: raise TypeError('Must be in range [0, 256)') await single_colour_response(ctx, r, g, b, a) except (ValueError, TypeError) as ex: raise neko.NekoCommandError(ex)
async def tag_inspect(self, ctx, tag_name): """ This is only runnable by the bot owner. """ async with ctx.bot.postgres_pool.acquire() as conn: book = neko.Book(ctx) with ctx.typing(): tag_name = tag_name.lower() results = await conn.fetch( ''' SELECT * FROM nekozilla.tags LEFT JOIN nekozilla.tags_attach ON pk = tag_pk WHERE LOWER(name) = ($1); ''', tag_name) if not results: raise neko.NekoCommandError('No results.') for result in results: data = dict(result) content = data.pop('content') author = data.pop('author') file = data.pop('file_name') # Don't want to send this!!! data.pop('b64data') user: discord.User = await ctx.bot.get_user_info(author) data['author'] = ' '.join([ 'BOT' if user.bot else '', user.display_name, str(user.id) ]) page = neko.Page(title=f'`{data.pop("name")}`', description=content) page.set_thumbnail(url=user.avatar_url) if file is not None: page.set_footer(text=f'Attached file: {file}') page.add_field(name='Attributes', value='\n'.join(f'**{k}**: `{v}`' for k, v in data.items())) book += page await book.send()
async def palette(self, ctx, *, colours): """ This is experimental, and slow. Thus, there is a cooldown restriction on this command to prevent slowing the bot down for everyone else. This only supports RGB-hex strings currently, as well as RGB-bytes, and HTML colour names. Anything that contains spaces must be surrounded by quotes. """ try: with ctx.typing(), io.BytesIO() as fp: # Parse args colours = neko.parse_quotes(colours) await ctx.bot.do_job_in_pool(utils.make_palette, fp, *colours) file = discord.File(fp, 'palette.png') await ctx.send(file=file) except (ValueError, TypeError, KeyError) as ex: string = f'No match: {ex}' raise neko.NekoCommandError(string)
async def tag_add_global(self, ctx: neko.Context, tag_name, *, content): """ This is only currently accessible by the bot owner. """ attachment = ctx.message.attachments if ctx.author.id == self.bot.owner_id: self.tag_add.reset_cooldown(ctx) if attachment: if len(attachment) > 1: raise neko.NekoCommandError( 'Can only upload at most one file.') else: # Get the first attachment. attachment: discord.Attachment = attachment[0] if attachment.size > _MAX_IMAGE_SIZE: raise neko.NekoCommandError( f'You can upload a maximum of {_MAX_IMAGE_SIZE/1024} ' 'KiB per tag.') else: attachment = None tag_name = tag_name.lower() # First, make sure tag is valid. if tag_name in self.invalid_tag_names: raise neko.NekoCommandError('Invalid tag name') async with self.bot.postgres_pool.acquire() as conn: # This is a multiple part query with a select and two inserts. # Transaction usage means if something else f***s up then ALL # changes made up to that point are deferred safely and the # database is left in a stable state. async with conn.transaction(isolation='serializable', readonly=False, deferrable=False): with ctx.channel.typing(): # Next, see if the tag already exists. existing = await conn.fetch( ''' SELECT 1 FROM nekozilla.tags WHERE LOWER(name) = ($1) AND guild IS NULL; ''', tag_name) if len(existing) > 0: raise neko.NekoCommandError('Tag already exists') await conn.execute( ''' INSERT INTO nekozilla.tags (name, author, guild, is_nsfw, content) VALUES (($1), ($2), NULL, ($3), ($4)); ''', tag_name, ctx.author.id, ctx.channel.nsfw, content) # If we have an attachment, we must first fetch it. if attachment is not None: resp = await self.bot.request('GET', attachment.url) data: bytes = await resp.read() base64_img = base64.b64encode(data) # Discord or Discord.py removes my bloody file extension # from the file name!!!!! REEE! self.logger.info( f'{ctx.author} uploaded {tag_name} {attachment.url}' f' in {ctx.guild}.{ctx.channel}. It was global.') url: str = attachment.url start_index = url.find(attachment.filename) if start_index != -1: attachment.filename = url[start_index:] await conn.execute( ''' INSERT INTO nekozilla.tags_attach (tag_pk, file_name, b64data) VALUES ( ( -- TODO: make this not shit. SELECT pk FROM nekozilla.tags WHERE name = ($1) AND guild IS NULL AND author = ($2) LIMIT 1 ), ($3), ($4) ); ''', tag_name, ctx.author.id, attachment.filename, base64_img.decode()) await self._del_msg_soon(ctx, await ctx.send('Added globally.'))
async def tag_group(self, ctx: neko.Context, tag_name=None, *args): """ Displays the tag if it can be found. The local tags are searched first, and then the global tags. If we start the tag with an "!", then we try the global tags list first instead. **22nd Jan 2018**: I have added a few meta-commands into the mix. From now on, the following can be added into a tag, and it will be resolved when the tag is retrieved: ${args} -> any text you put after the tag name.\r ${channel} -> the channel name.\r ${channel_mention} -> the channel name, but as a mention.\r ${channel_id} -> the channel snowflake ID.\r ${author} -> the display name of whoever invoked the tag.\r ${author_mention} -> mentions whoever invoked the tag.\r ${author_discriminator} -> shows the discriminator of whoever invoked the tag.\r ${author_username} -> shows the username of whoever invoked the tag. ${author_id} -> shows the snowflake ID for the user who invoked the tag. ${guild} -> shows the name of the guild (server) the tag is called on. ${guild_id} -> shows the ID of the guild (server) the tag is called on. """ if tag_name is None: book = neko.PaginatedBook(ctx=ctx, title='Tags', max_lines=15) desc = f'Run {ctx.prefix}help tag <command> for more info.\n\n' page = neko.Page(title='Tag commands') cmds = {*self.tag_group.walk_commands()} for cmd in copy.copy(cmds): # Remove any commands we cannot run. if not await cmd.can_run(ctx) or cmd.hidden: cmds.remove(cmd) # Generate strings. cmds = { '**' + ' '.join(cmd.qualified_name.split(' ')[1:]) + '** - ' + cmd.brief for cmd in cmds } for line in sorted(cmds): desc += f'{line}\n' desc += '\nThe following pages will list the available tags.' page.description = desc book += page async with ctx.typing(): await self._add_tag_list_to_pag(ctx, book) await book.send() return if tag_name.startswith('!'): tag_name = tag_name[1:] local_first = True else: local_first = False async with self.bot.postgres_pool.acquire() as conn: with ctx.channel.typing(): results = await conn.fetch( ''' SELECT content, file_name, b64data FROM nekozilla.tags LEFT OUTER JOIN nekozilla.tags_attach ON pk = tag_pk WHERE name = ($1) AND (guild = ($2) OR guild IS NULL) AND -- This will be TRUE OR FALSE -> TRUE if chan is NSFW -- and FALSE OR FALSE -> FALSE if chan is NOT NSFW. is_nsfw = (($3) OR FALSE) ORDER BY guild NULLS LAST; ''', tag_name, ctx.guild.id, ctx.channel.nsfw) if not results: raise neko.NekoCommandError('No tag found with that name.') else: first = results.pop(0 if local_first else -1) content = first['content'] attachment_name = first['file_name'] attachment_data = first['b64data'] if attachment_name is not None and attachment_data is not None: # Decode attachment data. attachment_data = base64.b64decode(attachment_data) # We allow a few dynamic bits and pieces. # TODO: document this. replacements = { ('${args}', ' '.join(str(arg) for arg in args)), ('${channel}', str(ctx.channel.name)), ('${channel_mention}', f'<#{ctx.channel.id}>'), ('${channel_id}', str(ctx.channel.id)), ('${author}', str(ctx.author.display_name)), ('${author_mention}', str(ctx.author.mention)), ('${author_discriminator}', str(ctx.author.discriminator)), ('${author_username}', str(ctx.author.name)), ('${author_id}', str(ctx.author.id)), ('${guild}', str(ctx.guild.name)), ('${guild_id}', str(ctx.guild.id)) } for replacement in replacements: content = content.replace(*replacement) if attachment_name is not None and attachment_data is not None: with io.BytesIO( initial_bytes=attachment_data) as att_bytes: att_bytes.seek(0) file = discord.File(att_bytes, filename=attachment_name) await ctx.send(content, file=file) else: await ctx.send(content)