class Regex: def __init__(self, test=False): self.replacements = Config('configs/replace.json', save=(not test)) self.permissions = Config('configs/perms.json', save=(not test)) if 'rep-blacklist' not in self.permissions: self.permissions['rep-blacklist'] = [] def add(self, regex, author_id=''): #Find requested replacement rep = get_match(regex) #ensure that replace was found before proceeding if not rep: return formatter.error('Could not find valid regex') p1 = formatter.escape_mentions(rep.group(2)) p2 = formatter.escape_mentions(rep.group(4)) #check regex for validity if not comp(p1, p2): return formatter.error('regex is invalid') #make sure that there are no similar regexes in db for i in self.replacements: if similar(p1, i): r = '\"{}\" -> \"{}\"'.format(i, self.replacements[i][0]) message = 'Similar regex already exists, delete or edit it\n{}'.format( formatter.inline(r)) return formatter.error(message) #make sure regex is not too broad if bad_re(p1): return formatter.error('regex is too broad') #check that regex does not already exist if p1 in self.replacements: return formatter.error('regex already exists') self.replacements[p1] = [p2, author_id] return formatter.ok() def edit(self, regex, author_id=''): #Find requested replacement rep = get_match(regex) #ensure that replace was found before proceeding if not rep: return formatter.error('Could not find valid regex') p1 = formatter.escape_mentions(rep.group(2)) p2 = formatter.escape_mentions(rep.group(4)) #check regex for validity if not comp(p1, p2): return formatter.error('regex is invalid') #make sure regex is not too broad if bad_re(p1): return formatter.error('regex is too broad') #ensure that replace was found before proceeding if p1 not in self.replacements: return formatter.error('Regex not in replacements.') #check if they have correct permissions if author_id != self.replacements[p1][1] \ and not perms.is_owner_check(author_id): #will uncomment next line when reps are a per server thing #and not perms.check_permissions(ctx.message, manage_messages=True): raise commands.errors.CheckFailure('Cannot edit') self.replacements[p1] = [p2, author_id] return formatter.ok() def rm(self, pattern, author_id=''): #pattern = re.sub('^(`)?\\(\\?[^\\)]*\\)', '\\1', pattern) pattern = formatter.escape_mentions(pattern) #ensure that replace was found before proceeding if re.search('^`.*`$', pattern) and pattern[1:-1] in self.replacements: pattern = pattern[1:-1] elif pattern not in self.replacements: return formatter.error('Regex not in replacements.') #check if they have correct permissions if author_id != self.replacements[pattern][1] \ and not perms.is_owner_check(author_id): raise commands.errors.CheckFailure('Cannot delete') self.replacements.pop(pattern) self.replacements.save() return formatter.ok() def ls(self): msg = '\n'.join(f'{k} -> {v}' for k, v in self.replacements.items()) return formatter.code(msg) def replace(self, message): rep = message for i in self.replacements: rep = re.sub(r'(?i)\b{}\b'.format(i), self.replacements[i][0], rep) if rep.lower() != message.lower(): return rep return None def is_banned(self, author_id): return author_id in self.permissions['rep-blacklist']
class Server: def __init__(self, bot): self.bot = bot self.conf = Config('configs/server.json') self.cut = {} heap = self.bot.get_cog('HeapCog') for rem in self.conf.pop('end_role', []): self.bot.loop.run_until_complete(heap.push(rem)) @perms.pm_or_perms(manage_messages=True) @commands.command(name='prune') async def _prune(self, ctx, num_to_delete: int, *message): """ deletes specified number of messages from channel if message is specified, message will be echoed by bot after prune USAGE: .prune <num> [user] [message...] NOTE: if first word after number is a user, only user's messages will be pruned """ # tmp channel/server pointer chan = ctx.message.channel serv = ctx.message.guild #if num_to_delete > 100: # api only allows up to 100 # await ctx.send('Sorry, only up to 100') # TODO - copy thing done in # return # self._paste if num_to_delete < 1: # delete nothing? await ctx.send('umm... no') # answer: no return # if the first word in the message matches a user, # remove that word from the message, store the user try: user = dh.get_user(serv or self.bot, message[0]) if user: message = message[1:] except: logger.debug('did not match a user') user = None check = lambda m: True if user: # if a user was matched, delete messages for that user only logger.debug(f'pruning for user {user.name}') check = lambda m: str(m.author.id) == str(user.id) message = ' '.join(message) #make the message a string logs = [] async for m in chan.history(limit=num_to_delete, reverse=True): if check(m): logs.append(m) deleted = len(logs) old = False while len(logs) > 0: # while there are messages to delete if len(logs) > 1: # if more than one left to delete and not old, if not old: # attempt batch delete [2-100] messages try: await chan.delete_messages(logs[:100]) except: # if problem when batch deleting old = True # then the messages must be old if old: # if old, traverse and delete individually for entry in logs[:100]: try: await entry.delete() except: logger.exception( '<{0.author.name}> {0.content}'.format(entry)) logs = logs[100:] else: # if only one message, delete individually await logs[0].delete() logs.remove(logs[0]) #report that prume was complete, how many were prunned, and the message await ctx.send( ok('Deleted {} message{} {}'.format( deleted, '' if deleted == 1 else 's', f'({message})' if message else ''))) @commands.group(name='role', aliases=['give', 'giveme', 'gimme']) async def _role(self, ctx): """ Manage publicly available roles """ # if no sub commands were called, guess at what the user wanted to do if ctx.invoked_subcommand is None: msg = ctx.message.content.split() # attempt to parse args if len(msg) < 2: await ctx.send('see help (`.help role`)') return role = msg[1] date = ' '.join(msg[2:]) # if the user cannot manage roles, then they must be requesting a role # or they are trying to do something that they are not allowed to if not perms.check_permissions(ctx.message, manage_roles=True): await self._request_wrap(ctx, role, date) # attempt to request role return #if the user does have permission to manage, they must be an admin/mod # ask them what they want to do - since they clearly did not know what # they were trying to do await ctx.send('Are you trying to [a]dd a new role ' + \ 'or are you [r]equesting this role for yourself?' ) try: # wait for them to reply def check(m): return m.author == ctx.message.author and \ m.channel == ctx.message.channel msg = await self.bot.wait_for('message', timeout=30, check=check) except: # if they do not reply, give them a helpful reply # without commenting on their IQ await ctx.send( error('Response timeout, maybe look at the help?')) return # if a reply was recived, check what they wanted to do and pass along msg = msg.content.lower() if msg.startswith('a') or 'add' in msg: # adding new role to list await self._add_wrap(ctx, role) reply = f"Please run `.role request {role}` to get the \"{role}\" role" await ctx.send(reply) elif msg.startswith( 'r') or 'request' in msg: # requesting existing role await self._request_wrap(ctx, role, date) else: # they can't read await ctx.send(error('I have no idea what you are attempting' + \ ' to do, maybe look at the help?') ) @_role.command(name='add', aliases=['create', 'a']) @perms.has_perms(manage_roles=True) async def _add(self, ctx, role: str): """ adds role to list of public roles """ await self._add_wrap(ctx, role) @_role.command(name='list', aliases=['ls', 'l']) async def _list(self, ctx): """ lists public roles avalible in the server """ # pull roles out of the config file serv = ctx.message.guild names = [] m_len = 0 available_roles = self.conf.get(str(serv.id), {}).get('pub_roles', []) # if no roles, say so if not available_roles: await ctx.send('no public roles in this server\n' + \ ' see `.help role create` and `.help role add`' ) return # For each id in list # find matching role in server # if role exists, add it to the role list # Note: this block also finds the strlen of the longest role name, # this will be used later for formatting for role_id in available_roles: role = discord.utils.find(lambda r: str(r.id) == role_id, serv.roles) if role: names.append(role.name) m_len = max(m_len, len(role.name)) # create a message with each role name and id on a seperate line # seperators(role - id) should align due to spacing - this is what the # lenght of the longest role name is used for msg = 'Roles:\n```' line = '{{:{}}} - {{}}\n'.format(m_len) for name, rid in zip(names, available_roles): msg += line.format(name, rid) # send message with role list await ctx.send(msg + '```') @_role.command(name='remove', aliases=['rm']) @perms.has_perms(manage_roles=True) async def _delete(self, ctx, role: str): """ removes role from list of public roles """ # attempt to find specified role and get list of roles in server serv = ctx.message.guild role = dh.get_role(serv, role) guild_id = str(serv.id) role_id = str(role.id) available_roles = self.conf.get(guild_id, {}).get('pub_roles', []) # if user failed to specify role, complain if not role: await ctx.send('Please specify a valid role') return if guild_id not in self.conf: self.conf[guild_id] = {'pub_roles': []} self.conf.save() elif 'pub_roles' not in self.conf[guild_id]: self.conf[guild_id]['pub_roles'] = [] self.conf.save() if role_id in available_roles: # if role is found, remove and report self.conf[guild_id]['pub_roles'].remove(guild_id) self.conf.save() await ctx.send(ok('role removed from public list')) else: # if role is not in server, just report await ctx.send(error('role is not in the list')) @_role.command(name='request', aliases=['r']) async def _request(self, ctx, role: str, date: str = ''): """ adds role to requester(if in list) """ await self._request_wrap(ctx, role, date) @_role.command(name='unrequest', aliases=['rmr', 'u']) async def _unrequest(self, ctx, role: str): """removes role from requester(if in list)""" # attempt to find role that user specied for removal auth = ctx.message.author serv = ctx.message.guild role = dh.get_role(serv, role) guild_id = str(serv.id) role_id = str(role.id) # if user failed to find specify role, complain if not role: await ctx.send('Please specify a valid role') return # get a list of roles that are listed as public and the user roles available_roles = self.conf.get(guild_id, {}).get('pub_roles', []) user_roles = discord.utils.find(lambda r: str(r.id) == role_id, auth.roles) # ONLY remove roles if they are in the public roles list # Unless there is no list, # in which case any of the user's roles can be removed if role_id in available_roles or user_roles: await auth.remove_roles(role) await ctx.send(ok('you no longer have that role')) else: await ctx.send(error('I\'m afraid that I can\'t remove that role')) # wrapper function for adding roles to public list async def _add_wrap(self, ctx, role): serv = ctx.message.guild # find the role, # if it is not found, create a new role role_str = role if type(role) != discord.Role: role = dh.get_role(serv, role_str) if not role: role = await serv.create_role(name=role_str, mentionable=True) await ctx.send(ok(f'New role created: {role_str}')) # if still no role, report and stop if not role: await ctx.send(error("could not find or create role role")) return guild_id = str(serv.id) role_id = str(role.id) # The @everyone role (also @here iiuc) cannot be given/taken if role.is_everyone: await ctx.send(error('umm... no')) return if guild_id not in self.conf: # if server does not have a list yet create it self.conf[guild_id] = {'pub_roles': [role_id]} elif 'pub_roles' not in self.conf[guild_id]: # if list is corruptted self.conf[guild_id]['pub_roles'] = [role_id] # fix it elif role_id in self.conf[guild_id][ 'pub_roles']: # if role is already there await ctx.send('role already in list') # report and stop return else: # otherwise add it to the list and end self.conf[guild_id]['pub_roles'].append(role_id) # save any changes to config file, and report success self.conf.save() await ctx.send(ok('role added to public role list')) # wrapper function for getting roles that are on the list async def _request_wrap(self, ctx, role, date=''): auth = ctx.message.author chan = ctx.message.channel serv = ctx.message.guild # attempt to find the role if a string was given, # if not found, stop if type(role) != discord.Role: role = dh.get_role(serv, role) if not role: await ctx.send(error("could not find role, ask a mod to create it") ) return # get list of public roles available_roles = self.conf.get(guild_id, {}).get('pub_roles', []) role_id = str(role.id) guild_id = str(serv.id) if role_id in available_roles: # if role is a public role, await auth.add_roles(role) # give it await ctx.send(ok('you now have that role')) else: # otherwise don't await ctx.send( error('I\'m afraid that I can\'t give you that role')) return if date: # if a timeout was specified end_time = dh.get_end_time(date)[0] role_end = RoleRemove(end_time, role_id, str(auth.id), str(chan.id), guild_id) await self.bot.get_cog('HeapCog').push(role_end, self.bot) @perms.pm_or_perms(manage_messages=True) @commands.command(name='cut') async def _cut(self, ctx, cut: str, skip: str = ''): ''' cuts num_to_cut messages from the current channel skips over num_to_skip messages (skips none if not specified) example: User1: first message User2: other message User3: final message Using ".cut 1" will cut User3's message Using ".cut 1 1" will cut User2's message Using ".cut 3" will cut all messages Using ".cut 3:other" will cut User2 and 3's messages Using ".cut id:XXX" will cut id XXX Using ".cut id:XXX id:YYY" will cut messages in range (id XXX, id YYY] messages will not be deleted until paste needs manage_messages perm in the current channel to use see .paste ''' #if num_to_cut > 100: # await ctx.send('Sorry, only up to 100') # return in_id = re.search('^id:(\\d+)$', cut) in_re = re.search('^(\d+):(.+)$', cut) if in_id: cut = await ctx.message.channel.get_message(int(in_id.group(1))) elif in_re: cut = int(in_re.group(1)) in_re = re.compile(in_re.group(2)) elif re.search('^\d+$', cut): cut = int(cut) else: await ctx.send(error('bad cut parameter')) return skip_id = re.search('^id:(\\d+)$', skip) skip_re = re.search('^(\d+):(.+)$', skip) if skip_id: skip = await ctx.message.channel.get_message(int(skip_id.group(1))) elif skip_re: skip = int(skip_re.group(1)) skip_re = re.compile(skip_re.group(2)) elif not skip or re.search('^\d+$', skip): skip = int(skip or '0') else: await ctx.send(error('bad skip parameter')) return if not cut or (type(cut) == int and cut < 1): # can't cut no messages await ctx.send('umm... no') return # store info in easier to access variables chan = ctx.message.channel bef = ctx.message.created_at aid = str(ctx.message.author.id) cid = str(chan.id) # delete the original `.cut` message(not part of cutting) # also sorta serves as confirmation that messages have been cut await ctx.message.delete() # if messages should be skipped when cutting # save the timestamp of the oldest message if skip: if type(skip) == int: run = lambda: chan.history(limit=skip, reverse=True) else: run = lambda: chan.history(after=skip, reverse=True) async for m in run(): if skip_re and not skip_re.search(m.content): continue bef = m.created_at break # save the logs to a list #store true in position 0 of list if channel is a nsfw channel logs = ['nsfw' in chan.name.lower()] if type(cut) == int: run = lambda: chan.history(limit=cut, before=bef, reverse=True) else: run = lambda: chan.history(after=cut, before=bef, reverse=True) async for m in run(): if in_re and in_re.search(m.content): in_re = False elif in_re: continue logs.append(m) # save logs to dict (associate with user) self.cut[aid] = logs @perms.has_perms(manage_messages=True) @commands.command(name='paste') async def _paste(self, ctx): ''' paste cutted messages to current channel needs manage_messages perm in the current channel to use deletes original messages see .cut ''' # get messages that were cut and other variables aid = str(ctx.message.author.id) chan = ctx.message.channel logs = self.cut.pop(aid, []) # if nothing was cut, stop if not logs: await ctx.send('You have not cut anything') return # it messages were cut in a nsfw channel, # do not paste unless this is a nsfw channel # NOTE: cutting/pasting to/from PMs is not possible(for now) if logs[0] and 'nsfw' not in chan.name.lower(): await ctx.send('That which hath been cut in nsfw, ' + \ 'mustn\'t be pasted in such a place' ) return # remove the nsfw indicator(since it's not really part of the logs) logs = logs[1:] # delete the `.paste` message await ctx.message.delete() # compress the messages - many messages can be squished into 1 big message # but ensure that output messages do not exceede the discord message limit buf = '' # current out message that is being compressed to out = [] # output messages that have been compressed for message in logs: # save messages as: # <nick> message # and attachments as(after the message): # filename: url_to_attachment if message.content or message.attachments: tmp = '<{0.author.name}> {0.content}\n'.format(message) for a in message.attachments: tmp += '{0.filename}: {0.url}\n'.format(a) else: tmp = '' # if this message would make the current output buffer too long, # append it to the output message list and reset the buffer # or just append to the buffer if it's not going to be too long if len(buf) + len(tmp) > 1900: out.append(buf) buf = tmp else: buf += tmp # if the message is composed of *only* embeds, # flush buffer, # and append embed to output list if message.embeds and not message.content: if buf: out.append(buf) buf = '' for embed in message.embeds: out.append(embed) # if there is still content in the buffer after messages have been traversed # treat buffer as complete message if buf: out.append(buf) # send each message in output list for mes in out: if type(mes) == str: if mes: await ctx.send(mes) else: # if it's an embed, n await ctx.send(embed=EmWrap(mes)) # it needs to be wrapped # once all messages have been pasted, delete(since cut) the old ones old = False # messages older than 2 weeks cannot be batch deleted while len(logs) > 0: # while there are messages to delete if len(logs) > 1: # if more than one left to delete and not old, if not old: # attempt batch delete [2-100] messages try: await chan.delete_messages(logs[:100]) except: # if problem when batch deleting old = True # then the messages must be old if old: # if old, traverse and delete individually for entry in logs[:100]: await entry.delete() logs = logs[100:] else: # if only one message, delete individually await logs[0].delete() logs.remove(logs[0]) # remove cut entry from dict of cuts if aid in self.cut: del self.cut[aid] @commands.command(name='topic') async def _topic(self, ctx, *, new_topic=''): """manage topic if a new_topic is specified, changes the topic otherwise, displays the current topic """ # store channel in tmp pointer c = ctx.message.channel if new_topic: # if a topic was passed, # change it if user has the permisssions to do so # or tell user that they can't do that if perms.check_permissions(ctx.message, manage_channels=True): await c.edit(topic=new_topic) await ctx.send( ok('Topic for #{} has been changed'.format(c.name))) else: await ctx.send( error('You cannot change the topic for #{}'.format(c.name)) ) elif c.topic: # if no topic has been passed, # say the topic await ctx.send('Topic for #{}: `{}`'.format(c.name, c.topic)) else: # if not topic in channel, # say so await ctx.send('#{} has no topic'.format(c.name)) @perms.has_perms(manage_roles=True) @commands.command(name='timeout_send', aliases=['ts']) async def _timeout_send(self, ctx, member: discord.Member, time: float = 300): """puts a member in timeout for a duration(default 5 min) usage `.timeout [add] @member [time in seconds]` """ heap = self.bot.get_cog('HeapCog') if not perms.is_owner() and \ ctx.message.author.server_permissions < member.server_permissions: await ctx.send('Can\'t send higher ranking members to timeout') return server = ctx.message.guild channel = ctx.message.channel if perms.in_group('timeout') and not perms.is_owner(): await ctx.send('You\'re in timeout... No.') return if not ctx.message.guild: await ctx.send('not in a server at the moment') return if time < 10: await ctx.send('And what would the point of that be?') return if time > 10000: await ctx.send('Too long, at this point consider banning them') return criteria = lambda m: re.search('(?i)^time?[ _-]?out.*', m.name) to_role = discord.utils.find(criteria, server.roles) to_chan = discord.utils.find(criteria, server.channels) try: timeout_obj = Timeout(channel, server, member, time) await heap.push(timeout_obj, self.bot, to_role, to_chan) except: for index, obj in enumerate(heap): if obj == timeout_obj: heap.pop(index) break await ctx.send( 'There was an error sending {}\'s to timeout \n({}{}\n)'. format( member.name, '\n - do I have permission to manage roles(and possibly channels)?', '\n - is my highest role above {}\'s highest role?'. format(member.name))) #raise @perms.has_perms(manage_roles=True) @commands.command(name='timeout_end', aliases=['te']) async def _timeout_end(self, ctx, member: discord.Member): """removes a member from timeout usage `.timeout end @member` """ server = ctx.message.guild channel = ctx.message.channel if perms.in_group('timeout') and not perms.is_owner(): await ctx.send('You\'re in timeout... No.') return if not ctx.message.guild: await ctx.send('not in a server at the moment') return # test timeout object for comparison test = namedtuple({ 'server_id': int(server.id), 'user_id': int(member.id) }) index = 0 # inext is used to more efficently pop from heap # error message in case ending timeout fails error_msg = 'There was an error ending {}\'s timeout \n({}{}\n)'.format( member.name, '\n - do I have permission to manage roles(and possibly channels)?', '\n - is my highest role above {}\'s highest role?'.format( member.name)) for timeout in Timeout.conf['timeouts']: # look trhough all timouts if timeout == test: # if found try: await timeout.end(self.bot, index) # attempt to end except: await ctx.send(error_msg ) # if error when ending, report return index += 1 # not found increment index else: # not found at all, report await ctx.send('{} is not in timeout...'.format(member.name)) return # checks timeouts and restores perms when timout expires async def check_timeouts(self): if 'timeouts' not in Timeout.conf: #create timeouts list if needed Timeout.conf['timeouts'] = [] while self == self.bot.get_cog('Server'): # in case of cog reload # while timeouts exist, and the next one's time has come, # end it while Timeout.conf['timeouts'] and \ Timeout.conf['timeouts'][0].time_left < 1: await Timeout.conf['timeouts'][0].end(self.bot, 0) # wait a bit and check again # if the next one ends in < 15s, wait that much instead of 15s if Timeout.conf['timeouts']: delay = min(Timeout.conf['timeouts'].time_left, 15) else: delay = 15 await asyncio.sleep(delay + 0.5)
class General: def __init__(self, bot): self.bot = bot self.loop = bot.loop self.stopwatches = {} self.conf = Config('configs/general.json') heap = self.bot.get_cog('HeapCog') if 'responses' not in self.conf: self.conf['responses'] = {} if 'todo' not in self.conf: self.conf['todo'] = {} if 'situations' not in self.conf: self.conf['situations'] = [] if '8-ball' not in self.conf: self.conf['8-ball'] = [] for rem in self.conf.pop('reminders', []): self.loop.run_until_complete(heap.push(rem, None)) self.conf.save() @commands.command(hidden=True) async def ping(self, ctx): """Pong.""" async with ctx.typing(): await ctx.send("Pong.") @commands.command() async def time(self, ctx, first=''): '''remind people to hurry up''' say = lambda msg: ctx.send(msg) if random.randrange(50) or not first.startswith('@'): async with ctx.typing(): now = datetime.now().replace(microsecond=0) await say(now.isoformat().replace('T', ' ')) else: async with ctx.typing(): await asyncio.sleep(1.2) await say('ゲネラルリベラル') async with ctx.typing(): await asyncio.sleep(0.4) await say('デフレイスパイラル') async with ctx.typing(): await asyncio.sleep(0.5) await say('ナチュラルミネラル') async with ctx.typing(): await asyncio.sleep(0.2) await say('さあお出で' + (': ' + first if first else '')) @commands.command() async def invite(self, ctx): '''reply with a link that allows this bot to be invited''' await ctx.send( f'https://discordapp.com/oauth2/authorize?client_id={self.bot.user.id}' + '&permissions=305260592&scope=bot') async def tally(self, message): chan = message.channel user = message.author mess = message.content loop = asyncio.get_event_loop() logger.debug('tally start') #bots don't get a vote if user.bot: return if len(mess.strip()) < 2 or \ mess.strip()[0] in self.bot.command_prefix + ['$','?','!']: return test_poll = Poll('', [], chan, 0, 1) heap = self.bot.get_cog('HeapCog') for poll in heap: if test_poll == poll: await loop.run_in_executor(None, poll.vote, user, mess) logger.debug('tally end') async def respond(self, message): if message.author.bot: return if len(message.content.strip()) < 2 or \ message.content.strip()[0] in self.bot.command_prefix + ['$','?','!']: return logger.debug('respond start') loop = asyncio.get_event_loop() for i in self.conf['responses']: if re.search("(?i){}".format(i[0]), message.content): rep = i[1] subs = { "\\{un\\}": message.author.name, "\\{um\\}": message.author.mention, "\\{ui\\}": message.author.mention, "\\{situations\\}": random.choice(self.conf['situations']) } for j in re.findall("\\(.*?\\|.*?\\)", rep): rep = rep.replace(j, random.choice(j[1:-1].split("|"))) for j in subs: rep = await loop.run_in_executor(None, re.sub, j, subs[j], rep) for j in re.findall("\\(.*?\\|.*?\\)", rep): rep = rep.replace(j, random.choice(j[1:-1].split("|"))) msg = re.sub("(?i){}".format(i[0]), rep, message.content) if rep: await message.channel.send(msg) break logger.debug('respond start') @commands.command(name='roll', aliases=['r', 'clench']) async def _roll(self, ctx, *dice): """rolls dice given pattern [Nd]S[(+|-)C] N: number of dice to roll S: side on the dice C: constant to add or subtract from each die roll """ loop = asyncio.get_event_loop() async with ctx.typing(): total, roll = await loop.run_in_executor(None, self.rolls, dice) roll = '\n'.join(roll) message = ctx.message.author.mention + ':\n' if '\n' in roll: message += code(roll + f'\nTotal: {total}') else: message += inline(roll) await ctx.send(message) @commands.command(name="8ball", aliases=["8"]) async def _8ball(self, ctx, *, question: str): """Ask 8 ball a question Question must end with a question mark. """ async with ctx.typing(): if question.endswith("?") and question != "?": await ctx.send("`" + random.choice(self.conf['8-ball']) + "`") else: await ctx.send("That doesn't look like a question.") @commands.group(aliases=['t', 'td']) async def todo(self, ctx): ''' manages user TODO list Note: if no sub-command is specified, TODOs will be listed ''' if ctx.invoked_subcommand is None: async with ctx.typing(): await self._td_list(ctx) @todo.command(name='list', aliases=['l', 'ls']) async def _td_list_wp(self, ctx): ''' prints your complete todo list ''' async with ctx.typing(): await self._td_list(ctx) @todo.command(name='add', aliases=['a', 'insert', 'i']) async def _td_add(self, ctx, *, task: str): ''' adds a new task to your todo list ''' async with ctx.typing(): todos = self.conf['todo'].get(str(ctx.message.author.id), []) todos.append([False, task]) self.conf['todo'][str(ctx.message.author.id)] = todos self.conf.save() await ctx.send(ok()) @todo.command(name='done', aliases=['d', 'complete', 'c']) async def _td_done(self, ctx, *, index: int): ''' sets/unsets a task as complete Note: indicies start at 1 ''' async with ctx.typing(): todos = self.conf['todo'].get(str(ctx.message.author.id), []) if len(todos) < index or index <= 0: await ctx.send(error('Invalid index')) else: index -= 1 todos[index][0] = not todos[index][0] self.conf['todo'][str(ctx.message.author.id)] = todos self.conf.save() await ctx.send(ok()) @todo.command(name='remove', aliases=['rem', 'rm', 'r']) async def _td_remove(self, ctx, *, index: int): ''' remove a task from your todo list Note: indicies start at 1 ''' async with ctx.typing(): todos = self.conf['todo'].get(str(ctx.message.author.id), []) if len(todos) < index or index <= 0: await ctx.send(error('Invalid index')) else: task = todos.pop(index - 1) self.conf['todo'][str(ctx.message.author.id)] = todos self.conf.save() await ctx.send(ok('Removed task #{}'.format(index))) async def _td_list(self, ctx): todos = self.conf['todo'].get(str(ctx.message.author.id), []) if not todos: await ctx.send('No TODOs found.') else: #TODO - ensure that the outgoing message is not too long msg = 'TODO:\n' length = len(str(len(todos))) done = '{{:0{}}} - ~~{{}}~~\n'.format(length) working = '{{:0{}}} - {{}}\n'.format(length) for i, todo in enumerate(todos, 1): if todo[0]: msg += done.format(i, todo[1]) else: msg += working.format(i, todo[1]) await ctx.send(msg) @commands.group(aliases=["sw"]) async def stopwatch(self, ctx): """ manages user stopwatch starts/stops/unpauses (depending on context) """ if ctx.invoked_subcommand is None: aid = str(ctx.message.author.id) if aid in self.stopwatches and self.stopwatches[aid][0]: await self._sw_stop(ctx) else: await self._sw_start(ctx) @stopwatch.command(name='start', aliases=['unpause', 'u', 'resume', 'r']) async def _sw_start_wrap(self, ctx): """ unpauses or creates new stopwatch """ await self._sw_start(ctx) async def _sw_start(self, ctx): aid = str(ctx.message.author.id) tme = ctx.message.timestamp.timestamp() if aid in self.stopwatches and self.stopwatches[aid][0]: await ctx.send('You\'ve already started a stopwatch.') elif aid in self.stopwatches: self.stopwatches[aid][0] = tme await ctx.send('Stopwatch resumed.') else: self.stopwatches[aid] = [tme, 0] await ctx.send('Stopwatch started.') @stopwatch.command(name='stop', aliases=['end', 'e']) async def _sw_stop_wrap(self, ctx): """ prints time and deletes timer works even if paused """ await self._sw_stop(ctx) async def _sw_stop(self, ctx): aid = str(ctx.message.author.id) now = ctx.message.timestamp.timestamp() old = self.stopwatches.pop(aid, None) if old: if old[0]: tme = now - old[0] + old[1] else: tme = old[1] tme = str(timedelta(seconds=tme)) msg = '```Stopwatch stopped: {}\n'.format(tme) for lap in zip(range(1, len(old)), old[2:]): msg += '\nLap {0:03} - {1}'.format(*lap) msg += '```' await ctx.send(msg) else: await ctx.send('No stop watches started, cannot stop.') @stopwatch.command(name='status', aliases=['look', 'peak']) async def _sw_status(self, ctx): aid = str(ctx.message.author.id) now = ctx.message.timestamp.timestamp() if aid in self.stopwatches: old = self.stopwatches[aid] if old[0]: tme = now - old[0] + old[1] else: tme = old[1] tme = str(timedelta(seconds=tme)) msg = '```Stopwatch time: {}'.format(tme) if old[0]: msg += '\n' else: msg += ' [paused]\n' for lap in zip(range(1, len(old)), old[2:]): msg += '\nLap {0:03} - {1}'.format(*lap) msg += '```' await ctx.send(msg) else: await ctx.send('No stop watches started, cannot look.') @stopwatch.command(name='lap', aliases=['l']) async def _sw_lap(self, ctx): """ prints time does not pause, does not resume, does not delete """ aid = str(ctx.message.author.id) now = ctx.message.timestamp.timestamp() if aid in self.stopwatches: old = self.stopwatches[aid] if old[0]: tme = now - old[0] + old[1] else: tme = old[1] tme = str(timedelta(seconds=tme)) await ctx.send("Lap #{:03} time: **{}**".format(len(old) - 1, tme)) if self.stopwatches[aid][-1] != tme: self.stopwatches[aid].append(tme) else: await ctx.send('No stop watches started, cannot lap.') @stopwatch.command(name='pause', aliases=['p', 'hold', 'h']) async def _sw_pause(self, ctx): """ pauses the stopwatch Also prints current time, does not delete """ aid = str(ctx.message.author.id) now = ctx.message.timestamp.timestamp() if aid in self.stopwatches and self.stopwatches[aid][0]: old = now - self.stopwatches[aid][0] + self.stopwatches[aid][1] self.stopwatches[aid] = [0, old] old = str(timedelta(seconds=old)) await ctx.send("Stopwatch paused: **{}**".format(old)) elif aid in self.stopwatches: await ctx.send('Stop watch already paused.') else: await ctx.send('No stop watches started, cannot pause.') def rolls(self, dice): out = [] if not dice: dice = ['20'] gobal_total = 0 for roll in dice: match = re.search('^((\\d+)?d)?(\\d+)([+-]\\d+)?$', roll, re.I) message = '' total = 0 if not match: message = 'Invalid roll' else: times = 1 sides = int(match.group(3)) add = 0 if match.group(2): times = int(match.group(2)) if match.group(4): add = int(match.group(4)) if times > 100: message = 'Cannot roll that many dice' elif sides > 120: message = 'Cannot find a dice with that many sides' elif times < 1: message = 'How?' elif sides < 2: message = 'No' else: for i in range(times): num = random.randint(1, sides) + add total += num message += '{}, '.format(num) message = message[:-2] gobal_total += total if times > 1: message += ' (sum = {})'.format(total) out.append('{}: {}'.format(roll, message)) return (gobal_total, out) @commands.command(aliases=['c', 'choice']) async def choose(self, ctx, *, choices): """Chooses a value from a comma seperated list""" choices = split(choices) choice = random.choice(choices) choice_reps = { r'(?i)^(should)\s+I\s+': r'You \1 ', r'(?i)^([wcs]hould|can|are|were|is)\s+(\S+)\s+': r'\2 \1 ', r'\?$': '.', r'(?i)^am\s+I\s+': 'Thou art ', r'(?i)\b(I|me)\b': 'you', r'(?i)\bmy\b': 'your' } for r in choice_reps: choice = re.sub(r, choice_reps[r], choice) message = ctx.message.author.mention + ':\n' message += inline(choice) await ctx.send(message) @commands.command(name='remindme', aliases=['remind']) async def _add_reminder(self, ctx, *, message: str): ''' adds a reminder 'at' can be used when specifing exact time 'in' is optional for offsets 'me' can be seperate or part of the command name (also optinal) cannot mix offsets and exact times Samples: .remind me in 5 h message .remind me in 5 hours 3 m message .remind me 1 week message .remind me 7 months message .remindme in 7 months message .remind me at 2017-10-23 message .remind me at 2017-10-23T05:11:56 message .remindme at 2017-10-23 05:11:56 message .remindme at 10/23/2017 5:11 PM message .remind at 7:11 message .remind at 7:11:15 message .remind [me] remove <id> .remind [me] end <id> ''' heap = self.bot.get_cog('HeapCog') author = str(ctx.message.author.id) channel = str(ctx.message.channel.id) match = re.match(r'(?i)^(me\s+)?(remove|end|stop)\s+(\d+)', message) if match: async with ctx.typing(): rid = int(match.group(3)) for index, item in enumerate(heap): if type(item) == Reminder \ and item.reminder_id == rid \ and item.user_id == author: heap.pop(index) await ctx.send( ok(f'Message with id {rid} has been removed')) return else: await ctx.send(ok(f'Could not find message with id {rid}')) else: r = Reminder(channel, author, message) await heap.push(r, ctx) @commands.command(aliases=['a', 'ask']) async def question(self, ctx): '''Answers a question with yes/no''' message = ctx.message.author.mention + ':\n' message += inline(random.choice(['yes', 'no'])) await ctx.send(message) @commands.command() async def poll(self, ctx, *, question): ''' Starts a poll format: poll question? opt1, opt2, opt3 or opt4... poll stop|end ''' heap = self.bot.get_cog('HeapCog') cid = int(ctx.message.channel.id) if question.lower().strip() in ['end', 'stop']: for index, poll in enumerate(heap): if isinstance(poll, Poll) and poll.channel_id == cid: heap.pop(index) await poll.end(self.bot) break else: await ctx.send('There is no poll active in this channel') return match = re.search(r'^(.*?\?)\s*(.*?)$', question) if not match: await ctx.send('Question could not be found.') return options = split(match.group(2)) question = escape_mentions(match.group(1)) poll = Poll(question, options, ctx.message.channel, 600) for item in heap: if poll == item: await ctx.send('There is a poll active in this channel already' ) return await heap.push(poll, ctx)