async def say(self, ctx): ''' Makes the bot say something (Leader+). Arguments: channel_mention, message ''' addCommand() user = ctx.message.author roles = user.roles isLeader = False for r in roles: if r.id == config['leaderRole'] or user.id == config['owner']: isLeader = True break if not isLeader: await self.bot.say('Sorry, only leaders have permission to do this.') return msg = ctx.message if not msg.channel_mentions: serverChannel = msg.server.default_channel await self.bot.say(f'Please mention a channel for me to send the message to, such as: **-say {serverChannel.mention} test**') return channel = msg.channel_mentions[0] txt = msg.content txt = txt.replace("-say", "", 1) txt = txt.replace(channel.mention, "", 1) txt = txt.strip() if not txt: await self.bot.say(f'Please add text to your command, such as: **-say {channel.mention} test**') return try: await self.bot.send_message(channel, txt) await self.bot.delete_message(msg) except discord.Forbidden: await self.bot.say(f'Sorry, I do not have permission to send a message to {channel.mention}.') return
async def purge(self, ctx, num=0): ''' Deletes given amount of messages (Admin+). Arguments: integer. Constraints: You can delete up to 100 messages at a time. ''' addCommand() msg = ctx.message user = msg.author roles = user.roles channel = msg.channel isAdmin = False for r in roles: if r.id == config['adminRole'] or user.id == config['owner']: isAdmin = True break if not isAdmin: await self.bot.say('Sorry, this command can only be used by Admins and above.') return if not num or num <1 or num > 100: await self.bot.say(f'Sorry, I cannot delete **{num}** messages, please enter a number between 1 and 100.') return await self.bot.delete_message(msg) await self.bot.purge_from(channel, limit=num) return
async def settimezone(self, ctx, *tz_or_loc): ''' Set your personal timezone. This timezone will be shown (among others) when you use the `worldtime` command. ''' addCommand() input = ' '.join(tz_or_loc).upper() if not input: timezone = None else: timezone = string_to_timezone(input) if timezone: tz = pytz.timezone(timezone) time = datetime.now(tz) time_str = time.strftime('%H:%M') user = await User.get(ctx.author.id) if user: await user.update(timezone=timezone).apply() else: await User.create(id=ctx.author.id, timezone=timezone) if timezone: await ctx.send( f'{ctx.author.mention} your timezone has been set to `{timezone}` ({time_str}).' ) else: await ctx.send( f'{ctx.author.mention} your timezone has been removed.')
async def purge(self, ctx, num=0): ''' Deletes given amount of messages (Admin+). Arguments: integer. Constraints: You can delete up to 100 messages at a time. ''' addCommand() if not isinstance(num, int): if is_int(num): num = int(num) else: raise commands.CommandError( message=f'Invalid argument: `{num}`.') if not num or num < 1 or num > 100: raise commands.CommandError(message=f'Invalid argument: `{num}`.') try: try: await ctx.message.delete() except: pass await ctx.channel.purge(limit=num) msg = await ctx.send(f'{num} messages deleted!') await asyncio.sleep(3) await msg.delete() except discord.Forbidden: raise commands.CommandError( message=f'Missing permissions: `delete_message`.')
async def modmail(self, ctx, public='', private=''): ''' Set up a public and private modmail channel. (Admin+) Any messages sent in the public channel will be instantly deleted and then copied to the private channel. To disable modmail, use this command without any arguments. Arguments: public: channel mention private: channel mention ''' addCommand() if not public and not private: guild = await Guild.get(ctx.guild.id) await guild.update(modmail_public=None, modmail_private=None).apply() await ctx.send( f'Modmail has been disabled for server **{ctx.guild.name}**.') return if len(ctx.message.channel_mentions) < 2: raise commands.CommandError( message= f'Required arguments missing: 2 channel mentions required.') public, private = ctx.message.channel_mentions[ 0], ctx.message.channel_mentions[1] guild = await Guild.get(ctx.guild.id) await guild.update(modmail_public=public.id, modmail_private=private.id).apply() await ctx.send( f'Modmail public and private channels for server **{ctx.guild.name}** have been set to {public.mention} and {private.mention}.' )
async def discord(self, ctx): ''' Gives the link for the Portables discord server. ''' addCommand() await self.bot.say( f'**Portables Discord:**\nhttps://discord.gg/QhBCYYr')
async def vos(self, ctx): ''' Returns the current Voice of Seren. ''' addCommand() now = datetime.utcnow() now = now.replace(second=0, microsecond=0) time_to_vos = self.bot.next_vos - now time_to_vos = timeDiffToString(time_to_vos) current = self.bot.vos['vos'] next_vos = self.bot.vos['next'] emoji0 = config[current[0].lower() + 'Emoji'] emoji1 = config[current[1].lower() + 'Emoji'] current_txt = f'{emoji0} {current[0]}\n{emoji1} {current[1]}' next_txt = f'{next_vos[0]}, {next_vos[1]}, {next_vos[2]}, {next_vos[3]}' title = f'Voice of Seren' colour = 0x00b2ff embed = discord.Embed(title=title, colour=colour, description=current_txt) embed.add_field(name=f'Up next ({time_to_vos})', value=next_txt, inline=False) await ctx.send(embed=embed)
async def roll(self, ctx, sides=6, num=1): ''' Rolls a dice. ''' addCommand() if is_int(num): num = int(num) else: raise commands.CommandError(message=f'Invalid argument: `{num}`.') if num < 1 or num > 100: raise commands.CommandError(message=f'Invalid argument: `{num}`.') if is_int(sides): sides = int(sides) else: raise commands.CommandError( message=f'Invalid argument: `{sides}`.') if sides < 2 or sides > 2147483647: raise commands.CommandError( message=f'Invalid argument: `{sides}`.') results = [] for _ in range(0, num): results.append(random.randint(1, sides)) result = str(results).replace('[', '').replace(']', '') await ctx.send(f'{ctx.author.mention} You rolled {result}!')
async def removenotification(self, ctx, id): ''' Removes a custom notification by ID. (Admin+) To get the ID of the notification that you want to remove, use the command "notifications". ''' addCommand() if not id: raise commands.CommandError( message=f'Required argument missing: `id`.') if not is_int(id): raise commands.CommandError( message=f'Invalid argument: `{id}`. Must be an integer.') else: id = int(id) notification = await Notification.query.where( Notification.guild_id == ctx.guild.id ).where(Notification.notification_id == id).gino.first() if not notification: raise commands.CommandError( message=f'Could not find custom notification: `{id}`.') await notification.delete() notifications = await Notification.query.where( Notification.guild_id == ctx.guild.id ).order_by(Notification.notification_id.asc()).gino.all() if notifications: for i, notification in enumerate(notifications): await notification.update(notification_id=i).apply() await ctx.send(f'Removed custom notification: `{id}`')
async def notifications(self, ctx): ''' Returns list of custom notifications for this server. ''' addCommand() notifications = await Notification.query.where( Notification.guild_id == ctx.guild.id ).order_by(Notification.notification_id.asc()).gino.all() if not notifications: raise commands.CommandError( message= f'Error: this server does not have any custom notifications.') msg = '' for notification in notifications: msg += f'id: {notification.notification_id}\nchannel: {notification.channel_id}\ntime: {notification.time} UTC\ninterval: {notification.interval} (seconds)\nmessage: {notification.message}\n\n' msg = msg.strip() if len(msg) <= 1994: await ctx.send(f'```{msg}```') else: # https://stackoverflow.com/questions/13673060/split-string-into-strings-by-length chunks, chunk_size = len( msg), 1994 # msg at most 2000 chars, and we have 6 ` chars msgs = [ msg[i:i + chunk_size] for i in range(0, chunks, chunk_size) ] for msg in msgs: await ctx.send(f'```{msg}```')
async def abbr(self, ctx): ''' Explains all abbreviations. ''' addCommand() msg = (f'**Abbreviations:**\n\n' f'Portables:\n' f'• R = Ranges\n' f'• M = Mills (Sawmills)\n' f'• W = Wells\n' f'• FO = Forges\n' f'• C = Crafters\n' f'• B = Braziers\n' f'• FL = Fletchers\n\n' f'Locations:\n' f'• CA = Combat Academy\n' f'• BE = Beach\n' f'• BA = Barbarian Assault\n' f'• SP = Shantay Pass\n' f'• BU = Burthorpe\n' f'• CW = Castle Wars\n' f'• Prif = Prifddinas\n' f'• MG = Max Guild\n' f'• VIP = Menaphos VIP skilling area') await self.bot.say(msg)
async def rps(self, ctx, choice=''): ''' Play rock, paper, scissors. ''' addCommand() if not choice.upper() in rpsUpper: raise commands.CommandError( message=f'Invalid argument: `{choice}`.') for x in rps: if choice.upper() == x.upper(): choice = x i = random.randint(0, 2) myChoice = rps[i] result = f'You chose **{choice}**. I choose **{myChoice}**.\n' choices = [myChoice, choice] if choice == myChoice: result += '**Draw!**' elif 'Rock' in choices and 'Paper' in choices: result += '**Paper** wins!' elif 'Rock' in choices and 'Scissors' in choices: result += '**Rock** wins!' elif 'Paper' in choices and 'Scissors' in choices: result += '**Scissors** win!' await ctx.send(result)
async def members(self, ctx, *roleName): ''' List members in a role. ''' addCommand() if not roleName: raise commands.CommandError( message=f'Required argument missing: `role`.') roleName = ' '.join(roleName) role = discord.utils.find(lambda r: r.name.upper() == roleName.upper(), ctx.guild.roles) if not role: role = discord.utils.find( lambda r: roleName.upper() in r.name.upper(), ctx.guild.roles) if not role: raise commands.CommandError( message=f'Could not find role: `{roleName}`.') txt = '' for m in role.members: if len(txt + m.mention) + 5 > 2048: txt += '\n...' break else: txt += m.mention + '\n' txt = txt.strip() embed = discord.Embed( title=f'Members in {roleName} ({len(role.members)})', colour=0x00b2ff, description=txt) await ctx.send(embed=embed)
async def shorten(self, ctx, url=''): ''' Shorten a URL. ''' addCommand() await ctx.channel.trigger_typing() if not url: raise commands.CommandError( message='Required argument missing: `url`.') if not validators.url(url): raise commands.CommandError( message= f'Invalid argument: `{url}`. Argument must be a valid URL.') r = await self.bot.aiohttp.get( f'https://is.gd/create.php?format=simple&url={url}') async with r: if r.status != 200: raise commands.CommandError( message= f'Error retrieving shortened URL, please try again in a minute.' ) data = await r.text() await ctx.send(data)
async def twitter(self, ctx): ''' Gives the link to the Portables twitter. ''' addCommand() await self.bot.say( f'**Portables Twitter:**\nhttps://www.twitter.com/PortablesRS')
async def roles(self, ctx): ''' Get a list of roles and member counts. ''' addCommand() msg = '' chars = max([len(role.name) for role in ctx.guild.roles]) + 1 counts = [len(role.members) for role in ctx.guild.roles] countChars = max([len(str(i)) for i in counts]) + 1 for i, role in enumerate(ctx.guild.roles): count = counts[i] msg += role.name + (chars - len(role.name)) * ' ' + str( count) + (countChars - len(str(count))) * ' ' + 'members\n' msg = msg.strip() if len(msg) <= 1994: await ctx.send(f'```{msg}```') else: chunks, chunk_size = len( msg), 1994 # msg at most 2000 chars, and we have 6 ` chars msgs = [ msg[i:i + chunk_size] for i in range(0, chunks, chunk_size) ] for msg in msgs: await ctx.send(f'```{msg}```')
async def status(self, ctx): ''' Returns the bot's current status. ''' now = datetime.utcnow() await self.bot.send_typing(ctx.message.channel) addCommand() time = now.replace(microsecond=0) time -= self.start_time time = timeDiffToString(time) cpuPercent = str(psutil.cpu_percent(interval=None)) cpuFreq = psutil.cpu_freq()[0] ram = psutil.virtual_memory() # total, available, percent, used, free, active, inactive, buffers, cached, shared, slab ramPercent = ram[2] ramTotal = ram[0] ramUsed = ram[3] title = f'**Status**' colour = 0x00e400 timestamp = datetime.utcnow() txt = f'**OK**. :white_check_mark: \n' embed = discord.Embed(title=title, colour=colour, timestamp=timestamp, description=txt) embed.add_field(name='CPU', value=f'{cpuPercent}% {int(cpuFreq)} MHz', inline=False) embed.add_field(name='RAM', value=f'{ramPercent}% {int(ramUsed/1000000)}/{int(ramTotal/1000000)} MB', inline=False) embed.add_field(name='Ping', value=f'Calculating...', inline=False) embed.add_field(name='Running time', value=f'{time}', inline=False) embed.add_field(name='Commands', value=f'{getCommandsAnswered()} commands answered', inline=False) embed.add_field(name='Events', value=f'{getEventsLogged()} events logged', inline=False) before = datetime.utcnow() msg = await self.bot.send_message(ctx.message.channel, embed=embed) ping = pingToString(datetime.utcnow() - before) embed.set_field_at(2, name='Ping', value=ping, inline=False) await self.bot.edit_message(msg, embed=embed) return
async def forums(self, ctx): ''' Gives the link to the Portables forum thread. ''' addCommand() await self.bot.say( f'**Portables forum thread:**\nhttp://services.runescape.com/m=forum/forums.ws?75,76,789,65988634' )
async def git(self, ctx): ''' Returns the link to the GitHub repository of this bot. ''' addCommand() await self.bot.delete_message(ctx.message) await self.bot.say( '**Portables bot GitHub:**\nhttps://github.com/ChattyRS/Portables')
async def sheets(self, ctx): ''' Gives the link to the public Portables sheet. ''' addCommand() await self.bot.say( f'**Portables sheets:**\nhttps://docs.google.com/spreadsheets/d/16Yp-eLHQtgY05q6WBYA2MDyvQPmZ4Yr3RHYiBCBj2Hc/pub' )
async def ping(self, ctx): ''' Pings the bot to check latency. ''' addCommand() before = datetime.utcnow() msg = await self.bot.say(f'Pong!') ping = pingToString(datetime.utcnow() - before) await self.bot.edit_message(msg, f'Pong! `{ping}`')
async def poll(self, ctx, hours='24', *options): ''' Create a poll in which users can vote by reacting. Poll duration can vary from 1 hour to 1 week (168 hours). Options must be separated by commas. ''' addCommand() if not is_int(hours): options = [hours] + list(options) hours = 24 else: hours = int(hours) if hours < 1 or hours > 168: raise commands.CommandError( message= f'Invalid argument: `{hours}`. Must be positive and less than 168.' ) options = ' '.join(options) options = options.split(',') if len(options) < 2: raise commands.CommandError( message= 'Error: insufficient options to create a poll. At least two options are required.' ) elif len(options) > 20: raise commands.CommandError( message= 'Error: too many options. This command only supports up to 20 options.' ) txt = '' i = 0 for opt in options: txt += f'\n{num_emoji[i]} {opt}' i += 1 txt += f'\n\nThis poll will be open for {hours} hours!' embed = discord.Embed( title='**Poll**', description=f'Created by {ctx.message.author.mention}\n{txt}', timestamp=datetime.utcnow()) msg = await ctx.send(embed=embed) embed.set_footer(text=f'ID: {msg.id}') await msg.edit(embed=embed) for num in range(i): await msg.add_reaction(num_emoji[num]) await Poll.create(guild_id=ctx.guild.id, author_id=ctx.author.id, channel_id=ctx.channel.id, message_id=msg.id, end_time=datetime.utcnow() + timedelta(hours=hours))
async def rsnotify(self, ctx, channel=''): ''' Changes server's RS notification channel. (Admin+) Arguments: channel. If no channel is given, notifications will no longer be sent. ''' addCommand() await ctx.channel.trigger_typing() if ctx.message.channel_mentions: channel = ctx.message.channel_mentions[0] elif channel: found = False for c in ctx.guild.text_channels: if channel.upper() in c.name.upper(): channel = c found = True break if not found: raise commands.CommandError( message=f'Could not find channel: `{channel}`.') else: guild = await Guild.get(ctx.guild.id) if guild.notification_channel_id: await guild.update(notification_channel_id=None).apply() await ctx.send( f'I will no longer send notifications in server **{ctx.guild.name}**.' ) return else: raise commands.CommandError( message=f'Required argument missing: `channel`.') permissions = discord.Permissions.none() colour = discord.Colour.default() roleNames = [] for role in ctx.guild.roles: roleNames.append(role.name.upper()) for rank in ranks: if not rank.upper() in roleNames: try: await ctx.guild.create_role(name=rank, permissions=permissions, colour=colour, hoist=False, mentionable=True) except discord.Forbidden: raise commands.CommandError( message=f'Missing permissions: `create_roles`.') guild = await Guild.get(ctx.guild.id) await guild.update(notification_channel_id=channel.id).apply() await ctx.send( f'The notification channel for server **{ctx.guild.name}** has been changed to {channel.mention}.' )
async def ports(self, ctx): ''' Explains how to get portable locations. ''' addCommand() await self.bot.delete_message(ctx.message) botChannel = config['botChannel'] await self.bot.say( f'For a list of portable locations, please use the `{prefix[0]}portables` command in the <#{botChannel}> channel.' )
async def math(self, ctx, *formulas): ''' Calculates the result of a given mathematical problem. Supported operations: Basic: +, -, *, / Modulus: % or mod Powers: ^ Square roots: sqrt() Logarithms: log(,[base]) (default base=e) Absolute value: abs() Rounding: round(), floor(), ceil() Trigonometry: sin(), cos(), tan() (in radians) Parentheses: () Constants: pi, e, phi, tau, etc... Complex/imaginary numbers: i Infinity: inf Sum: sum(start, end, f(x)) (start and end inclusive) Product: product(start, end, f(x)) (start and end inclusive) ''' addCommand() await ctx.channel.trigger_typing() formula = '' for f in formulas: formula += f + ' ' formula = formula.strip() if not formula: raise commands.CommandError( message=f'Required argument missing: `formula`.') try: input = format_input(formula.lower(), 0) manager = multiprocessing.Manager() val = manager.dict() p = multiprocessing.Process(target=calculate, args=(input, val)) p.start() p.join(5) if p.is_alive(): p.terminate() p.join() raise commands.CommandError('Execution timed out.') result = val['val'] output = format_output(result) formula = beautify_input(formula) embed = discord.Embed(title='Math', description=f'`{formula} = {output}`') embed.set_footer(text='Wrong? Please let me know! DM Chatty#0001') await ctx.send(embed=embed) except Exception as e: raise commands.CommandError( message=f'Invalid mathematical expression:\n```{e}```')
async def deleteall(self, ctx, channel=''): ''' Deletes all messages that will be sent in the given channel. (Admin+) Arguments: channel (mention, name, or id) ''' addCommand() if not channel: raise commands.CommandError( message=f'Required argument missing: `channel`.') elif ctx.message.channel_mentions: channel = ctx.message.channel_mentions[0] else: found = False if is_int(channel): for c in ctx.guild.text_channels: if c.id == int(channel): channel = c found = True break if not found: for c in ctx.guild.text_channels: if c.name.upper() == channel.upper(): channel = c found = True break if not found: for c in ctx.guild.text_channels: if channel.upper() in c.name.upper(): channel = c found = True break if not found: raise commands.CommandError( message=f'Could not find channel: `{channel}`.') guild = await Guild.get(ctx.guild.id) if guild.delete_channel_ids: if channel.id in guild.delete_channel_ids: await guild.update(delete_channel_ids=guild.delete_channel_ids. remove(channel.id)).apply() await ctx.send( f'Messages in {channel.mention} will no longer be deleted.' ) else: await guild.update( delete_channel_ids=guild.delete_channel_ids + [channel.id] ).apply() await ctx.send( f'All future messages in {channel.mention} will be deleted.' ) else: await guild.update(delete_channel_ids=[channel.id]).apply() await ctx.send( f'All future messages in {channel.mention} will be deleted.')
async def hall_of_fame(self, ctx, channel='', react_num=10): ''' Sets the hall of fame channel and number of reactions for this server. (Admin+) Arguments: channel (mention, name, or id), react_num (int) After [react_num] reactions with the :star2: emoji, messages will be shown in the hall of fame channel. ''' addCommand() guild = await Guild.get(ctx.guild.id) if not channel: await guild.update(hall_of_fame_channel_id=None).apply() await ctx.send(f'Disabled hall of fame for this server.') return elif ctx.message.channel_mentions: channel = ctx.message.channel_mentions[0] else: found = False if is_int(channel): for c in ctx.guild.text_channels: if c.id == int(channel): channel = c found = True break if not found: for c in ctx.guild.text_channels: if c.name.upper() == channel.upper(): channel = c found = True break if not found: for c in ctx.guild.text_channels: if channel.upper() in c.name.upper(): channel = c found = True break if not found: raise commands.CommandError( message=f'Could not find channel: `{channel}`.') if react_num < 1: raise commands.CommandError( message= f'Invalid argument: `react_num` (`{react_num}`). Must be at least 1.' ) await guild.update(hall_of_fame_channel_id=channel.id, hall_of_fame_react_num=react_num).apply() await ctx.send( f'Set the hall of fame channel for this server to {channel.mention}. The number of :star2: reactions required has been set to `{react_num}`.' )
async def sinkhole(self, ctx): ''' Returns the time until the next sinkhole. ''' addCommand() now = datetime.utcnow() now = now.replace(microsecond=0) msg = config[ 'sinkholeEmoji'] + " **Sinkhole** will spawn in " + timeDiffToString( self.bot.next_sinkhole - now) + "." await ctx.send(msg)
async def goebies(self, ctx): ''' Returns the time until the next goebies supply run. ''' addCommand() now = datetime.utcnow() now = now.replace(microsecond=0) msg = config[ 'goebiesEmoji'] + " **Goebies supply run** will begin in " + timeDiffToString( self.bot.next_goebies - now) + "." await ctx.send(msg)
async def flipcoin(self, ctx): ''' Flips a coin. ''' addCommand() i = random.randint(0, 1) result = '' if i: result = 'heads' else: result = 'tails' await ctx.send(f'{ctx.author.mention} {result}!')