async def transfer(self, ctx: AceContext, tag_name: TagEditConverter(), *, new_owner: disnake.Member): '''Transfer ownership of a tag to another member.''' tag_name, record = tag_name if new_owner.bot: raise commands.CommandError('Can\'t transfer tag to bot.') if record.get('user_id') == new_owner.id: raise commands.CommandError('User already owns tag.') prompt = ctx.prompt( title='Tag transfer request', prompt= f'{ctx.author.mention} wants to transfer ownership of the tag \'{tag_name}\' to you.\n\nDo you accept?', user_override=new_owner) if not await prompt: raise commands.CommandError('Tag transfer aborted.') res = await self.db.execute('UPDATE tag SET user_id=$1 WHERE id=$2', new_owner.id, record.get('id')) if res == 'UPDATE 1': await ctx.send('Tag \'{}\' transferred to \'{}\''.format( record.get('name'), new_owner.display_name)) else: raise commands.CommandError('Unknown error occured.')
async def get_xkcd_json(self, url): async with self.bot.aiohttp.get(url) as resp: if resp.status == 404: raise commands.CommandError('Comic does not exist.') if resp.status != 200: raise commands.CommandError('Request failed.') return await resp.json()
async def _on_unstar(self, board, starrer, star_channel, message, star_message, record): if record: result = await self.db.execute( 'DELETE FROM starrers WHERE star_id=$1 AND user_id=$2', record.get('id'), starrer.id) # if nothing was deleted, the star message doesn't need to be updated if result == 'DELETE 0': raise commands.CommandError( 'You have not previously starred this, or you are the original starrer.' ) # otherwise we need to update the star count starrer_count = await self.db.fetchval( 'SELECT COUNT(*) FROM starrers WHERE star_id=$1', record.get('id')) if star_message is not None: await self.update_star(record.get('message_id'), star_message, starrer_count + 1) else: raise commands.CommandError( 'This message has not previously been starred.')
async def test(self, ctx): '''Test your welcome command.''' entry = await self.config.get_entry(ctx.guild.id, construct=False) if entry is None: raise WELCOME_NOT_SET_UP_ERROR channel = entry.channel if channel is None: if entry.channel_id is None: raise commands.CommandError( 'You haven\'t set up a welcome channel yet.\nSet up with `welcome channel [channel]`' ) else: raise commands.CommandError( 'Welcome channel previously set but not found.\nPlease set again using `welcome channel [channel]`' ) if entry.enabled is False: raise commands.CommandError( 'Welcome messages are disabled.\nEnable with `welcome enable`') if entry.content is None: raise commands.CommandError( 'No welcome message set.\nSet with `welcome message <message>`' ) await self.on_member_join(ctx.author)
async def convert(self, ctx, mult): try: mult = float(mult) except ValueError: raise commands.CommandError('Argument has to be float.') if mult < 1.0: raise commands.CommandError('Unit must be more than 1.') return mult
async def create_tag(self, ctx, tag_name, content): try: await self.db.execute( 'INSERT INTO tag (name, guild_id, user_id, created_at, content) VALUES ($1, $2, $3, $4, $5)', tag_name, ctx.guild.id, ctx.author.id, datetime.utcnow(), content) except asyncpg.UniqueViolationError: raise commands.CommandError('Tag already exists.') except Exception: raise commands.CommandError( 'Failed to create tag for unknown reasons.')
async def _on_star_event_meta(self, event, board, message, starrer): # get the starmessage record if it exists row = await self.db.fetchrow( 'SELECT * FROM star_msg WHERE guild_id=$1 AND (message_id=$2 OR star_message_id=$2)', message.guild.id, message.id) if message.channel.id == board.channel_id: # if it's from the starboard itself, it *has* to be a message from ourself if message.author != self.bot.user: raise commands.CommandError( 'Can\'t star messages from the starboard.') # if the reaction event happened in the starboard channel we already have a reference # to both the channel and starred message star_channel = message.channel star_message = message message = None else: # get the star channel. this raises commanderror on failure star_channel = await self._get_star_channel(message.guild, board=board) star_message_id = None if row is None else row.get( 'star_message_id') # we should also find the starred message if star_message_id is None: # if row is none, this is a new star - and no starred message exists star_message = None else: # if we have the record we catch fetch the starred message try: star_message = await star_channel.fetch_message( star_message_id) except disnake.HTTPException: raise SB_STAR_MSG_NOT_FOUND_ERROR # stop if attempted star is too old # if star_message was not found (star is new) then use the original messages timestamp # if a star_message *was* found, use that instead as it's the new basis of "star message age" if disnake.utils.utcnow() - STAR_CUTOFF > (star_message or message).created_at: raise commands.CommandError( 'Stars can\'t be added or removed from messages older than a week.' ) # trigger event # message and star_message can be populated, or *one* of them can be None await event(board, starrer, star_channel, message, star_message, row)
async def weather(self, ctx, *, location: str): '''Check the weather at a location.''' if APIXU_KEY is None: raise commands.CommandError('The host has not set up an API key.') url = 'http://api.weatherstack.com/current' params = { 'access_key': APIXU_KEY, 'query': location } async with ctx.channel.typing(): try: async with ctx.http.get(url, params=params) as resp: if resp.status != 200: raise QUERY_ERROR data = await resp.json() except asyncio.TimeoutError: raise QUERY_ERROR if data.get('success', True) is False: raise commands.CommandError('Unable to find a location match.') location = data['location'] current = data['current'] observation_time = datetime.strptime(current['observation_time'], '%I:%M %p').time() e = disnake.Embed( title='Weather for {}, {} {}'.format(location['name'], location['region'], location['country'].upper()), description='*{}*'.format(' / '.join(current["weather_descriptions"])), timestamp=datetime.combine(date.today(), observation_time), ) e.set_footer(text='Observed') if current['weather_icons']: e.set_thumbnail(url=current['weather_icons'][0]) e.add_field(name='Temperature', value='{}°C'.format(current['temperature'])) e.add_field(name='Feels Like', value='{}°C'.format(current['feelslike'])) e.add_field(name='Precipitation', value='{} mm'.format(current['precip'])) e.add_field(name='Humidity', value='{}%'.format(current['humidity'])) e.add_field(name='Wind Speed', value='{} kph'.format(current['wind_speed'])) e.add_field(name='Wind Direction', value=current['wind_dir']) await ctx.send(embed=e)
async def version(self, ctx): '''Get changelog and download for the latest AutoHotkey_L version.''' url = 'https://api.github.com/repos/Lexikos/AutoHotkey_L/releases' async with ctx.http.get(url) as resp: if resp.status != 200: raise commands.CommandError('Query failed.') js = await resp.json() latest = js[0] asset = latest['assets'][0] content = self.h2m_version.convert(latest['body']) e = disnake.Embed(description=content, color=disnake.Color.green()) e.set_author(name='AutoHotkey_L ' + latest['name'], icon_url=latest['author']['avatar_url']) e.add_field(name='Release page', value=f"[Click here]({latest['html_url']})") e.add_field(name='Installer download', value=f"[Click here]({asset['browser_download_url']})") e.add_field(name='Downloads', value=asset['download_count']) await ctx.send(embed=e)
async def cmd_docs(self, ctx: commands.Context, *, query: str = None): '''Search the AutoHotkey documentation. Enter multiple queries by separating with commas.''' if query is None: await ctx.send(DOCS_FORMAT.format('')) return spl = dict.fromkeys(sq.strip() for sq in query.lower().split(',')) if len(spl) > 3: raise commands.CommandError('Maximum three different queries.') embeds = [] for subquery in spl.keys(): name = self.search_docs(subquery, k=1)[0] result = await self.get_doc(self._docs_id[name], entry=True, syntax=True) if not result: if len(spl.keys()) == 1: raise DOCS_NO_MATCH else: continue embeds.append(self.craft_docs_page(result, force_name=name)) await ctx.send(embeds=embeds)
async def ask(self, ctx): '''Responds with the currently open for claiming channels.''' controller: Controller = self.controllers.get(ctx.message.guild.id, None) if not controller: return channels = controller.open_channels(forecast=False) if not channels: raise commands.CommandError('No help channels are currently open for claiming. Please wait for a channel to become available.') mentions = [c.mention for c in channels] mention_count = len(mentions) if mention_count < 3: ment = ' and '.join(mentions) else: mentions[-1] = 'and ' + mentions[-1] ment = ', '.join(mentions) text = ( 'If you\'re looking for scripting help you should ask in an open help channel.\n\n' 'The currently available help channels are {0}.' ).format(ment) await ctx.send(text)
async def choose(self, ctx, *choices: commands.clean_content): '''Pick a random item from a list separated by spaces.''' choose_prompts = ( 'I have chosen', 'I have a great feeling about', 'I\'ve decided on', 'Easy choice', 'I think', ) if len(choices) < 2: raise commands.CommandError('At least two choices are necessary.') selected = choice(choices) e = disnake.Embed( description=selected ) e.set_author(name=choice(choose_prompts), icon_url=self.bot.user.display_avatar.url) msg = await ctx.send(':thinking:') await asyncio.sleep(3) await msg.edit(content=None, embed=e)
async def convert(self, ctx, argument): argument = await super().convert(ctx, argument) if argument in (role.emoji for role in ctx.head.selector.roles): raise commands.CommandError('This emoji already exists in this selector.') return argument
async def bill(self, ctx, *, query: str = None): '''Get a random Bill Wurtz video from his website, with optional search.''' async with ctx.typing(): if query is None: picked = choice(list(self.bill_cache.keys())) elif query.startswith('latest'): picked = self.bill_latest else: picked, score, junk = process.extractOne(query, self.bill_cache.keys()) if score < 50: raise commands.CommandError( "Couldn't match that search with certainty.\n" f"Closest match: '{picked.strip()}'" ) href, bill_date = self.bill_cache[picked] await ctx.send( f"{bill_date}: " f"*{disnake.utils.escape_markdown(picked)}* \n" f"{BILL_WURTZ_URL + href}" )
async def nato(self, ctx, count: int = 3): '''Learn the NATO phonetic alphabet.''' if count < 1: raise commands.CommandError('Please pick a length larger than 0.') if count > 16: raise commands.CommandError( 'Sorry, please pick lengths lower or equal to 16.') lets = sample(LETTERS, k=count) await ctx.send(f'**{"".join(lets).upper()}**?') def check(m): return m.channel == ctx.channel and m.author == ctx.author try: msg = await self.bot.wait_for('message', check=check, timeout=60.0) except asyncio.TimeoutError: await ctx.send(f'Sorry {ctx.author.mention}, time ran out!') return answer = msg.content.lower().split() async def failed(): right = [] for let in lets: asd = NATO[let] right.append(asd[0] if isinstance(asd, tuple) else asd) await ctx.send( f'Sorry, that was wrong! The correct answer was `{" ".join(right).upper()}`' ) if len(answer) != len(lets): return await failed() for index, part in enumerate(answer): answer = NATO[lets[index]] if isinstance(answer, tuple): if part not in answer: return await failed() else: if part != answer: return await failed() await ctx.send('Correct! ✅')
async def get_xkcd_comic(self, ctx, id): url = f'https://xkcd.com/{id}/info.0.json' async with ctx.http.get(url) as resp: if resp.status != 200: raise commands.CommandError('Request failed.') comic_json = await resp.json() e = self.make_xkcd_embed(comic_json) return e
async def convert(self, ctx, argument): lowered = argument.lower() if lowered in ('yes', 'y', 'true', 't', '1', 'enable', 'on'): return True elif lowered in ('no', 'n', 'false', 'f', '0', 'disable', 'off'): return False else: raise commands.CommandError('Input could not be interpreted as boolean.')
async def _multiprompt(self, ctx, msg, preds): outs = list() def pred(message): return message.author.id == ctx.author.id and ctx.channel.id == ctx.channel.id def new_embed(question): e = disnake.Embed(description=question) e.set_footer(text=EDIT_FOOTER) return e for question, conv in preds: try: await msg.edit(embed=new_embed(question)) except disnake.HTTPException: raise commands.CommandError('Could not replace the message embed. Did the message get deleted?') while True: try: message = await self.bot.wait_for('message', check=pred, timeout=60.0) await message.delete() except asyncio.TimeoutError: return None if message.content.lower() == 'exit': return None try: value = await conv.convert(ctx, message.content) except commands.CommandError as exc: if not msg.embeds: try: await msg.delete() except disnake.HTTPException: pass raise commands.CommandError('Embed seems to have been removed, aborting.') e = msg.embeds[0] e.set_footer(text='NOTE: ' + str(exc) + ' ' + RETRY_MSG) await msg.edit(embed=e) continue outs.append(value) break return outs
async def about(self, ctx, *, command: str = None): '''Show info about the bot or a command.''' if command is None: await self._about_bot(ctx) else: cmd = self.bot.get_command(command) if cmd is None or cmd.hidden: raise commands.CommandError('No command with that name found.') await self._about_command(ctx, cmd)
async def clear(self, ctx, message_count: int, user: MaybeMemberConverter = None): '''Simple purge command. Clear messages, either from user or indiscriminately.''' if message_count < 1: raise commands.CommandError( 'Please choose a positive message amount to clear.') if message_count > 100: raise commands.CommandError( 'Please choose a message count below 100.') def all_check(msg): if msg.id == RULES_MSG_ID: return False return True def user_check(msg): return msg.author.id == user.id and all_check(msg) try: await ctx.message.delete() except disnake.HTTPException: pass try: deleted = await ctx.channel.purge( limit=message_count, check=all_check if user is None else user_check) except disnake.HTTPException: raise commands.CommandError( 'Failed deleting messages. Does the bot have the necessary permissions?' ) count = len(deleted) log.info('%s cleared %s messages in %s', po(ctx.author), count, po(ctx.guild)) await ctx.send(f'Deleted {count} message{"s" if count > 1 else ""}.', delete_after=5)
async def disable(self, ctx): '''Disable welcome messages.''' entry = await self.config.get_entry(ctx.guild.id) if entry.enabled is False: raise commands.CommandError('Welcome messages already disabled.') await entry.update(enabled=False) await ctx.send('Welcome messages disabled.')
async def channel(self, ctx, *, channel: disnake.TextChannel = None): '''Set or view welcome message channel.''' entry = await self.config.get_entry(ctx.guild.id) if channel is None: if entry.channel_id is None: raise commands.CommandError('Welcome channel not yet set.') channel = entry.channel if channel is None: raise commands.CommandError( 'Channel previously set but not found, try setting a new one.' ) else: await entry.update(channel_id=channel.id) await ctx.send(f'Welcome channel set to {channel.mention}')
async def convert(self, ctx, code): if code.startswith('https://p.ahkscript.org/'): url = code.replace('?p=', '?r=') async with ctx.http.get(url) as resp: if resp.status == 200 and str(resp.url) == url: code = await resp.text() else: raise commands.CommandError( 'Failed fetching code from pastebin.') return code
async def convert(self, ctx, argument): try: role = await super().convert(ctx, argument) except commands.CommandError as exc: raise commands.CommandError(str(exc)) if role == ctx.guild.default_role: raise commands.CommandError('The *everyone* role is not allowed.') if role.id in (other_role.role_id for selector in ctx.head.selectors for other_role in selector.roles): raise commands.CommandError('This role already exists somewhere else.') if ctx.author != ctx.guild.owner and role >= ctx.author.top_role: raise commands.CommandError('Sorry, you can\'t add roles higher than your top role.') config = await ctx.bot.config.get_entry(ctx.guild.id) if role == config.mod_role: raise commands.CommandError('Can\'t add moderation role to selector.') return role.id
async def reminders(self, ctx): '''List your reminders in this guild.''' res = await self.db.fetch( 'SELECT * FROM remind WHERE guild_id=$1 AND user_id=$2 ORDER BY id DESC', ctx.guild.id, ctx.author.id) if not len(res): raise commands.CommandError('Couldn\'t find any reminders.') await RemindPager(ctx, res, per_page=3).go()
async def random(self, ctx): '''Show a random starred message.''' entry = await self.db.fetchrow( 'SELECT * FROM star_msg WHERE star_message_id IS NOT NULL ORDER BY random() LIMIT 1' ) if entry is None: raise commands.CommandError('No starred messages to pick from.') await ctx.invoke(self.show, message=entry)
async def threshold(self, ctx, *, threshold: int = None): '''Starred messages with fewer than `threshold` stars will be removed from the starboard after a week has passed. To disable auto-cleaning completely, leave argument blank.''' board = await self.get_board(ctx.guild.id) if threshold is None: if board.threshold is None: raise commands.CommandError( 'Auto-cleaning is already disabled.') await board.update(threshold=None) await ctx.send('Starboard auto-cleaning disabled.') else: if not 0 < threshold < 32767: raise commands.CommandError( 'Auto-clean star threshold has to be between 0 and 32767.') await board.update(threshold=threshold) await ctx.send( 'Starred messages with fewer than {} stars after a week will now be removed from the starboard.' .format(threshold))
async def post_star(self, star_channel, message, starrer_count): try: star_message = await star_channel.send( self.get_header(message.id, starrer_count), embed=self.get_embed(message, starrer_count)) await star_message.add_reaction(STAR_EMOJI) except disnake.HTTPException: raise commands.CommandError( 'Failed posting to starboard.\nMake sure the bot has permissions to post there.' ) return star_message
async def message(self, ctx, *, message: str): '''Set a new welcome message.''' if len(message) > 1024: raise commands.CommandError( 'Welcome message has to be shorter than 1024 characters.') # make sure an entry for this exists... entry = await self.config.get_entry(ctx.guild.id) await entry.update(content=message) await ctx.send('Welcome message updated. Do `welcome test` to test.')
async def delete(self, ctx, *, tag_name: TagEditConverter(allow_mod=True)): '''Delete a tag.''' if not await ctx.prompt( title='Are you sure?', prompt='This will delete the tag permanently.'): raise commands.CommandError('Tag deletion aborted.') tag_name, record = tag_name await self.db.execute('DELETE FROM tag WHERE id=$1', record.get('id')) await ctx.send(f"Tag \'{record.get('name')}\' deleted.")