def test_eventual_accuracy(self): result = AsyncResult() should_be_called_at = None def f(): result.set(datetime.utcnow()) e = Eventual(f) should_be_called_at = datetime.utcnow() + timedelta(milliseconds=100) e.set_next_schedule(should_be_called_at) called_at = result.get() self.assertGreater(called_at, should_be_called_at)
def test_eventual_coalesce(): ref = {'value': 0} done = Event() e = Eventual(None) def func(): ref['value'] += 1 # Only the first time if ref['value'] == 1: e.set_next_schedule(datetime.utcnow() - timedelta(seconds=10)) time.sleep(1) done.set() e.function = func e.set_next_schedule(datetime.utcnow() + timedelta(milliseconds=500)) done.wait() assert ref['value'] == 1
def test_eventual_lowering(): result = AsyncResult() e = Eventual(lambda: result.set(datetime.utcnow())) e.set_next_schedule(datetime.utcnow() + timedelta(seconds=10)) e.set_next_schedule(datetime.utcnow() + timedelta(seconds=5)) should_be_called_at = datetime.utcnow() + timedelta(milliseconds=100) e.set_next_schedule(should_be_called_at) called_at = result.get() assert called_at >= should_be_called_at assert called_at <= should_be_called_at + timedelta(milliseconds=100)
class UtilitiesPlugin(Plugin): def load(self, ctx): super(UtilitiesPlugin, self).load(ctx) self.reminder_task = Eventual(self.trigger_reminders) self.spawn_later(10, self.queue_reminders) def queue_reminders(self): try: next_reminder = Reminder.select().order_by( Reminder.remind_at.asc() ).limit(1).get() except Reminder.DoesNotExist: return self.reminder_task.set_next_schedule(next_reminder.remind_at) @Plugin.command('coin', group='random', global_=True) def coin(self, event): """ Flip a coin """ event.msg.reply(random.choice(['heads', 'tails'])) @Plugin.command('number', '[end:int] [start:int]', group='random', global_=True) def random_number(self, event, end=10, start=0): """ Returns a random number """ # Because someone will be an idiot if end > 9223372036854775807: return event.msg.reply(':warning: ending number too big!') if end <= start: return event.msg.reply(':warning: ending number must be larger than starting number!') event.msg.reply(str(random.randint(start, end))) @Plugin.command('cat', global_=True) def cat(self, event): # Sometimes random.cat gives us gifs (smh) for _ in range(3): try: r = requests.get('https://aws.random.cat/meow') r.raise_for_status() except: continue url = r.json()['file'] if not url.endswith('.gif'): break else: return event.msg.reply('404 cat not found :(') r = requests.get(url) r.raise_for_status() event.msg.reply('', attachments=[('cat.jpg', r.content)]) @Plugin.command('urban', '<term:str...>', global_=True) def urban(self, event, term): r = requests.get('http://api.urbandictionary.com/v0/define', params={ 'term': term, }) r.raise_for_status() data = r.json() if not len(data['list']): return event.msg.reply(':warning: no matches') event.msg.reply(u'{} - {}'.format( S(data['list'][0]['word']), S(data['list'][0]['definition']), )) @Plugin.command('pwnd', '<email:str>', global_=True) def pwnd(self, event, email): r = requests.get('https://haveibeenpwned.com/api/v2/breachedaccount/{}'.format( email )) if r.status_code == 404: return event.msg.reply(":white_check_mark: you haven't been pwnd yet, awesome!") r.raise_for_status() data = r.json() sites = [] for idx, site in enumerate(data): sites.append(u'{} - {} ({})'.format( site['BreachDate'], site['Title'], site['Domain'], )) return event.msg.reply(u":warning: You've been pwnd on {} sites:\n{}".format( len(sites), '\n'.join(sites), )) @Plugin.command('geoip', '<ip:str>', global_=True) def geoip(self, event, ip): r = requests.get('http://json.geoiplookup.io/{}'.format(ip)) r.raise_for_status() data = r.json() event.msg.reply(u'{} - {}, {} ({}) | {}, {}'.format( data['isp'], data['city'], data['region'], data['country_code'], data['latitude'], data['longitude'], )) @Plugin.command('emoji', '<emoji:str>', global_=True) def emoji(self, event, emoji): if not EMOJI_RE.match(emoji): return event.msg.reply(u'Unknown emoji: `{}`'.format(S(emoji))) fields = [] name, eid = EMOJI_RE.findall(emoji)[0] fields.append('**ID:** {}'.format(eid)) fields.append('**Name:** {}'.format(S(name))) guild = self.state.guilds.find_one(lambda v: eid in v.emojis) if guild: fields.append('**Guild:** {} ({})'.format(S(guild.name), guild.id)) anim = emoji.startswith('<a:') fields.append('**Animated:** {}'.format('Yes' if anim else 'No')) ext = 'gif' if anim else 'png' url = 'https://discordapp.com/api/emojis/{}.{}'.format(eid, ext) r = requests.get(url) r.raise_for_status() return event.msg.reply('\n'.join(fields), attachments=[('emoji.'+ext, r.content)]) # Full credit goes to: Xenthys def get_emoji_url(self, emoji): name = '-'.join(char.encode("unicode_escape").decode("utf-8")[2:].lstrip("0") for char in emoji) return 'https://cdn.oceanlord.me/emoji/{}.png'.format(name) if name.find('--') == -1 else None @Plugin.command('jumbo', '<emojis:str...>', global_=True) def jumbo(self, event, emojis): emojis = emojis.split(' ') if len(emojis) == 1: url = ext = '' emoji = emojis[0] if EMOJI_RE.match(emoji): _, eid = EMOJI_RE.findall(emoji)[0] ext = 'gif' if emoji.startswith('<a:') else 'png' url = 'https://discordapp.com/api/emojis/{}.{}'.format(eid, ext) else: ext = 'png' url = self.get_emoji_url(emoji) if not url: raise CommandFail('provided emoji is invalid') r = requests.get(url) try: r.raise_for_status() except requests.HTTPError: raise CommandFail('provided emoji is invalid') return event.msg.reply('', attachments=[('emoji.'+ext, r.content)]) else: urls = [] for emoji in emojis[:5]: if EMOJI_RE.match(emoji): _, eid = EMOJI_RE.findall(emoji)[0] urls.append('https://discordapp.com/api/emojis/{}.png'.format(eid)) else: url = self.get_emoji_url(emoji) urls.append(url) if url else None width, height, images = 0, 0, [] for r in Pool(6).imap(requests.get, urls): try: r.raise_for_status() except requests.HTTPError: continue img = Image.open(BytesIO(r.content)) height = img.height if img.height > height else height width += img.width + 10 images.append(img) if not images: raise CommandFail('provided emojis are invalid') image = Image.new('RGBA', (width, height)) width_offset = 0 for img in images: image.paste(img, (width_offset, 0)) width_offset += img.width + 10 combined = BytesIO() image.save(combined, 'png', quality=55) combined.seek(0) return event.msg.reply('', attachments=[('emoji.png', combined)]) @Plugin.command('seen', '<user:user>', global_=True) def seen(self, event, user): try: msg = Message.select(Message.timestamp).where( Message.author_id == user.id ).order_by(Message.timestamp.desc()).limit(1).get() except Message.DoesNotExist: return event.msg.reply(u"I've never seen {}".format(user)) event.msg.reply(u'I last saw {} {} ago (at {})'.format( user, humanize_duration(datetime.utcnow() - msg.timestamp), msg.timestamp )) @Plugin.command('search', '<query:str...>', global_=True) def search(self, event, query): queries = [] if query.isdigit(): queries.append((User.user_id == query)) q = USER_MENTION_RE.findall(query) if len(q) and q[0].isdigit(): queries.append((User.user_id == q[0])) else: queries.append((User.username ** u'%{}%'.format(query.replace('%', '')))) if '#' in query: username, discrim = query.rsplit('#', 1) if discrim.isdigit(): queries.append(( (User.username == username) & (User.discriminator == int(discrim)))) users = User.select().where(reduce(operator.or_, queries)) if len(users) == 0: return event.msg.reply(u'No users found for query `{}`'.format(S(query, escape_codeblocks=True))) if len(users) == 1: if users[0].user_id in self.state.users: return self.info(event, self.state.users.get(users[0].user_id)) return event.msg.reply(u'Found the following users for your query: ```{}```'.format( u'\n'.join(map(lambda i: u'{} ({})'.format(unicode(i), i.user_id), users[:25])) )) @Plugin.command('server', '[guild_id:snowflake]', global_=True) def server(self, event, guild_id=None): guild = self.state.guilds.get(guild_id) if guild_id else event.guild if not guild: raise CommandFail('invalid server') content = [] content.append(u'**\u276F Server Information**') created_at = to_datetime(guild.id) content.append(u'Created: {} ago ({})'.format( humanize.naturaldelta(datetime.utcnow() - created_at), created_at.isoformat(), )) content.append(u'Members: {}'.format(len(guild.members))) content.append(u'Features: {}'.format(', '.join(guild.features) or 'none')) content.append(u'\n**\u276F Counts**') text_count = sum(1 for c in guild.channels.values() if not c.is_voice) voice_count = len(guild.channels) - text_count content.append(u'Roles: {}'.format(len(guild.roles))) content.append(u'Text: {}'.format(text_count)) content.append(u'Voice: {}'.format(voice_count)) content.append(u'\n**\u276F Members**') status_counts = defaultdict(int) for member in guild.members.values(): if not member.user.presence: status = Status.OFFLINE else: status = member.user.presence.status status_counts[status] += 1 for status, count in sorted(status_counts.items(), key=lambda i: str(i[0]), reverse=True): content.append(u'<{}> - {}'.format( STATUS_EMOJI[status], count )) embed = MessageEmbed() if guild.icon: embed.set_thumbnail(url=guild.icon_url) embed.color = get_dominant_colors_guild(guild) embed.description = '\n'.join(content) event.msg.reply('', embed=embed) @Plugin.command('info', '<user:user|snowflake>') def info(self, event, user): if isinstance(user, (int, long)): try: r = self.bot.client.api.http(Routes.USERS_GET, dict(user=user)) # hacky method cause this old version of Disco doesn't have a method for this and we're too lazy to update data = r.json() User = namedtuple('User', [ 'avatar', 'discriminator', 'id', 'username', 'presence' ]) user = User( avatar=data["avatar"], discriminator=data["discriminator"], id=int(data["id"]), username=data["username"], presence=None ) except APIException as e: raise CommandFail('invalid user') content = [] content.append(u'**\u276F User Information**') content.append(u'ID: {}'.format(user.id)) content.append(u'Profile: <@{}>'.format(user.id)) if user.presence: emoji, status = get_status_emoji(user.presence) content.append('Status: {} <{}>'.format(status, emoji)) if user.presence.game and user.presence.game.name: if user.presence.game.type == GameType.DEFAULT: content.append(u'Game: {}'.format(user.presence.game.name)) else: content.append(u'Stream: [{}]({})'.format(user.presence.game.name, user.presence.game.url)) created_dt = to_datetime(user.id) content.append('Created: {} ago ({})'.format( humanize.naturaldelta(datetime.utcnow() - created_dt), created_dt.isoformat() )) member = event.guild.get_member(user.id) if event.guild else None if member: content.append(u'\n**\u276F Member Information**') if member.nick: content.append(u'Nickname: {}'.format(member.nick)) content.append('Joined: {} ago ({})'.format( humanize.naturaldelta(datetime.utcnow() - member.joined_at), member.joined_at.isoformat(), )) if member.roles: content.append(u'Roles: {}'.format( ', '.join((member.guild.roles.get(r).name for r in member.roles)) )) # Execute a bunch of queries async newest_msg = Message.select(Message.timestamp).where( (Message.author_id == user.id) & (Message.guild_id == event.guild.id) ).limit(1).order_by(Message.timestamp.desc()).async() oldest_msg = Message.select(Message.timestamp).where( (Message.author_id == user.id) & (Message.guild_id == event.guild.id) ).limit(1).order_by(Message.timestamp.asc()).async() infractions = Infraction.select( Infraction.guild_id, fn.COUNT('*') ).where( (Infraction.user_id == user.id) ).group_by(Infraction.guild_id).tuples().async() voice = GuildVoiceSession.select( GuildVoiceSession.user_id, fn.COUNT('*'), fn.SUM(GuildVoiceSession.ended_at - GuildVoiceSession.started_at) ).where( (GuildVoiceSession.user_id == user.id) & (~(GuildVoiceSession.ended_at >> None)) ).group_by(GuildVoiceSession.user_id).tuples().async() # Wait for them all to complete (we're still going to be as slow as the # slowest query, so no need to be smart about this.) wait_many(newest_msg, oldest_msg, infractions, voice, timeout=10) tags = to_tags(guild_id=event.msg.guild.id) if newest_msg.value and oldest_msg.value: statsd.timing('sql.duration.newest_msg', newest_msg.value._query_time, tags=tags) statsd.timing('sql.duration.oldest_msg', oldest_msg.value._query_time, tags=tags) newest_msg = newest_msg.value.get() oldest_msg = oldest_msg.value.get() content.append(u'\n **\u276F Activity**') content.append('Last Message: {} ago ({})'.format( humanize_duration(datetime.utcnow() - newest_msg.timestamp), newest_msg.timestamp.isoformat(), )) content.append('First Message: {} ago ({})'.format( humanize_duration(datetime.utcnow() - oldest_msg.timestamp), oldest_msg.timestamp.isoformat(), )) if infractions.value: statsd.timing('sql.duration.infractions', infractions.value._query_time, tags=tags) infractions = list(infractions.value) total = sum(i[1] for i in infractions) content.append(u'\n**\u276F Infractions**') content.append('Total Infractions: {}'.format(total)) content.append('Unique Servers: {}'.format(len(infractions))) if voice.value: statsd.timing('plugin.utilities.info.sql.voice', voice.value._query_time, tags=tags) voice = list(voice.value) content.append(u'\n**\u276F Voice**') content.append(u'Sessions: {}'.format(voice[0][1])) content.append(u'Time: {}'.format(humanize.naturaldelta( voice[0][2] ))) embed = MessageEmbed() avatar = u'https://cdn.discordapp.com/avatars/{}/{}.png'.format( user.id, user.avatar, ) embed.set_author(name=u'{}#{}'.format( user.username, user.discriminator, ), icon_url=avatar) embed.set_thumbnail(url=avatar) embed.description = '\n'.join(content) embed.color = get_dominant_colors_user(user, avatar) event.msg.reply('', embed=embed) def trigger_reminders(self): reminders = Reminder.with_message_join().where( (Reminder.remind_at < (datetime.utcnow() + timedelta(seconds=1))) ) waitables = [] for reminder in reminders: waitables.append(self.spawn(self.trigger_reminder, reminder)) for waitable in waitables: waitable.join() self.queue_reminders() def trigger_reminder(self, reminder): message = reminder.message_id channel = self.state.channels.get(message.channel_id) if not channel: self.log.warning('Not triggering reminder, channel %s was not found!', message.channel_id) reminder.delete_instance() return msg = channel.send_message(u'<@{}> you asked me at {} ({} ago) to remind you about: {}'.format( message.author_id, reminder.created_at, humanize_duration(reminder.created_at - datetime.utcnow()), S(reminder.content) )) # Add the emoji options msg.add_reaction(SNOOZE_EMOJI) msg.add_reaction(GREEN_TICK_EMOJI) try: mra_event = self.wait_for_event( 'MessageReactionAdd', message_id=msg.id, conditional=lambda e: ( (e.emoji.name == SNOOZE_EMOJI or e.emoji.id == GREEN_TICK_EMOJI_ID) and e.user_id == message.author_id ) ).get(timeout=30) except gevent.Timeout: reminder.delete_instance() return finally: # Cleanup msg.delete_reaction(SNOOZE_EMOJI) msg.delete_reaction(GREEN_TICK_EMOJI) if mra_event.emoji.name == SNOOZE_EMOJI: reminder.remind_at = datetime.utcnow() + timedelta(minutes=20) reminder.save() msg.edit(u'Ok, I\'ve snoozed that reminder for 20 minutes.') return reminder.delete_instance() @Plugin.command('delete', '[reminder:str]', group='reminder', global_=True) @Plugin.command('remove', '[reminder:str]', group='reminder', global_=True) @Plugin.command('clean', '[reminder:str]', group='reminder', global_=True) @Plugin.command('clear', '[reminder:str]', group='reminder', global_=True) @Plugin.command('delete', '[reminder:str]', group='r', global_=True) @Plugin.command('remove', '[reminder:str]', group='r', global_=True) @Plugin.command('clean', '[reminder:str]', group='r', global_=True) @Plugin.command('clear', '[reminder:str]', group='r', global_=True) def cmd_remind_clear(self, event, reminder='all'): if reminder == 'all': count = Reminder.count_for_user(event.author.id) if Reminder.count_for_user(event.author.id) == 0: return event.msg.reply('<:{}> cannot clear reminders when you don\'t have any'.format(RED_TICK_EMOJI)) msg = event.msg.reply('Ok, clear {} reminders?'.format(count)) msg.chain(False).\ add_reaction(GREEN_TICK_EMOJI).\ add_reaction(RED_TICK_EMOJI) try: mra_event = self.wait_for_event( 'MessageReactionAdd', message_id=msg.id, conditional=lambda e: ( e.emoji.id in (GREEN_TICK_EMOJI_ID, RED_TICK_EMOJI_ID) and e.user_id == event.author.id )).get(timeout=10) except gevent.Timeout: return finally: msg.delete() if mra_event.emoji.id != GREEN_TICK_EMOJI_ID: return count = Reminder.delete_all_for_user(event.author.id) return event.msg.reply(':ok_hand: I cleared {} reminders for you'.format(count)) else: try: # stupid catch because python sucks try: reminder = int(reminder) except: return event.msg.reply('cannot convert `{}` to `int`'.format(S(reminder))) r = Reminder.select(Reminder).where( (Reminder.message_id << Reminder.with_message_join((Message.id, )).where( Message.author_id == event.author.id )) & (Reminder.id == reminder) ).get() except Reminder.DoesNotExist: return event.msg.reply('<:{}> cannot find reminder #{}'.format(RED_TICK_EMOJI, reminder)) msg = event.msg.reply('Ok, clear reminder #{}?'.format(reminder)) msg.chain(False).\ add_reaction(GREEN_TICK_EMOJI).\ add_reaction(RED_TICK_EMOJI) try: mra_event = self.wait_for_event( 'MessageReactionAdd', message_id=msg.id, conditional=lambda e: ( e.emoji.id in (GREEN_TICK_EMOJI_ID, RED_TICK_EMOJI_ID) and e.user_id == event.author.id )).get(timeout=10) except gevent.Timeout: return finally: msg.delete() if mra_event.emoji.id != GREEN_TICK_EMOJI_ID: return Reminder.delete_for_user(event.author.id, r.id) return event.msg.reply(':ok_hand: I cleared reminder #{} for you'.format(r.id)) @Plugin.command('add', '<duration:str> <content:str...>', group='r', global_=True) @Plugin.command('remind', '<duration:str> <content:str...>', global_=True) def cmd_remind(self, event, duration, content): if Reminder.count_for_user(event.author.id) > 30: return event.msg.reply(':warning: you can only have 15 reminders going at once!') remind_at = parse_duration(duration) if remind_at > (datetime.utcnow() + timedelta(seconds=5 * YEAR_IN_SEC)): return event.msg.reply(':warning: thats too far in the future, I\'ll forget!') r = Reminder.create( message_id=event.msg.id, remind_at=remind_at, content=content ) self.reminder_task.set_next_schedule(r.remind_at) event.msg.reply(':ok_hand: I\'ll remind you at {} ({}) #{}'.format( r.remind_at.isoformat(), humanize_duration(r.remind_at - datetime.utcnow()), r.id )) # @Plugin.command('list global', '[count:int]', context={'mode': 'global'}, group='r', global_=True) @Plugin.command('list', '[limit:int]', context={'mode': 'server'}, group='r', global_=True) @Plugin.command('list', '[limit:int]', context={'mode': 'server'}, group='remind', global_=True) # @Plugin.command('reminders global', '[count:int]', context={'mode': 'global'}, global_=True) @Plugin.command('reminders', '[limit:int]', context={'mode': 'server'}, global_=True) def cmd_remind_list(self, event, limit=None, mode=None): user = event.msg.author count = Reminder.count_for_user(user.id) avatar = u'https://cdn.discordapp.com/avatars/{}/{}.png'.format( user.id, user.avatar, ) embed = MessageEmbed() embed.title = '{} reminder{}'.format(count, '' if count == 1 else 's') embed.set_author(name=u'{}#{}'.format( user.username, user.discriminator, ), icon_url=avatar) embed.color = get_dominant_colors_user(user, avatar) embed.set_footer(text='You can cancel reminders with !r clear [ID]') if count == 0: embed.description = 'You have no upcoming reminders.' else: query = Reminder.select(Reminder).where( (Reminder.message_id << Reminder.with_message_join((Message.id, )).where( Message.author_id == event.author.id )) & (Reminder.remind_at > (datetime.utcnow() + timedelta(seconds=1))) ).order_by(Reminder.remind_at).limit(limit) for reminder in query: time = humanize_duration(reminder.remind_at - datetime.utcnow()) channel = Message.select().where(Message.id == reminder.message_id).get().channel_id channel = self.state.channels.get(channel) embed.add_field( name=u'#{} in {}'.format( reminder.id, time ), value=u'[`#{}`](https://discordapp.com/channels/{}/{}/{}) {}'.format( channel.name if channel.type != ChannelType.DM else 'Jetski', channel.guild_id if channel.type != ChannelType.DM else '@me', channel.id, reminder.message_id, S(reminder.content) ) ) return event.msg.reply(embed=embed)
class UtilitiesPlugin(Plugin): def load(self, ctx): super(UtilitiesPlugin, self).load(ctx) self.reminder_task = Eventual(self.trigger_reminders) self.spawn_later(10, self.queue_reminders) def queue_reminders(self): try: next_reminder = Reminder.select().order_by( Reminder.remind_at.asc()).limit(1).get() except Reminder.DoesNotExist: return self.reminder_task.set_next_schedule(next_reminder.remind_at) @Plugin.command('coin', group='random', global_=True) def coin(self, event): """ Flip a coin """ event.msg.reply(random.choice(['heads', 'tails'])) @Plugin.command('number', '[end:int] [start:int]', group='random', global_=True) def random_number(self, event, end=10, start=0): """ Returns a random number """ # Because someone will be an idiot if end > 9223372036854775807: return event.msg.reply(':warning: ending number too big!') if end <= start: return event.msg.reply( ':warning: ending number must be larger than starting number!') event.msg.reply(str(random.randint(start, end))) @Plugin.command('cat', global_=True) def cat(self, event): # Sometimes random.cat gives us gifs (smh) for _ in range(3): try: r = requests.get('http://random.cat/meow') r.raise_for_status() except: continue url = r.json()['file'] if not url.endswith('.gif'): break else: return event.msg.reply('404 cat not found :(') r = requests.get(url) r.raise_for_status() event.msg.reply('', attachments=[('cat.jpg', r.content)]) @Plugin.command('emoji', '<emoji:str>', global_=True) def emoji(self, event, emoji): if not EMOJI_RE.match(emoji): return event.msg.reply(u'Unknown emoji: `{}`'.format(emoji)) fields = [] name, eid = EMOJI_RE.findall(emoji)[0] fields.append('**ID:** {}'.format(eid)) fields.append('**Name:** {}'.format(S(name))) guild = self.state.guilds.find_one(lambda v: eid in v.emojis) if guild: fields.append('**Guild:** {} ({})'.format(S(guild.name), guild.id)) url = 'https://discordapp.com/api/emojis/{}.png'.format(eid) r = requests.get(url) r.raise_for_status() return event.msg.reply('\n'.join(fields), attachments=[('emoji.png', r.content)]) @Plugin.command('jumbo', '<emojis:str...>', global_=True) def jumbo(self, event, emojis): urls = [] for emoji in emojis.split(' ')[:5]: if EMOJI_RE.match(emoji): _, eid = EMOJI_RE.findall(emoji)[0] urls.append( 'https://discordapp.com/api/emojis/{}.png'.format(eid)) else: urls.append(get_emoji_url(emoji)) width, height, images = 0, 0, [] for r in Pool(6).imap(requests.get, urls): try: r.raise_for_status() except requests.HTTPError: return img = Image.open(BytesIO(r.content)) height = img.height if img.height > height else height width += img.width + 10 images.append(img) image = Image.new('RGBA', (width, height)) width_offset = 0 for img in images: image.paste(img, (width_offset, 0)) width_offset += img.width + 10 combined = BytesIO() image.save(combined, 'png', quality=55) combined.seek(0) return event.msg.reply('', attachments=[('emoji.png', combined)]) @Plugin.command('seen', '<user:user>', global_=True) def seen(self, event, user): try: msg = Message.select(Message.timestamp).where( Message.author_id == user.id).order_by( Message.timestamp.desc()).limit(1).get() except Message.DoesNotExist: return event.msg.reply(u"I've never seen {}".format(user)) event.msg.reply(u'I last saw {} {} ago (at {})'.format( user, humanize.naturaldelta(datetime.utcnow() - msg.timestamp), msg.timestamp)) @Plugin.command('search', '<query:str...>', global_=True) def search(self, event, query): queries = [] if query.isdigit(): queries.append((User.user_id == query)) q = USER_MENTION_RE.findall(query) if len(q) and q[0].isdigit(): queries.append((User.user_id == q[0])) else: queries.append( (User.username**u'%{}%'.format(query.replace('%', '')))) if '#' in query: username, discrim = query.rsplit('#', 1) if discrim.isdigit(): queries.append(((User.username == username) & (User.discriminator == int(discrim)))) users = User.select().where(reduce(operator.or_, queries)) if len(users) == 0: return event.msg.reply(u'No users found for query `{}`'.format( S(query, escape_codeblocks=True))) if len(users) == 1: if users[0].user_id in self.state.users: return self.info(event, self.state.users.get(users[0].user_id)) return event.msg.reply( u'Found the following users for your query: ```{}```'.format( u'\n'.join( map(lambda i: u'{} ({})'.format(unicode(i), i.user_id), users[:25])))) @Plugin.command('server', '[guild_id:snowflake]', global_=True) def server(self, event, guild_id=None): guild = self.state.guilds.get(guild_id) if guild_id else event.guild if not guild: raise CommandFail('invalid server') content = [] content.append(u'**\u276F Server Information**') created_at = to_datetime(guild.id) content.append(u'Created: {} ago ({})'.format( humanize.naturaldelta(datetime.utcnow() - created_at), created_at.isoformat(), )) content.append(u'Members: {}'.format(len(guild.members))) content.append(u'Features: {}'.format(', '.join(guild.features) or 'none')) content.append(u'\n**\u276F Counts**') text_count = sum(1 for c in guild.channels.values() if not c.is_voice) voice_count = len(guild.channels) - text_count content.append(u'Roles: {}'.format(len(guild.roles))) content.append(u'Text: {}'.format(text_count)) content.append(u'Voice: {}'.format(voice_count)) content.append(u'\n**\u276F Members**') status_counts = defaultdict(int) for member in guild.members.values(): if not member.user.presence: status = Status.OFFLINE else: status = member.user.presence.status status_counts[status] += 1 for status, count in sorted(status_counts.items(), key=lambda i: str(i[0]), reverse=True): content.append(u'<{}> - {}'.format(STATUS_EMOJI[status], count)) embed = MessageEmbed() if guild.icon: embed.set_thumbnail(url=guild.icon_url) embed.color = get_dominant_colors_guild(guild) embed.description = '\n'.join(content) event.msg.reply('', embed=embed) @Plugin.command('info', '<user:user>') def info(self, event, user): content = [] content.append(u'**\u276F User Information**') content.append(u'ID: {}'.format(user.id)) content.append(u'Profile: <@{}>'.format(user.id)) if user.presence: emoji, status = get_status_emoji(user.presence) content.append('Status: {} <{}>'.format(status, emoji)) if user.presence.game and user.presence.game.name: if user.presence.game.type == GameType.DEFAULT: content.append(u'Game: {}'.format(user.presence.game.name)) else: content.append(u'Stream: [{}]({})'.format( user.presence.game.name, user.presence.game.url)) created_dt = to_datetime(user.id) content.append('Created: {} ago ({})'.format( humanize.naturaldelta(datetime.utcnow() - created_dt), created_dt.isoformat())) member = event.guild.get_member(user.id) if event.guild else None if member: content.append(u'\n**\u276F Member Information**') if member.nick: content.append(u'Nickname: {}'.format(member.nick)) content.append('Joined: {} ago ({})'.format( humanize.naturaldelta(datetime.utcnow() - member.joined_at), member.joined_at.isoformat(), )) if member.roles: content.append(u'Roles: {}'.format(', '.join( (member.guild.roles.get(r).name for r in member.roles)))) # Execute a bunch of queries async infractions = Infraction.select( Infraction.guild_id, fn.COUNT('*')).where( (Infraction.user_id == user.id)).group_by( Infraction.guild_id).tuples(). async () # Wait for them all to complete (we're still going to be as slow as the # slowest query, so no need to be smart about this.) wait_many(infractions, timeout=10) tags = to_tags(guild_id=event.msg.guild.id) if infractions.value: statsd.timing('sql.duration.infractions', infractions.value._query_time, tags=tags) infractions = list(infractions.value) total = sum(i[1] for i in infractions) content.append(u'\n**\u276F Infractions**') content.append('Total Infractions: {}'.format(total)) content.append('Unique Servers: {}'.format(len(infractions))) embed = MessageEmbed() avatar = u'https://cdn.discordapp.com/avatars/{}/{}.png'.format( user.id, user.avatar, ) embed.set_author(name=u'{}#{}'.format( user.username, user.discriminator, ), icon_url=avatar) embed.set_thumbnail(url=avatar) embed.description = '\n'.join(content) embed.color = get_dominant_colors_user(user, avatar) event.msg.reply('', embed=embed) def trigger_reminders(self): reminders = Reminder.with_message_join().where( (Reminder.remind_at < (datetime.utcnow() + timedelta(seconds=1)))) waitables = [] for reminder in reminders: waitables.append(self.spawn(self.trigger_reminder, reminder)) for waitable in waitables: waitable.join() self.queue_reminders() def trigger_reminder(self, reminder): message = reminder.message_id channel = self.state.channels.get(message.channel_id) if not channel: self.log.warning( 'Not triggering reminder, channel %s was not found!', message.channel_id) reminder.delete_instance() return msg = channel.send_message( u'<@{}> you asked me at {} ({} ago) to remind you about: {}'. format( message.author_id, reminder.created_at, humanize.naturaldelta(reminder.created_at - datetime.utcnow()), S(reminder.content))) # Add the emoji options msg.add_reaction(SNOOZE_EMOJI) msg.add_reaction(GREEN_TICK_EMOJI) try: mra_event = self.wait_for_event( 'MessageReactionAdd', message_id=msg.id, conditional=lambda e: ((e.emoji.name == SNOOZE_EMOJI or e.emoji.id == GREEN_TICK_EMOJI_ID) and e.user_id == message.author_id )).get(timeout=30) except gevent.Timeout: reminder.delete_instance() return finally: # Cleanup msg.delete_reaction(SNOOZE_EMOJI) msg.delete_reaction(GREEN_TICK_EMOJI) if mra_event.emoji.name == SNOOZE_EMOJI: reminder.remind_at = datetime.utcnow() + timedelta(minutes=20) reminder.save() msg.edit(u'Ok, I\'ve snoozed that reminder for 20 minutes.') return reminder.delete_instance() @Plugin.command('clear', group='r', global_=True) def cmd_remind_clear(self, event): count = Reminder.delete_for_user(event.author.id) return event.msg.reply( ':ok_hand: I cleared {} reminders for you'.format(count)) @Plugin.command('add', '<duration:str> <content:str...>', group='r', global_=True) @Plugin.command('remind', '<duration:str> <content:str...>', global_=True) def cmd_remind(self, event, duration, content): if Reminder.count_for_user(event.author.id) > 30: return event.msg.reply( ':warning: you an only have 15 reminders going at once!') remind_at = parse_duration(duration) if remind_at > (datetime.utcnow() + timedelta(seconds=5 * YEAR_IN_SEC)): return event.msg.reply( ':warning: thats too far in the future, I\'ll forget!') r = Reminder.create(message_id=event.msg.id, remind_at=remind_at, content=content) self.reminder_task.set_next_schedule(r.remind_at) event.msg.reply(':ok_hand: I\'ll remind you at {} ({})'.format( r.remind_at.isoformat(), humanize.naturaldelta(r.remind_at - datetime.utcnow()), ))
class UtilitiesPlugin(Plugin): def load(self, ctx): super(UtilitiesPlugin, self).load(ctx) self.reminder_task = Eventual(self.trigger_reminders) self.spawn_later(10, self.queue_reminders) def queue_reminders(self): try: next_reminder = Reminder.select().order_by( Reminder.remind_at.asc()).limit(1).get() except Reminder.DoesNotExist: return self.reminder_task.set_next_schedule(next_reminder.remind_at) @Plugin.command('coin', group='random', global_=True) def coin(self, event): """ Flip a coin """ event.msg.reply(random.choice(['heads', 'tails'])) @Plugin.command('number', '[end:int] [start:int]', group='random', global_=True) def random_number(self, event, end=10, start=0): """ Returns a random number """ # Because someone will be an idiot if end > 9223372036854775807: return event.msg.reply(':warning: ending number too big!') if end <= start: return event.msg.reply( ':warning: ending number must be larger than starting number!') event.msg.reply(str(random.randint(start, end))) @Plugin.command('cat', global_=True) def cat(self, event): # Sometimes random.cat gives us gifs (smh) for _ in range(3): try: r = requests.get('http://random.cat/meow') r.raise_for_status() except: continue url = r.json()['file'] if not url.endswith('.gif'): break else: return event.msg.reply('404 cat not found :(') r = requests.get(url) r.raise_for_status() event.msg.reply('', attachments=[('cat.jpg', r.content)]) @Plugin.command('urban', '<term:str...>', global_=True) def urban(self, event, term): r = requests.get('http://api.urbandictionary.com/v0/define', params={ 'term': term, }) r.raise_for_status() data = r.json() if not len(data['list']): return event.msg.reply(':warning: no matches') event.msg.reply('{} - {}'.format( data['list'][0]['word'], data['list'][0]['definition'], )) @Plugin.command('pwnd', '<email:str>', global_=True) def pwnd(self, event, email): r = requests.get( 'https://haveibeenpwned.com/api/v2/breachedaccount/{}'.format( email)) if r.status_code == 404: return event.msg.reply( ":white_check_mark: you haven't been pwnd yet, awesome!") r.raise_for_status() data = r.json() sites = [] for idx, site in enumerate(data): sites.append('{} - {} ({})'.format( site['BreachDate'], site['Title'], site['Domain'], )) return event.msg.reply( ":warning: You've been pwnd on {} sites:\n{}".format( len(sites), '\n'.join(sites), )) @Plugin.command('geoip', '<ip:str>', global_=True) def geoip(self, event, ip): r = requests.get('http://json.geoiplookup.io/{}'.format(ip)) r.raise_for_status() data = r.json() event.msg.reply(u'{} - {}, {} ({}) | {}, {}'.format( data['isp'], data['city'], data['region'], data['country_code'], data['latitude'], data['longitude'], )) @Plugin.command('emoji', '<emoji:str>', global_=True) def emoji(self, event, emoji): if not EMOJI_RE.match(emoji): return event.msg.reply(u'Unknown emoji: `{}`'.format(emoji)) fields = [] name, eid = EMOJI_RE.findall(emoji)[0] fields.append('**ID:** {}'.format(eid)) fields.append('**Name:** {}'.format(name)) guild = self.state.guilds.find_one(lambda v: eid in v.emojis) if guild: fields.append('**Guild:** {} ({})'.format(guild.name, guild.id)) url = 'https://discordapp.com/api/emojis/{}.png'.format(eid) r = requests.get(url) r.raise_for_status() return event.msg.reply('\n'.join(fields), attachments=[('emoji.png', r.content)]) @Plugin.command('jumbo', '<emojis:str...>', global_=True) def jumbo(self, event, emojis): urls = [] for emoji in emojis.split(' ')[:5]: if EMOJI_RE.match(emoji): _, eid = EMOJI_RE.findall(emoji)[0] urls.append( 'https://discordapp.com/api/emojis/{}.png'.format(eid)) else: urls.append(get_emoji_url(emoji)) width, height, images = 0, 0, [] for r in Pool(6).imap(requests.get, urls): try: r.raise_for_status() except requests.HTTPError: return img = Image.open(BytesIO(r.content)) height = img.height if img.height > height else height width += img.width + 10 images.append(img) image = Image.new('RGBA', (width, height)) width_offset = 0 for img in images: image.paste(img, (width_offset, 0)) width_offset += img.width + 10 combined = BytesIO() image.save(combined, 'png', quality=55) combined.seek(0) return event.msg.reply('', attachments=[('emoji.png', combined)]) @Plugin.command('seen', '<user:user>', global_=True) def seen(self, event, user): try: msg = Message.select(Message.timestamp).where( Message.author_id == user.id).order_by( Message.timestamp.desc()).limit(1).get() except Message.DoesNotExist: return event.msg.reply(u"I've never seen {}".format(user)) event.msg.reply(u'I last saw {} {} ({})'.format( user, humanize.naturaldelta(datetime.utcnow() - msg.timestamp), msg.timestamp)) @Plugin.command('search', '<query:str...>', global_=True) def search(self, event, query): queries = [] if query.isdigit(): queries.append((User.user_id == query)) q = USER_MENTION_RE.findall(query) if len(q) and q[0].isdigit(): queries.append((User.user_id == q[0])) else: queries.append( (User.username**u'%{}%'.format(query.replace('%', '')))) if '#' in query: username, discrim = query.rsplit('#', 1) if discrim.isdigit(): queries.append(((User.username == username) & (User.discriminator == int(discrim)))) users = User.select().where(reduce(operator.or_, queries)) if len(users) == 0: return event.msg.reply(u'No users found for query `{}`'.format( S(query, escape_codeblocks=True))) if len(users) == 1: if users[0].user_id in self.state.users: return self.info(event, self.state.users.get(users[0].user_id)) return event.msg.reply( u'Found the following users for your query: ```{}```'.format( u'\n'.join( map(lambda i: u'{} ({})'.format(unicode(i), i.user_id), users[:25])))) @Plugin.command('server', '[guild_id:snowflake]', global_=True) def server(self, event, guild_id=None): guild = self.state.guilds.get(guild_id) if guild_id else event.guild if not guild: raise CommandFail('invalid server') content = [] content.append(u'**\u276F Server Information**') created_at = to_datetime(guild.id) content.append(u'Created: {} ago ({})'.format( humanize.naturaldelta(datetime.utcnow() - created_at), created_at.isoformat(), )) content.append(u'Members: {}'.format(len(guild.members))) content.append(u'Features: {}'.format(', '.join(guild.features) or 'none')) content.append(u'\n**\u276F Counts**') text_count = sum(1 for c in guild.channels.values() if not c.is_voice) voice_count = len(guild.channels) - text_count content.append(u'Roles: {}'.format(len(guild.roles))) content.append(u'Text: {}'.format(text_count)) content.append(u'Voice: {}'.format(voice_count)) content.append(u'\n**\u276F Members**') status_counts = defaultdict(int) for member in guild.members.values(): if not member.user.presence: status = Status.OFFLINE else: status = member.user.presence.status status_counts[status] += 1 for status, count in sorted(status_counts.items(), key=lambda i: str(i[0]), reverse=True): content.append(u'<{}> - {}'.format(STATUS_EMOJI[status], count)) embed = MessageEmbed() if guild.icon: embed.set_thumbnail(url=guild.icon_url) embed.color = get_dominant_colors_guild(guild) embed.description = '\n'.join(content) event.msg.reply('', embed=embed) @Plugin.command('info', '<user:user>', global_=True) def info(self, event, user): content = [] content.append(u'**\u276F User Information**') if user.presence: emoji, status = get_status_emoji(user.presence) content.append('Status: {} <{}>'.format(status, emoji)) if user.presence.game and user.presence.game.name: if user.presence.game.type == GameType.DEFAULT: content.append(u'Game: {}'.format(user.presence.game.name)) else: content.append(u'Stream: [{}]({})'.format( user.presence.game.name, user.presence.game.url)) created_dt = to_datetime(user.id) content.append('Created: {} ago ({})'.format( humanize.naturaldelta(datetime.utcnow() - created_dt), created_dt.isoformat())) member = event.guild.get_member(user.id) if event.guild else None if member: content.append(u'\n**\u276F Member Information**') if member.nick: content.append(u'Nickname: {}'.format(member.nick)) content.append('Joined: {} ago ({})'.format( humanize.naturaldelta(datetime.utcnow() - member.joined_at), member.joined_at.isoformat(), )) if member.roles: content.append(u'Roles: {}'.format(', '.join( (member.guild.roles.get(r).name for r in member.roles)))) try: newest_msg = Message.select(Message.timestamp).where( (Message.author_id == user.id) & (Message.guild_id == event.guild.id)).order_by( Message.timestamp.desc()).get() oldest_msg = Message.select(Message.timestamp).where( (Message.author_id == user.id) & (Message.guild_id == event.guild.id)).order_by( Message.timestamp.asc()).get() content.append(u'\n **\u276F Activity**') content.append('Last Message: {} ago ({})'.format( humanize.naturaldelta(datetime.utcnow() - newest_msg.timestamp), newest_msg.timestamp.isoformat(), )) content.append('First Message: {} ago ({})'.format( humanize.naturaldelta(datetime.utcnow() - oldest_msg.timestamp), oldest_msg.timestamp.isoformat(), )) except Message.DoesNotExist: pass infractions = list( Infraction.select(Infraction.guild_id, fn.COUNT('*')).where( (Infraction.user_id == user.id)).group_by( Infraction.guild_id).tuples()) if infractions: total = sum(i[1] for i in infractions) content.append(u'\n**\u276F Infractions**') content.append('Total Infractions: {}'.format(total)) content.append('Unique Servers: {}'.format(len(infractions))) voice = list( GuildVoiceSession.select( GuildVoiceSession.user_id, fn.COUNT('*'), fn.SUM(GuildVoiceSession.ended_at - GuildVoiceSession.started_at)).where( (GuildVoiceSession.user_id == user.id) & (~(GuildVoiceSession.ended_at >> None))).group_by( GuildVoiceSession.user_id).tuples()) if voice: content.append(u'\n**\u276F Voice**') content.append(u'Sessions: {}'.format(voice[0][1])) content.append(u'Time: {}'.format( humanize.naturaldelta(voice[0][2]))) embed = MessageEmbed() avatar = u'https://cdn.discordapp.com/avatars/{}/{}.png'.format( user.id, user.avatar, ) embed.set_author(name=u'{}#{} (<@{}>)'.format( user.username, user.discriminator, user.id, ), icon_url=avatar) embed.set_thumbnail(url=avatar) embed.description = '\n'.join(content) embed.color = get_dominant_colors_user(user, avatar) event.msg.reply('', embed=embed) def trigger_reminders(self): reminders = Reminder.with_message_join().where( (Reminder.remind_at < (datetime.utcnow() + timedelta(seconds=1)))) for reminder in reminders: message = reminder.message_id channel = self.state.channels.get(message.channel_id) if not channel: self.log.warning( 'Not triggering reminder, channel %s was not found!', message.channel_id) continue channel.send_message( u'<@{}> you asked me at {} ({} ago) to remind you about: {}'. format( message.author_id, reminder.created_at, humanize.naturaldelta(reminder.created_at - datetime.utcnow()), S(reminder.content))) reminder.delete_instance() self.queue_reminders() @Plugin.command('clear', group='r', global_=True) def cmd_remind_clear(self, event): count = Reminder.delete_for_user(event.author.id) return event.msg.reply( ':ok_hand: I cleared {} reminders for you'.format(count)) @Plugin.command('add', '<duration:str> <content:str...>', group='r', global_=True) @Plugin.command('remind', '<duration:str> <content:str...>', global_=True) def cmd_remind(self, event, duration, content): if Reminder.count_for_user(event.author.id) > 30: return event.msg.reply( ':warning: you an only have 15 reminders going at once!') remind_at = parse_duration(duration) if remind_at > (datetime.utcnow() + timedelta(seconds=5 * YEAR_IN_SEC)): return event.msg.reply( ':warning: thats too far in the future, I\'ll forget!') r = Reminder.create(message_id=event.msg.id, remind_at=remind_at, content=content) self.reminder_task.set_next_schedule(r.remind_at) event.msg.reply(':ok_hand: I\'ll remind you at {} ({})'.format( r.remind_at.isoformat(), humanize.naturaldelta(r.remind_at - datetime.utcnow()), ))
class RemindersPlugin(Plugin): def load(self, ctx): super(RemindersPlugin, self).load(ctx) self.reminder_task = Eventual(self.trigger_reminders) self.spawn_later(10, self.queue_reminders) def queue_reminders(self): try: next_reminder = Reminder.select().order_by( Reminder.remind_at.asc()).limit(1).get() except Reminder.DoesNotExist: return self.reminder_task.set_next_schedule(next_reminder.remind_at) def trigger_reminders(self): reminders = Reminder.with_message_join().where( (Reminder.remind_at < (datetime.utcnow() + timedelta(seconds=1)))) waitables = [] for reminder in reminders: waitables.append(self.spawn(self.trigger_reminder, reminder)) for waitable in waitables: waitable.join() self.queue_reminders() def trigger_reminder(self, reminder): message = reminder.message_id channel = self.bot.client.state.channels.get(message.channel_id) if not channel: self.log.warning( 'Not triggering reminder, channel %s was not found!', message.channel_id) reminder.delete_instance() return msg = channel.send_message( '<@{}> you asked me at {} ({} ago) to remind you about: {}'.format( message.author_id, reminder.created_at, humanize_duration(reminder.created_at - datetime.utcnow()), S(reminder.content))) # Add the emoji options msg.add_reaction(SNOOZE_EMOJI) msg.add_reaction(GREEN_TICK_EMOJI) try: mra_event = self.wait_for_event( 'MessageReactionAdd', message_id=msg.id, conditional=lambda e: ((e.emoji.name == SNOOZE_EMOJI or e.emoji.id == GREEN_TICK_EMOJI_ID) and e.user_id == message.author_id )).get(timeout=30) except gevent.Timeout: reminder.delete_instance() return finally: # Cleanup msg.delete_reaction(SNOOZE_EMOJI) msg.delete_reaction(GREEN_TICK_EMOJI) if mra_event.emoji.name == SNOOZE_EMOJI: reminder.remind_at = datetime.utcnow() + timedelta(minutes=20) reminder.save() msg.edit('Ok, I\'ve snoozed that reminder for 20 minutes.') return reminder.delete_instance() @Plugin.command( 'delete global', '[reminder:str]', group='reminder', aliases=['remove global global', 'clean global', 'clear global'], context={'mode': 'global'}, global_=True) @Plugin.command('delete global', '[reminder:str]', group='r', aliases=['remove global', 'clean', 'clear global'], context={'mode': 'global'}, global_=True) @Plugin.command('delete', '[reminder:str]', group='reminder', aliases=['remove', 'clean', 'clear'], context={'mode': 'server'}, global_=True) @Plugin.command('delete', '[reminder:str]', group='r', aliases=['remove', 'clean', 'clear'], context={'mode': 'server'}, global_=True) def cmd_remind_clear(self, event, reminder='all', mode='server'): if reminder == 'all': count = Reminder.count_for_user( event.author.id, event.guild.id ) if mode == 'server' else Reminder.count_for_user(event.author.id) if Reminder.count_for_user(event.author.id) == 0: return event.msg.reply( '<:{}> cannot clear reminders when you don\'t have any'. format(RED_TICK_EMOJI)) msg = event.msg.reply('Ok, clear {} reminders?'.format(count)) msg.chain(False).\ add_reaction(GREEN_TICK_EMOJI).\ add_reaction(RED_TICK_EMOJI) try: mra_event = self.wait_for_event( 'MessageReactionAdd', message_id=msg.id, conditional=lambda e: (e.emoji.id in (GREEN_TICK_EMOJI_ID, RED_TICK_EMOJI_ID) and e.user_id == event.author.id)).get(timeout=10) except gevent.Timeout: return finally: msg.delete() if mra_event.emoji.id != GREEN_TICK_EMOJI_ID: return count = Reminder.delete_all_for_user( event.author.id, event.guild.id ) if mode == 'server' else Reminder.delete_all_for_user( event.author.id) return event.msg.reply( ':ok_hand: I cleared {} reminders for you'.format(count)) else: try: # stupid catch because python sucks try: reminder = int(reminder) except: return event.msg.reply( 'cannot convert `{}` to `int`'.format(S(reminder))) r = Reminder.select(Reminder).where( (Reminder.message_id << Reminder.with_message_join( (Message.id, )).where( Message.author_id == event.author.id)) & (Reminder.id == reminder)).get() except Reminder.DoesNotExist: return event.msg.reply('<:{}> cannot find reminder #{}'.format( RED_TICK_EMOJI, reminder)) msg = event.msg.reply('Ok, clear reminder #{}?'.format(reminder)) msg.chain(False).\ add_reaction(GREEN_TICK_EMOJI).\ add_reaction(RED_TICK_EMOJI) try: mra_event = self.wait_for_event( 'MessageReactionAdd', message_id=msg.id, conditional=lambda e: (e.emoji.id in (GREEN_TICK_EMOJI_ID, RED_TICK_EMOJI_ID) and e.user_id == event.author.id)).get(timeout=10) except gevent.Timeout: return finally: msg.delete() if mra_event.emoji.id != GREEN_TICK_EMOJI_ID: return Reminder.delete_for_user(event.author.id, r.id) return event.msg.reply( ':ok_hand: I cleared reminder #{} for you'.format(r.id)) @Plugin.command('add', '<duration:str> <content:str...>', group='r', global_=True) @Plugin.command('remind', '<duration:str> <content:str...>', global_=True) def cmd_remind(self, event, duration, content): if Reminder.count_for_user(event.author.id) > 30: return event.msg.reply( ':warning: you can only have 15 reminders going at once!') remind_at = parse_duration(duration) if remind_at > (datetime.utcnow() + timedelta(seconds=5 * YEAR_IN_SEC)): return event.msg.reply( ':warning: thats too far in the future, I\'ll forget!') r = Reminder.create(message_id=event.msg.id, remind_at=remind_at, content=content) self.reminder_task.set_next_schedule(r.remind_at) event.msg.reply(':ok_hand: I\'ll remind you at {} ({}) #{}'.format( r.remind_at.isoformat(), humanize_duration(r.remind_at - datetime.utcnow()), r.id)) @Plugin.command('list global', '[limit:int]', context={'mode': 'global'}, group='r', global_=True) @Plugin.command('list global', '[limit:int]', context={'mode': 'global'}, group='remind', global_=True) @Plugin.command('reminders global', '[limit:int]', context={'mode': 'global'}, global_=True) @Plugin.command('list', '[limit:int]', context={'mode': 'server'}, group='r', global_=True) @Plugin.command('list', '[limit:int]', context={'mode': 'server'}, group='remind', global_=True) @Plugin.command('reminders', '[limit:int]', context={'mode': 'server'}, global_=True) def cmd_remind_list(self, event, limit=None, mode='server'): user = event.msg.author count = Reminder.count_for_user(user.id, event.guild.id) total_count = Reminder.count_for_user(user.id) embed = MessageEmbed() embed.title = '{} reminder{} ({} total)'.format( count if mode == 'server' else total_count, 's' if (count != 1 and mode == 'server') or (total_count != 1 and mode == 'global') else '', total_count) embed.set_author(name=user, icon_url=user.avatar_url) embed.color = get_dominant_colors_user(user, user.get_avatar_url('png')) embed.set_footer(text='You can cancel reminders with !r clear [ID]') if (count == 0 and mode == 'server') or total_count == 0: embed.description = 'You have no upcoming reminders{}.'.format( ' in this server. Use `!r list global` to list all your upcoming reminders' if total_count > 0 else '') else: query = Reminder.select(Reminder).where( (Reminder.message_id << Reminder.with_message_join( (Message.id, )).where( (Message.author_id == event.author.id) & (Message.guild_id == event.guild.id if mode == 'server' else True))) & (Reminder.remind_at > (datetime.utcnow() + timedelta(seconds=1)))).order_by( Reminder.remind_at).limit(limit) for reminder in query: time = humanize_duration(reminder.remind_at - datetime.utcnow()) channel = Message.select().where( Message.id == reminder.message_id).get().channel_id channel = self.bot.client.state.channels.get(channel) embed.add_field( name='#{} in {}'.format(reminder.id, time), value='[`#{}`](https://discordapp.com/channels/{}/{}/{}) {}' .format( channel.name if channel.type != ChannelType.DM else 'Jetski', channel.guild_id if channel.type != ChannelType.DM else '@me', channel.id, reminder.message_id, S(reminder.content))) return event.msg.reply(embed=embed)
class InfractionsPlugin(Plugin): def load(self, ctx): super(InfractionsPlugin, self).load(ctx) self.inf_task = Eventual(self.clear_infractions) self.spawn_later(5, self.queue_infractions) def queue_infractions(self): next_infraction = list(Infraction.select().where( (Infraction.active == 1) & (~(Infraction.expires_at >> None)) ).order_by(Infraction.expires_at.asc()).limit(1)) if not next_infraction: self.log.info('[INF] no infractions to wait for') return self.log.info('[INF] waiting until %s for %s', next_infraction[0].expires_at, next_infraction[0].id) self.inf_task.set_next_schedule(next_infraction[0].expires_at) def clear_infractions(self): expired = list(Infraction.select().where( (Infraction.active == 1) & (Infraction.expires_at < datetime.utcnow()) )) self.log.info('[INF] attempting to clear %s expired infractions', len(expired)) for item in expired: guild = self.state.guilds.get(item.guild_id) if not guild: self.log.warning('[INF] failed to clear infraction %s, no guild exists', item.id) continue # TODO: hacky type_ = {i.index: i for i in Infraction.Types.attrs}[item.type_] if type_ == Infraction.Types.TEMPBAN: self.call( 'ModLogPlugin.create_debounce', guild.id, ['GuildBanRemove'], user_id=item.user_id, ) guild.delete_ban(item.user_id) # TODO: perhaps join on users above and use username from db self.call( 'ModLogPlugin.log_action_ext', Actions.MEMBER_TEMPBAN_EXPIRE, guild.id, user_id=item.user_id, user=unicode(self.state.users.get(item.user_id) or item.user_id), inf=item ) elif type_ == Infraction.Types.TEMPMUTE or Infraction.Types.TEMPROLE: member = guild.get_member(item.user_id) if member: if item.metadata['role'] in member.roles: self.call( 'ModLogPlugin.create_debounce', guild.id, ['GuildMemberUpdate'], user_id=item.user_id, role_id=item.metadata['role'], ) member.remove_role(item.metadata['role']) self.call( 'ModLogPlugin.log_action_ext', Actions.MEMBER_TEMPMUTE_EXPIRE, guild.id, member=member, inf=item ) else: GuildMemberBackup.remove_role( item.guild_id, item.user_id, item.metadata['role']) else: self.log.warning('[INF] failed to clear infraction %s, type is invalid %s', item.id, item.type_) continue # TODO: n+1 item.active = False item.save() # Wait a few seconds to backoff from a possible bad loop, and requeue new infractions gevent.sleep(5) self.queue_infractions() @Plugin.listen('GuildMemberUpdate', priority=Priority.BEFORE) def on_guild_member_update(self, event): pre_member = event.guild.members.get(event.id) if not pre_member: return pre_roles = set(pre_member.roles) post_roles = set(event.roles) if pre_roles == post_roles: return removed = pre_roles - post_roles # If the user was unmuted, mark any temp-mutes as inactive if event.config.mute_role in removed: Infraction.clear_active(event, event.user.id, [Infraction.Types.TEMPMUTE]) @Plugin.listen('GuildBanRemove') def on_guild_ban_remove(self, event): Infraction.clear_active(event, event.user.id, [Infraction.Types.BAN, Infraction.Types.TEMPBAN]) @Plugin.command('unban', '<user:snowflake> [reason:str...]', level=CommandLevels.MOD) def unban(self, event, user, reason=None): try: GuildBan.get(user_id=user, guild_id=event.guild.id) event.guild.delete_ban(user) except GuildBan.DoesNotExist: raise CommandFail('user with id `{}` is not banned'.format(user)) Infraction.create( guild_id=event.guild.id, user_id=user, actor_id=event.author.id, type_=Infraction.Types.UNBAN, reason=reason ) raise CommandSuccess('unbanned user with id `{}`'.format(user)) @Plugin.command('archive', group='infractions', level=CommandLevels.ADMIN) def infractions_archive(self, event): user = User.alias() actor = User.alias() q = Infraction.select(Infraction, user, actor).join( user, on=((Infraction.user_id == user.user_id).alias('user')) ).switch(Infraction).join( actor, on=((Infraction.actor_id == actor.user_id).alias('actor')) ).where(Infraction.guild_id == event.guild.id) buff = StringIO() w = csv.writer(buff) for inf in q: w.writerow([ inf.id, inf.user_id, unicode(inf.user).encode('utf-8'), inf.actor_id, unicode(inf.actor).encode('utf-8'), unicode({i.index: i for i in Infraction.Types.attrs}[inf.type_]).encode('utf-8'), unicode(inf.reason).encode('utf-8'), ]) event.msg.reply('Ok, here is an archive of all infractions', attachments=[ ('infractions.csv', buff.getvalue()) ]) @Plugin.command('info', '<infraction:int>', group='infractions', level=CommandLevels.MOD) def infraction_info(self, event, infraction): try: user = User.alias() actor = User.alias() infraction = Infraction.select(Infraction, user, actor).join( user, on=((Infraction.user_id == user.user_id).alias('user')) ).switch(Infraction).join( actor, on=((Infraction.actor_id == actor.user_id).alias('actor')) ).where( (Infraction.id == infraction) & (Infraction.guild_id == event.guild.id) ).get() except Infraction.DoesNotExist: raise CommandFail('cannot find an infraction with ID `{}`'.format(infraction)) type_ = {i.index: i for i in Infraction.Types.attrs}[infraction.type_] embed = MessageEmbed() if type_ in (Infraction.Types.MUTE, Infraction.Types.TEMPMUTE, Infraction.Types.TEMPROLE): embed.color = 0xfdfd96 elif type_ in (Infraction.Types.KICK, Infraction.Types.SOFTBAN): embed.color = 0xffb347 else: embed.color = 0xff6961 embed.title = str(type_).title() embed.set_thumbnail(url=infraction.user.get_avatar_url()) embed.add_field(name='User', value=unicode(infraction.user), inline=True) embed.add_field(name='Moderator', value=unicode(infraction.actor), inline=True) embed.add_field(name='Active', value='yes' if infraction.active else 'no', inline=True) if infraction.active and infraction.expires_at: embed.add_field(name='Expires', value=humanize.naturaldelta(infraction.expires_at - datetime.utcnow())) embed.add_field(name='Reason', value=infraction.reason or '_No Reason Given', inline=False) embed.timestamp = infraction.created_at.isoformat() event.msg.reply('', embed=embed) @Plugin.command('search', '[query:user|str...]', group='infractions', level=CommandLevels.MOD) def infraction_search(self, event, query=None): q = (Infraction.guild_id == event.guild.id) if query and isinstance(query, list) and isinstance(query[0], DiscoUser): query = query[0].id elif query: query = ' '.join(query) if query and (isinstance(query, int) or query.isdigit()): q &= ( (Infraction.id == int(query)) | (Infraction.user_id == int(query)) | (Infraction.actor_id == int(query))) elif query: q &= (Infraction.reason ** query) user = User.alias() actor = User.alias() infractions = Infraction.select(Infraction, user, actor).join( user, on=((Infraction.user_id == user.user_id).alias('user')) ).switch(Infraction).join( actor, on=((Infraction.actor_id == actor.user_id).alias('actor')) ).where(q).order_by(Infraction.created_at.desc()).limit(6) tbl = MessageTable() tbl.set_header('ID', 'Created', 'Type', 'User', 'Moderator', 'Active', 'Reason') for inf in infractions: type_ = {i.index: i for i in Infraction.Types.attrs}[inf.type_] reason = inf.reason or '' if len(reason) > 256: reason = reason[:256] + '...' if inf.active: active = 'yes' if inf.expires_at: active += ' (expires in {})'.format(humanize.naturaldelta(inf.expires_at - datetime.utcnow())) else: active = 'no' tbl.add( inf.id, inf.created_at.isoformat(), str(type_), unicode(inf.user), unicode(inf.actor), active, clamp(reason, 128) ) event.msg.reply(tbl.compile()) @Plugin.command('recent', aliases=['latest'], group='infractions', level=CommandLevels.MOD) def infractions_recent(self, event): # TODO: f*****g write this bruh pass @Plugin.command('duration', '<infraction:int> <duration:str>', group='infractions', level=CommandLevels.MOD) def infraction_duration(self, event, infraction, duration): try: inf = Infraction.get(id=infraction) except Infraction.DoesNotExist: raise CommandFail('invalid infraction (try `!infractions recent`)') if inf.actor_id != event.author.id and event.user_level < CommandLevels.ADMIN: raise CommandFail('only administrators can modify the duration of infractions created by other moderators') if not inf.active: raise CommandFail('that infraction is not active and cannot be updated') expires_dt = parse_duration(duration, inf.created_at) converted = False if inf.type_ in [Infraction.Types.MUTE.index, Infraction.Types.BAN.index]: inf.type_ = ( Infraction.Types.TEMPMUTE if inf.type_ == Infraction.Types.MUTE.index else Infraction.Types.TEMPBAN ) converted = True elif inf.type_ not in [ Infraction.Types.TEMPMUTE.index, Infraction.Types.TEMPBAN.index, Infraction.Types.TEMPROLE.index]: raise CommandFail('cannot set the duration for that type of infraction') inf.expires_at = expires_dt inf.save() self.queue_infractions() if converted: raise CommandSuccess('ok, I\'ve made that infraction temporary, it will now expire on {}'.format( inf.expires_at.isoformat() )) else: raise CommandSuccess('ok, I\'ve updated that infractions duration, it will now expire on {}'.format( inf.expires_at.isoformat() )) @Plugin.command('reason', '<infraction:int> <reason:str...>', group='infractions', level=CommandLevels.MOD) def reason(self, event, infraction, reason): try: inf = Infraction.get(id=infraction) except Infraction.DoesNotExist: inf = None if inf is None or inf.guild_id != event.guild.id: event.msg.reply('Unknown infraction ID') return if not inf.actor_id: inf.actor_id = event.author.id if inf.actor_id != event.author.id and event.user_level < event.config.reason_edit_level: raise CommandFail('you do not have the permissions required to edit other moderators infractions') inf.reason = reason inf.save() raise CommandSuccess('I\'ve updated the reason for infraction #{}'.format(inf.id)) def can_act_on(self, event, victim_id, throw=True): if event.author.id == victim_id: if not throw: return False raise CommandFail('cannot execute that action on yourself') victim_level = self.bot.plugins.get('CorePlugin').get_level(event.guild, victim_id) if event.user_level <= victim_level: if not throw: return False raise CommandFail('invalid permissions') return True def confirm_action(self, event, message): if not event.config.confirm_actions: return if event.config.confirm_actions_reaction: event.msg.add_reaction(GREEN_TICK_EMOJI) return msg = event.msg.reply(message) if event.config.confirm_actions_expiry > 0: # Close over this thread local expiry = event.config.confirm_actions_expiry def f(): gevent.sleep(expiry) msg.delete() # Run this in a greenlet so we dont block event execution self.spawn(f) @Plugin.command('mute', '<user:user|snowflake> [reason:str...]', level=CommandLevels.MOD) @Plugin.command('tempmute', '<user:user|snowflake> <duration:str> [reason:str...]', level=CommandLevels.MOD) def tempmute(self, event, user, duration=None, reason=None): if not duration and reason: duration = parse_duration(reason.split(' ')[0], safe=True) if duration: if ' ' in reason: reason = reason.split(' ', 1)[-1] else: reason = None elif duration: duration = parse_duration(duration) member = event.guild.get_member(user) if member: self.can_act_on(event, member.id) if not event.config.mute_role: raise CommandFail('mute is not setup on this server') if event.config.mute_role in member.roles: raise CommandFail(u'{} is already muted'.format(member.user)) # If we have a duration set, this is a tempmute if duration: # Create the infraction Infraction.tempmute(self, event, member, reason, duration) self.queue_infractions() self.confirm_action(event, maybe_string( reason, u':ok_hand: {u} is now muted for {t} (`{o}`)', u':ok_hand: {u} is now muted for {t}', u=member.user, t=humanize.naturaldelta(duration - datetime.utcnow()), )) else: existed = False # If the user is already muted check if we can take this from a temp # to perma mute. if event.config.mute_role in member.roles: existed = Infraction.clear_active(event, member.id, [Infraction.Types.TEMPMUTE]) # The user is 100% muted and not tempmuted at this point, so lets bail if not existed: raise CommandFail(u'{} is already muted'.format(member.user)) Infraction.mute(self, event, member, reason) existed = u' [was temp-muted]' if existed else '' self.confirm_action(event, maybe_string( reason, u':ok_hand: {u} is now muted (`{o}`)' + existed, u':ok_hand: {u} is now muted' + existed, u=member.user, )) else: raise CommandFail('invalid user') @Plugin.command( 'temprole', '<user:user|snowflake> <role:snowflake|str> <duration:str> [reason:str...]', level=CommandLevels.MOD) def temprole(self, event, user, role, duration, reason=None): member = event.guild.get_member(user) if not member: raise CommandFail('invalid user') self.can_act_on(event, member.id) role_id = role if isinstance(role, (int, long)) else event.config.role_aliases.get(role.lower()) if not role_id or role_id not in event.guild.roles: raise CommandFail('invalid or unknown role') if role_id in member.roles: raise CommandFail(u'{} is already in that role'.format(member.user)) expire_dt = parse_duration(duration) Infraction.temprole(self, event, member, role_id, reason, expire_dt) self.queue_infractions() self.confirm_action(event, maybe_string( reason, u':ok_hand: {u} is now in the {r} role for {t} (`{o}`)', u':ok_hand: {u} is now in the {r} role for {t}', r=event.guild.roles[role_id].name, u=member.user, t=humanize.naturaldelta(expire_dt - datetime.utcnow()), )) @Plugin.command('unmute', '<user:user|snowflake>', level=CommandLevels.MOD) def unmute(self, event, user, reason=None): # TOOD: eventually we should pull the role from the GuildMemberBackup if they arent in server member = event.guild.get_member(user) if member: self.can_act_on(event, member.id) if not event.config.mute_role: raise CommandFail('mute is not setup on this server') if event.config.mute_role not in member.roles: raise CommandFail(u'{} is not muted'.format(member.user)) Infraction.clear_active(event, member.id, [Infraction.Types.MUTE, Infraction.Types.TEMPMUTE]) self.call( 'ModLogPlugin.create_debounce', event, ['GuildMemberUpdate'], role_id=event.config.mute_role, ) member.remove_role(event.config.mute_role) self.call( 'ModLogPlugin.log_action_ext', Actions.MEMBER_UNMUTED, event.guild.id, member=member, actor=unicode(event.author) if event.author.id != member.id else 'Automatic', ) self.confirm_action(event, u':ok_hand: {} is now unmuted'.format(member.user)) else: raise CommandFail('invalid user') @Plugin.command('kick', '<user:user|snowflake> [reason:str...]', level=CommandLevels.MOD) def kick(self, event, user, reason=None): member = event.guild.get_member(user) if member: self.can_act_on(event, member.id) Infraction.kick(self, event, member, reason) self.confirm_action(event, maybe_string( reason, u':ok_hand: kicked {u} (`{o}`)', u':ok_hand: kicked {u}', u=member.user, )) else: raise CommandFail('invalid user') @Plugin.command('mkick', parser=True, level=CommandLevels.MOD) @Plugin.parser.add_argument('users', type=long, nargs='+') @Plugin.parser.add_argument('-r', '--reason', default='', help='reason for modlog') def mkick(self, event, args): members = [] for user_id in args.users: member = event.guild.get_member(user_id) if not member: # TODO: this sucks, batch these raise CommandFail('failed to kick {}, user not found'.format(user_id)) if not self.can_act_on(event, member.id, throw=False): raise CommandFail('failed to kick {}, invalid permissions'.format(user_id)) members.append(member) msg = event.msg.reply('Ok, kick {} users for `{}`?'.format(len(members), args.reason or 'no reason')) msg.chain(False).\ add_reaction(GREEN_TICK_EMOJI).\ add_reaction(RED_TICK_EMOJI) try: mra_event = self.wait_for_event( 'MessageReactionAdd', message_id=msg.id, conditional=lambda e: ( e.emoji.id in (GREEN_TICK_EMOJI_ID, RED_TICK_EMOJI_ID) and e.user_id == event.author.id )).get(timeout=10) except gevent.Timeout: return finally: msg.delete() if mra_event.emoji.id != GREEN_TICK_EMOJI_ID: return for member in members: Infraction.kick(self, event, member, args.reason) raise CommandSuccess('kicked {} users'.format(len(members))) @Plugin.command('ban', '<user:user|snowflake> [reason:str...]', level=CommandLevels.MOD) @Plugin.command('forceban', '<user:snowflake> [reason:str...]', level=CommandLevels.MOD) def ban(self, event, user, reason=None): member = None if isinstance(user, (int, long)): self.can_act_on(event, user) Infraction.ban(self, event, user, reason, guild=event.guild) else: member = event.guild.get_member(user) if member: self.can_act_on(event, member.id) Infraction.ban(self, event, member, reason, guild=event.guild) else: raise CommandFail('invalid user') self.confirm_action(event, maybe_string( reason, u':ok_hand: banned {u} (`{o}`)', u':ok_hand: banned {u}', u=member.user if member else user, )) @Plugin.command('mban', parser=True, level=CommandLevels.MOD) @Plugin.parser.add_argument('users', type=long, nargs='+') @Plugin.parser.add_argument('-r', '--reason', default='', help='reason for modlog') def mban(self, event, args): members = [] for user_id in args.users: member = event.guild.get_member(user_id) if not member: raise CommandFail('failed to ban {}, user not found'.format(user_id)) if not self.can_act_on(event, member.id, throw=False): raise CommandFail('failed to ban {}, invalid permissions'.format(user_id)) members.append(member) msg = event.msg.reply('Ok, ban {} users for `{}`?'.format(len(members), args.reason or 'no reason')) msg.chain(False).\ add_reaction(GREEN_TICK_EMOJI).\ add_reaction(RED_TICK_EMOJI) try: mra_event = self.wait_for_event( 'MessageReactionAdd', message_id=msg.id, conditional=lambda e: ( e.emoji.id in (GREEN_TICK_EMOJI_ID, RED_TICK_EMOJI_ID) and e.user_id == event.author.id )).get(timeout=10) except gevent.Timeout: return finally: msg.delete() if mra_event.emoji.id != GREEN_TICK_EMOJI_ID: return for member in members: Infraction.ban(self, event, member, args.reason, guild=event.guild) raise CommandSuccess('banned {} users'.format(len(members))) @Plugin.command('softban', '<user:user|snowflake> [reason:str...]', level=CommandLevels.MOD) def softban(self, event, user, reason=None): """ Ban then unban a user from the server (with an optional reason for the modlog) """ member = event.guild.get_member(user) if member: self.can_act_on(event, member.id) Infraction.softban(self, event, member, reason) self.confirm_action(event, maybe_string( reason, u':ok_hand: soft-banned {u} (`{o}`)', u':ok_hand: soft-banned {u}', u=member.user, )) else: raise CommandFail('invald user') @Plugin.command('tempban', '<user:user|snowflake> <duration:str> [reason:str...]', level=CommandLevels.MOD) def tempban(self, event, duration, user, reason=None): member = event.guild.get_member(user) if member: self.can_act_on(event, member.id) expires_dt = parse_duration(duration) Infraction.tempban(self, event, member, reason, expires_dt) self.queue_infractions() self.confirm_action(event, maybe_string( reason, u':ok_hand: temp-banned {u} for {t} (`{o}`)', u':ok_hand: temp-banned {u} for {t}', u=member.user, t=humanize.naturaldelta(expires_dt - datetime.utcnow()), )) else: raise CommandFail('invalid user') @Plugin.command('warn', '<user:user|snowflake> [reason:str...]', level=CommandLevels.MOD) def warn(self, event, user, reason=None): member = None member = event.guild.get_member(user) if member: self.can_act_on(event, member.id) Infraction.warn(self, event, member, reason, guild=event.guild) else: raise CommandFail('invalid user') self.confirm_action(event, maybe_string( reason, u':ok_hand: warned {u} (`{o}`)', u':ok_hand: warned {u}', u=member.user if member else user, ))
class UtilitiesPlugin(Plugin): def load(self, ctx): super(UtilitiesPlugin, self).load(ctx) self.reminder_task = Eventual(self.trigger_reminders) self.spawn_later(10, self.queue_reminders) def queue_reminders(self): try: next_reminder = Reminder.select().order_by( Reminder.remind_at.asc()).limit(1).get() except Reminder.DoesNotExist: return self.reminder_task.set_next_schedule(next_reminder.remind_at) @Plugin.command('coin', group='random', global_=True) def coin(self, event): """ Flip a coin """ raise CommandSuccess(random.choice(['heads', 'tails'])) @Plugin.command('number', '[end:int] [start:int]', group='random', global_=True) def random_number(self, event, end=10, start=0): """ Returns a random number """ # Because someone will be an idiot if end > 9223372036854775807: raise CommandSuccess('Ending number too big!') if end <= start: raise CommandSuccess( 'Ending number must be larger than starting number!') raise CommandSuccess(str(random.randint(start, end))) @Plugin.command('cat', '{bentley}', global_=True) def cat(self, event, bentley=False): try: if bentley: URL = 'https://bentley.tadhg.sh/api/random' data = requests.get(URL).json() fname = 'bentley-' + str( data['id']) # Probably don't have to, but gonna. cat = requests.get(data['url']) else: URL = 'https://api.thecatapi.com/v1/images/search' data = requests.get(URL).json() fname = data[0]['id'] cat = requests.get(data[0]['url']) cat.raise_for_status() fext = cat.headers['content-type'].split('/')[-1].split(';')[0] event.msg.reply('', attachments=[('cat-{}.{}'.format(fname, fext), cat.content)]) except: return event.msg.reply('{} Cat not found :('.format( cat.status_code)) @Plugin.command('otter', global_=True) def otter(self, event): try: URL = 'https://otter.bruhmomentlol.repl.co/random' otter = requests.get(URL) otter.raise_for_status() fext = otter.headers['x-file-ext'] event.msg.reply('', attachments=[('otter.{}'.format(fext), otter.content)]) except: return event.msg.reply('{} Otter not found :('.format( otter.status_code)) @Plugin.command('dog', global_=True) def dog(self, event): try: URL = 'https://api.thedogapi.com/v1/images/search' data = requests.get(URL).json() dog = requests.get(data[0]['url']) dog.raise_for_status() fname = data[0]['id'] fext = dog.headers['content-type'].split('/')[-1].split(';')[0] event.msg.reply('', attachments=[('dog-{}.{}'.format(fname, fext), dog.content)]) except Exception as e: return event.msg.reply('{} Dog not found :('.format( dog.status_code)) @Plugin.command('emoji', '<emoji:str>', global_=True) def emoji(self, event, emoji): if not EMOJI_RE.match(emoji): raise CommandFail('Unknown emoji: `{}`'.format(emoji)) fields = [] name, eid = EMOJI_RE.findall(emoji)[0] fields.append('**ID:** {}'.format(eid)) fields.append('**Name:** {}'.format(S(name))) guild = self.state.guilds.find_one(lambda v: eid in v.emojis) if guild: fields.append('**Guild:** {} ({})'.format(S(guild.name), guild.id)) url = 'https://cdn.discordapp.com/emojis/{}.png?v=1'.format(eid) r = requests.get(url) r.raise_for_status() return event.msg.reply('\n'.join(fields), attachments=[('emoji.png', r.content)]) @Plugin.command('jumbo', '<emojis:str...>', global_=True) def jumbo(self, event, emojis): urls = [] for emoji in emojis.split(' ')[:5]: if EMOJI_RE.match(emoji): _, eid = EMOJI_RE.findall(emoji)[0] urls.append( 'https://cdn.discordapp.com/emojis/{}.png?v=1'.format(eid)) else: urls.append(get_emoji_url(emoji)) width, height, images = 0, 0, [] for r in Pool(6).imap(requests.get, urls): try: r.raise_for_status() except requests.HTTPError: return img = Image.open(BytesIO(r.content)) height = img.height if img.height > height else height width += img.width + 10 images.append(img) image = Image.new('RGBA', (width, height)) width_offset = 0 for img in images: image.paste(img, (width_offset, 0)) width_offset += img.width + 10 combined = BytesIO() image.save(combined, 'png', quality=55) combined.seek(0) return event.msg.reply('', attachments=[('emoji.png', combined)]) @Plugin.command('seen', '<user:user>', global_=True) def seen(self, event, user: User): try: msg = Message.select(Message.timestamp).where( Message.author_id == user.id).order_by( Message.timestamp.desc()).limit(1).get() except Message.DoesNotExist: raise CommandFail("I've never seen {}".format(user)) raise CommandSuccess('I last saw {} {}'.format( user, int(msg.timestamp.timestamp()))) @Plugin.command('search', '<query:str...>', global_=True) def search(self, event, query: str): queries = [] if query.isdigit(): queries.append((User.user_id == query)) q = USER_MENTION_RE.findall(query) if len(q) and q[0].isdigit(): queries.append((User.user_id == q[0])) else: queries.append( (User.username**'%{}%'.format(query.replace('%', '')))) if '#' in query: username, discrim = query.rsplit('#', 1) if discrim is not None: queries.append(((User.username == username) & (User.discriminator == discrim))) users = User.select().where(reduce(operator.or_, queries)).limit(10) if len(users) == 0: raise CommandFail('No users found for query `{}`'.format( S(query, escape_codeblocks=True))) if len(users) == 1: if users[0].user_id in self.state.users: return self.info(event, self.state.users.get(users[0].user_id)) raise CommandSuccess( 'Found the following users for your query: ```{}```'.format( '\n'.join([ '{} ({})'.format(str(i), i.user_id) for i in users[:25] ]))) @Plugin.command('server', '[guild_id:snowflake]', aliases=['guild'], global_=True) def server(self, event, guild_id=None): guild: Guild = self.state.guilds.get( guild_id) if guild_id else event.guild if not guild: raise CommandFail('Invalid server') embed = MessageEmbed() embed.set_author( MessageEmbedAuthor(name=guild.name, icon_url=guild.icon_url())) # General Abouts about_field = MessageEmbedField() about_field.name = '**\u276F About**' about_text = 'Created by {} ({}) — <t:{}:R>'.format( guild.owner, guild.owner.id, int(to_datetime(guild.id).replace(tzinfo=pytz.UTC).timestamp())) about_text += '\nMembers: {:,}/{:,}'.format( guild.approximate_presence_count, guild.member_count) about_text += '\nRegion: {}'.format(guild.region) about_field.value = about_text embed.add_field(about_field) # General Counts counts_field = MessageEmbedField() counts_field.name = '\n**\u276F Counts**' text_count = sum(1 for c in list(guild.channels.values()) if not c.is_voice and not c.is_thread) voice_count = len(guild.channels) - text_count counts_field.value = 'Roles: {:,}\nText: {:,}\nVoice: {:,}'.format( len(guild.roles), text_count, voice_count) embed.add_field(counts_field) # Security security_field = MessageEmbedField() security_field.name = '\n**\u276F Security**' security_field.value = 'Verification: {}\nExplicit Content: {}'.format( guild.verification_level, guild.explicit_content_filter) embed.add_field(security_field) # Features features_field = MessageEmbedField() features_field.name = '\n**\u276F Features**' features_field.value = 'Features: {}'.format(', '.join(guild.features)) embed.add_field(features_field) if guild.icon: embed.color = get_dominant_colors_guild(guild) event.msg.reply('', embed=embed) @Plugin.command('info', '[user:user|snowflake]', aliases='whois') def info(self, event, user: User = None): if not user: user = event.author else: if not isinstance(user, DiscoUser): try: user = self.state.guilds[event.guild.id].members[user].user except KeyError: try: user = self.state.users[user] except KeyError: try: user = self.bot.client.api.users_get(user) except APIException: return event.msg.reply( ':eyes: User not found').after(3).delete() self.client.api.channels_typing(event.channel.id) content = [] content.append('**\u276F User Information**') content.append('Profile: <@{}>'.format(user.id)) created_dt = to_datetime(user.id) content.append('Created: <t:{0}:R> (<t:{0}:f>)'.format( int(created_dt.replace(tzinfo=pytz.UTC).timestamp()))) member = event.guild.get_member(user.id) if event.guild else None if user.public_flags: badges = '' user_badges = list(UserFlags(user.public_flags)) for badge in user_badges: badges += '<{}> '.format(BADGE_EMOJI[badge]) content.append('Badges: {}'.format(badges)) if member: content.append('\n**\u276F Member Information**') if member.nick: content.append('Nickname: {}'.format(member.nick)) content.append('Joined: <t:{0}:R> (<t:{0}:f>)'.format( int(member.joined_at.replace(tzinfo=pytz.UTC).timestamp()))) content.append('Messages: {}'.format( int( Message.select(fn.Count( Message.id)).where((Message.author_id == user.id) & (Message.guild_id == event.guild. id)).tuples()[0][0]))) if member.roles: content.append('Roles: {}'.format(', '.join( ('<@&{}>'.format(r) for r in member.roles)))) # Execute a bunch of queries newest_msg = Message.select(fn.MAX(Message.id)).where( (Message.author_id == user.id) & (Message.guild_id == event.guild.id)).tuples()[0][0] infractions = Infraction.select(Infraction.id).where( (Infraction.user_id == user.id) & (Infraction.guild_id == event.guild.id)).tuples() if newest_msg: content.append('\n **\u276F Activity**') content.append('Last Message: <t:{0}:R> (<t:{0}:f>)'.format( int((to_datetime(newest_msg).replace( tzinfo=pytz.UTC)).timestamp()))) # content.append('First Message: {} ({})'.format( # humanize.naturaltime(datetime.utcnow() - to_datetime(oldest_msg)), # to_datetime(oldest_msg).strftime("%b %d %Y %H:%M:%S"), # )) if len(infractions) > 0: content.append('\n**\u276F Infractions**') total = len(infractions) content.append('Total Infractions: **{:,}**'.format(total)) embed = MessageEmbed() try: avatar = User.with_id(user.id).get_avatar_url() except APIException: avatar = user.get_avatar_url( ) # This fails if the user has never been seen by speedboat. embed.set_author(name='{} ({})'.format( str(user), user.id, ), icon_url=avatar) embed.set_thumbnail(url=avatar) embed.description = '\n'.join(content) embed.color = get_dominant_colors_user(user, avatar) event.msg.reply('', embed=embed) @Plugin.command('config', global_=True) def config_cmd(self, event): raise CommandSuccess('{}/guilds/{}/config'.format( WEB_URL, event.guild.id)) def trigger_reminders(self): reminders = Reminder.with_message_join().where( (Reminder.remind_at < (datetime.utcnow() + timedelta(seconds=1)))) waitables = [] for reminder in reminders: waitables.append(self.spawn(self.trigger_reminder, reminder)) for waitable in waitables: waitable.join() self.queue_reminders() def trigger_reminder(self, reminder: Reminder): message = Message.get(reminder.message_id) channel = self.state.channels.get(message.channel_id) if not channel: self.log.warning( 'Not triggering reminder, channel %s was not found!', message.channel_id) reminder.delete_instance() return msg = channel.send_message( '<@{}> you asked me on <t:{reminder_time}:f> (<t:{reminder_time}:R>) to remind you about: {}' .format( message.author_id, S(reminder.content), reminder_time=int( reminder.created_at.replace(tzinfo=pytz.UTC).timestamp()), ), allowed_mentions={'users': [str(message.author_id)]}) # Add the emoji options msg.add_reaction(SNOOZE_EMOJI) msg.add_reaction(GREEN_TICK_EMOJI) try: mra_event = self.wait_for_event( 'MessageReactionAdd', message_id=msg.id, conditional=lambda e: ((e.emoji.name == SNOOZE_EMOJI or e.emoji.id == GREEN_TICK_EMOJI_ID) and e.user_id == message.author_id )).get(timeout=30) except gevent.Timeout: reminder.delete_instance() return finally: # Cleanup try: msg.delete_all_reactions() except APIException: # We don't have permission to remove reactions, but, we don't want to fail the reminder. pass if mra_event.emoji.name == SNOOZE_EMOJI: reminder.remind_at = datetime.utcnow() + timedelta(minutes=20) reminder.save() msg.edit( 'Ok, I\'ve snoozed that reminder. You\'ll get another notification in 20 minutes.' ) return reminder.delete_instance() @Plugin.command('clear', group='r', global_=True) def cmd_remind_clear(self, event): count = Reminder.delete_for_user(event.author.id) raise CommandSuccess('I cleared {} reminders for you'.format(count)) @Plugin.command('add', '<duration:str> [content:str...]', group='r', global_=True) @Plugin.command('remind', '<duration:str> [content:str...]', global_=True) def cmd_remind(self, event, duration, content=None): if Reminder.count_for_user(event.author.id) > 15: raise CommandFail('You can only have 15 reminders going at once!') remind_at = parse_duration(duration) if remind_at > (datetime.utcnow() + timedelta(seconds=5 * YEAR_IN_SEC)): raise CommandFail('That\'s too far in the future... I\'ll forget!') if event.msg.message_reference.message_id: referenced_msg: MessageReference = event.channel.get_message( event.msg.message_reference.message_id) content = 'https://discord.com/channels/{}/{}/{}'.format( self.state.channels.get(referenced_msg.channel_id).guild_id, referenced_msg.channel_id, referenced_msg.id) elif not content: raise CommandFail( 'You need to provide content for the reminder, or reply to a message!' ) r = Reminder.create(message_id=event.msg.id, remind_at=remind_at, content=content) self.reminder_task.set_next_schedule(r.remind_at) raise CommandSuccess( 'I\'ll remind you at <t:{0}:f> (<t:{0}:R>)'.format( int(r.remind_at.replace(tzinfo=pytz.UTC).timestamp())))
class UtilitiesPlugin(Plugin): def load(self, ctx): super(UtilitiesPlugin, self).load(ctx) self.reminder_task = Eventual(self.trigger_reminders) self.spawn_later(10, self.queue_reminders) def queue_reminders(self): try: next_reminder = Reminder.select().order_by( Reminder.remind_at.asc()).limit(1).get() except Reminder.DoesNotExist: return self.reminder_task.set_next_schedule(next_reminder.remind_at) @Plugin.command('coin', group='random', global_=True) def coin(self, event): """ Flip a coin """ raise CommandSuccess(random.choice(['heads', 'tails'])) @Plugin.command('number', '[end:int] [start:int]', group='random', global_=True) def random_number(self, event, end=10, start=0): """ Returns a random number """ # Because someone will be an idiot if end > 9223372036854775807: raise CommandSuccess('Ending number too big!') if end <= start: raise CommandSuccess( 'Ending number must be larger than starting number!') raise CommandSuccess(str(random.randint(start, end))) @Plugin.command('cat', global_=True) def cat(self, event): try: r = requests.get( 'https://api.thecatapi.com/v1/images/search?format=src') r.raise_for_status() ext = r.headers['content-type'].split('/')[-1].split(';')[0] event.msg.reply('', attachments=[('cat.{}'.format(ext), r.content)]) except: return event.msg.reply(r.status_code + ' Cat not found :(') @Plugin.command('dog', global_=True) def dog(self, event): try: r = requests.get( 'https://api.thedogapi.com/v1/images/search?format=src') r.raise_for_status() ext = r.headers['content-type'].split('/')[-1].split(';')[0] event.msg.reply('', attachments=[('dog.{}'.format(ext), r.content)]) except Exception as e: return event.msg.reply(e.with_traceback + ' Dog not found :(') @Plugin.command('emoji', '<emoji:str>', global_=True) def emoji(self, event, emoji): if not EMOJI_RE.match(emoji): raise CommandFail('Unknown emoji: `{}`'.format(emoji)) fields = [] name, eid = EMOJI_RE.findall(emoji)[0] fields.append('**ID:** {}'.format(eid)) fields.append('**Name:** {}'.format(S(name))) guild = self.state.guilds.find_one(lambda v: eid in v.emojis) if guild: fields.append('**Guild:** {} ({})'.format(S(guild.name), guild.id)) url = 'https://cdn.discordapp.com/emojis/{}.png?v=1'.format(eid) r = requests.get(url) r.raise_for_status() return event.msg.reply('\n'.join(fields), attachments=[('emoji.png', r.content)]) @Plugin.command('jumbo', '<emojis:str...>', global_=True) def jumbo(self, event, emojis): urls = [] for emoji in emojis.split(' ')[:5]: if EMOJI_RE.match(emoji): _, eid = EMOJI_RE.findall(emoji)[0] urls.append( 'https://cdn.discordapp.com/emojis/{}.png?v=1'.format(eid)) else: urls.append(get_emoji_url(emoji)) width, height, images = 0, 0, [] for r in Pool(6).imap(requests.get, urls): try: r.raise_for_status() except requests.HTTPError: return img = Image.open(BytesIO(r.content)) height = img.height if img.height > height else height width += img.width + 10 images.append(img) image = Image.new('RGBA', (width, height)) width_offset = 0 for img in images: image.paste(img, (width_offset, 0)) width_offset += img.width + 10 combined = BytesIO() image.save(combined, 'png', quality=55) combined.seek(0) return event.msg.reply('', attachments=[('emoji.png', combined)]) @Plugin.command('seen', '<user:user>', global_=True) def seen(self, event, user): try: msg = Message.select(Message.timestamp).where( Message.author_id == user.id).order_by( Message.timestamp.desc()).limit(1).get() except Message.DoesNotExist: raise CommandFail("I've never seen {}".format(user)) raise CommandSuccess( 'I last saw {} {} (at {})'. format( #TODO: Prettify this timestamp response like in inf latest user, humanize.naturaltime(datetime.utcnow() - msg.timestamp), msg.timestamp)) @Plugin.command('search', '<query:str...>', global_=True) def search(self, event, query): queries = [] if query.isdigit(): queries.append((User.user_id == query)) q = USER_MENTION_RE.findall(query) if len(q) and q[0].isdigit(): queries.append((User.user_id == q[0])) else: queries.append( (User.username**'%{}%'.format(query.replace('%', '')))) if '#' in query: username, discrim = query.rsplit('#', 1) if discrim.isdigit(): queries.append(((User.username == username) & (User.discriminator == int(discrim)))) users = User.select().where(reduce(operator.or_, queries)).limit(10) if len(users) == 0: raise CommandFail('No users found for query `{}`'.format( S(query, escape_codeblocks=True))) if len(users) == 1: if users[0].user_id in self.state.users: return self.info(event, self.state.users.get(users[0].user_id)) raise CommandSuccess( 'Found the following users for your query: ```{}```'.format( '\n'.join([ '{} ({})'.format(str(i), i.user_id) for i in users[:25] ]))) @Plugin.command('server', '[guild_id:snowflake]', global_=True) def server(self, event, guild_id=None): guild = self.state.guilds.get(guild_id) if guild_id else event.guild if not guild: raise CommandFail('Invalid server') content = [] content.append('**\u276F Server Information**') content.append('Owner: {} ({})'.format(guild.owner, guild.owner.id)) created_at = to_datetime(guild.id) content.append('Created: {} ({})'.format( humanize.naturaltime(datetime.utcnow() - created_at), created_at.isoformat(), )) content.append('Members: {:,}'.format(len(guild.members))) if guild.features: content.append('Features: {}'.format(', '.join(guild.features))) content.append('\n**\u276F Counts**') text_count = sum(1 for c in list(guild.channels.values()) if not c.is_voice) voice_count = len(guild.channels) - text_count content.append('Roles: {}'.format(len(guild.roles))) content.append('Text: {}'.format(text_count)) content.append('Voice: {}'.format(voice_count)) content.append('\n**\u276F Members**') status_counts = defaultdict(int) for member in list(guild.members.values()): if not member.user.presence: status = Status.OFFLINE else: status = member.user.presence.status status_counts[status] += 1 for status, count in sorted(list(status_counts.items()), key=lambda i: str(i[0]), reverse=True): content.append('<{}> - {}'.format(STATUS_EMOJI[status], count)) embed = MessageEmbed() if guild.icon: embed.set_thumbnail(url=guild.icon_url) # TODO: Fix whatever caused me to need to do this try: embed.color = get_dominant_colors_guild(guild) except: embed.color = 0x7289DA embed.description = '\n'.join(content) event.msg.reply('', embed=embed) @Plugin.command('info', '[user:user|snowflake]') def info(self, event, user=None): if not user: user = event.author else: if not isinstance(user, DiscoUser): try: user = self.state.guilds[event.guild.id].members[user].user except KeyError: try: user = self.state.users[user] except KeyError: try: user = self.bot.client.api.users_get(user) except APIException: return event.msg.reply( 'User not found :eyes:').after(3).delete() self.client.api.channels_typing(event.channel.id) content = [] content.append('**\u276F User Information**') content.append('Profile: <@{}>'.format(user.id)) created_dt = to_datetime(user.id) content.append('Created: {} ({})'.format( humanize.naturaltime(datetime.utcnow() - created_dt), created_dt.strftime("%b %d %Y %H:%M:%S"))) member = event.guild.get_member(user.id) if event.guild else None if user.presence: #I couldn't get this to work w/o it lol emoji, status = get_status_emoji(user.presence) content.append('Status: <{}> {}'.format(emoji, status)) if user.presence.game and user.presence.game.name: if user.presence.game.type == ActivityTypes.DEFAULT: content.append('{}'.format(user.presence.game.name)) if user.presence.game.type == ActivityTypes.CUSTOM: content.append('Custom Status: {}'.format( user.presence.game.state)) if user.presence.game.type == ActivityTypes.LISTENING: content.append('Listening to {} on Spotify'.format( user.presence.game.details) ) #In the embed, details is the songname. if user.presence.game.type == ActivityTypes.STREAMING: content.append('Streaming: [{}]({})'.format( user.presence.game.name, user.presence.game.url)) if user.public_flags: badges = '' user_badges = list(UserFlags(user.public_flags)) for badge in user_badges: badges += '<{}> '.format(BADGE_EMOJI[badge]) content.append('Badges: {}'.format(badges)) if member: content.append('\n**\u276F Member Information**') if member.nick: content.append('Nickname: {}'.format(member.nick)) content.append('Joined: {} ago ({})'.format( humanize.naturaldelta(datetime.utcnow() - member.joined_at), member.joined_at.strftime("%b %d %Y %H:%M:%S"), )) if member.roles: content.append('Roles: {}'.format(', '.join( ('<@&{}>'.format(member.guild.roles.get(r).id) for r in member.roles)))) # Execute a bunch of queries newest_msg = Message.select(fn.MAX(Message.id)).where( (Message.author_id == user.id) & (Message.guild_id == event.guild.id)).tuples()[0][0] oldest_msg = Message.select(fn.MIN(Message.id)).where( (Message.author_id == user.id) & (Message.guild_id == event.guild.id)).tuples()[0][0] #Slow Query voice = GuildVoiceSession.select( fn.COUNT(GuildVoiceSession.user_id), fn.SUM(GuildVoiceSession.ended_at - GuildVoiceSession.started_at) ).where((GuildVoiceSession.user_id == user.id) & (~(GuildVoiceSession.ended_at >> None)) & (GuildVoiceSession.guild_id == event.guild.id)).tuples()[0] infractions = Infraction.select(Infraction.id).where( (Infraction.user_id == user.id) & (Infraction.guild_id == event.guild.id)).tuples() if newest_msg and oldest_msg: content.append('\n **\u276F Activity**') content.append('Last Message: {} ({})'.format( humanize.naturaltime(datetime.utcnow() - to_datetime(newest_msg)), to_datetime(newest_msg).strftime("%b %d %Y %H:%M:%S"), )) content.append('First Message: {} ({})'.format( humanize.naturaltime(datetime.utcnow() - to_datetime(oldest_msg)), to_datetime(oldest_msg).strftime("%b %d %Y %H:%M:%S"), )) if len(infractions) > 0: content.append('\n**\u276F Infractions**') total = len(infractions) content.append('Total Infractions: **{:,}**'.format(total)) if voice[0]: content.append('\n**\u276F Voice**') content.append('Sessions: `{:,}`'.format(voice[0])) content.append('Time: `{}`'.format( str(humanize.naturaldelta(voice[1])).title())) embed = MessageEmbed() try: avatar = User.with_id(user.id).get_avatar_url() except: avatar = user.get_avatar_url( ) # This fails if the user has never been seen by speedboat. embed.set_author(name='{}#{} ({})'.format( user.username, user.discriminator, user.id, ), icon_url=avatar) embed.set_thumbnail(url=avatar) embed.description = '\n'.join(content) embed.color = get_dominant_colors_user(user, avatar) event.msg.reply('', embed=embed) def trigger_reminders(self): reminders = Reminder.with_message_join().where( (Reminder.remind_at < (datetime.utcnow() + timedelta(seconds=1)))) waitables = [] for reminder in reminders: waitables.append(self.spawn(self.trigger_reminder, reminder)) for waitable in waitables: waitable.join() self.queue_reminders() def trigger_reminder(self, reminder): message = Message.get(reminder.message_id) channel = self.state.channels.get(message.channel_id) if not channel: self.log.warning( 'Not triggering reminder, channel %s was not found!', message.channel_id) reminder.delete_instance() return msg = channel.send_message( '<@{}> you asked me at {} ({}) to remind you about: {}'.format( message.author_id, reminder.created_at, humanize.naturaltime(datetime.utcnow() - reminder.created_at), S(reminder.content))) # Add the emoji options msg.add_reaction(SNOOZE_EMOJI) msg.add_reaction(GREEN_TICK_EMOJI) try: mra_event = self.wait_for_event( 'MessageReactionAdd', message_id=msg.id, conditional=lambda e: ((e.emoji.name == SNOOZE_EMOJI or e.emoji.id == GREEN_TICK_EMOJI_ID) and e.user_id == message.author_id )).get(timeout=30) except gevent.Timeout: reminder.delete_instance() return finally: # Cleanup msg.delete_reaction(SNOOZE_EMOJI) msg.delete_reaction(GREEN_TICK_EMOJI) if mra_event.emoji.name == SNOOZE_EMOJI: reminder.remind_at = datetime.utcnow() + timedelta(minutes=20) reminder.save() msg.edit('Ok, I\'ve snoozed that reminder for 20 minutes.') return reminder.delete_instance() @Plugin.command('clear', group='r', global_=True) def cmd_remind_clear(self, event): count = Reminder.delete_for_user(event.author.id) raise CommandSuccess('I cleared {} reminders for you'.format(count)) @Plugin.command('add', '<duration:str> <content:str...>', group='r', global_=True) @Plugin.command('remind', '<duration:str> <content:str...>', global_=True) def cmd_remind(self, event, duration, content): if Reminder.count_for_user(event.author.id) > 15: raise CommandFail('You can only have 15 reminders going at once!') remind_at = parse_duration(duration) if remind_at > (datetime.utcnow() + timedelta(seconds=5 * YEAR_IN_SEC)): raise CommandSuccess('Thats too far in the future, I\'ll forget!') r = Reminder.create(message_id=event.msg.id, remind_at=remind_at, content=content) self.reminder_task.set_next_schedule(r.remind_at) raise CommandSuccess('I\'ll remind you at {} ({})'.format( r.remind_at.isoformat(), humanize.naturaldelta(r.remind_at - datetime.utcnow()), ))
class AdminPlugin(Plugin): def load(self, ctx): super(AdminPlugin, self).load(ctx) self.cleans = {} self.inf_task = Eventual(self.clear_infractions) self.spawn(self.queue_infractions) def queue_infractions(self): time.sleep(5) next_infraction = list(Infraction.select().where( (Infraction.active == 1) & (~(Infraction.expires_at >> None))).order_by( Infraction.expires_at.asc()).limit(1)) if not next_infraction: self.log.info('No infractions to wait for') return self.log.info('Waiting until %s', next_infraction[0].expires_at) self.inf_task.set_next_schedule(next_infraction[0].expires_at) def clear_infractions(self): expired = list(Infraction.select().where((Infraction.active == 1) & ( Infraction.expires_at < datetime.utcnow()))) for item in expired: guild = self.state.guilds.get(item.guild_id) if not guild: continue # TODO: hacky type_ = {i.index: i for i in Infraction.Types.attrs}[item.type_] if type_ == Infraction.Types.TEMPBAN: # TODO: debounce guild.delete_ban(item.user_id) elif type_ == Infraction.Types.TEMPMUTE: member = guild.get_member(item.user_id) if member: if item.metadata['role'] in member.roles: member.remove_role(item.metadata['role']) else: GuildMemberBackup.remove_role(item.guild_id, item.user_id, item.metadata['role']) # TODO: n+1 item.active = False item.save() self.queue_infractions() def restore_user(self, event, member): try: backup = GuildMemberBackup.get(guild_id=event.guild_id, user_id=member.user.id) except GuildMemberBackup.DoesNotExist: return kwargs = {} if event.config.persist.roles: roles = set(event.guild.roles.keys()) if event.config.persist.role_ids: roles &= set(event.config.persist.role_ids) roles = set(backup.roles) & roles if roles: kwargs['roles'] = list(roles) if event.config.persist.nickname and backup.nick is not None: kwargs['nick'] = backup.nick if event.config.persist.voice and (backup.mute or backup.deaf): kwargs['mute'] = backup.mute kwargs['deaf'] = backup.deaf if not kwargs: return self.bot.plugins.get('ModLogPlugin').create_debounce( event, member.user.id, 'restore') member.modify(**kwargs) self.bot.plugins.get('ModLogPlugin').log_action_ext( Actions.MEMBER_RESTORE, event) @Plugin.listen('MessageCreate') def on_message_create(self, event): if not event.config.DONT_MENTION_B1NZY: return if B1NZY_USER_ID not in event.mentions: return member = event.guild.get_member(event.author) if not member or member.roles: return duration = datetime.utcnow() + timedelta(days=7) Infraction.tempban(self, event, member, 'AUTOBAN - mentioned b1nzy', duration) event.message.reply( u'{} pinged b1nzy for some reason, they are rip now...'.format( member)) @Plugin.listen('GuildMemberRemove', priority=Priority.BEFORE) def on_guild_member_remove(self, event): if event.user.id in event.guild.members: GuildMemberBackup.create_from_member( event.guild.members.get(event.user.id)) @Plugin.listen('GuildMemberAdd') def on_guild_member_add(self, event): if not event.config.persist: return self.restore_user(event, event.member) @Plugin.listen('GuildMemberUpdate', priority=Priority.BEFORE) def on_guild_member_update(self, event): pre_member = event.guild.members.get(event.id) if not pre_member: return pre_roles = set(pre_member.roles) post_roles = set(event.roles) if pre_roles == post_roles: return removed = pre_roles - post_roles # If the user was unmuted, mark any temp-mutes as inactive if event.config.mute_role in removed: Infraction.clear_active(event, event.user.id, [Infraction.Types.TEMPMUTE]) @Plugin.listen('GuildBanRemove') def on_guild_ban_remove(self, event): Infraction.clear_active( event, event.user.id, [Infraction.Types.BAN, Infraction.Types.TEMPBAN]) @Plugin.command('unban', '<user:snowflake> [reason:str...]', level=CommandLevels.MOD) def unban(self, event, user, reason=None): try: GuildBan.get(user_id=user, guild_id=event.guild.id) event.guild.delete_ban(user) except GuildBan.DoesNotExist: raise CommandFail('user with id `{}` is not banned'.format(user)) Infraction.create(guild_id=event.guild.id, user_id=user, actor_id=event.author.id, type_=Infraction.Types.UNBAN, reason=reason) raise CommandSuccess('unbanned user with id `{}`'.format(user)) @Plugin.command('archive', group='infractions', level=CommandLevels.ADMIN) def infractions_archive(self, event): user = User.alias() actor = User.alias() q = Infraction.select(Infraction, user, actor).join( user, on=((Infraction.user_id == user.user_id).alias('user')) ).switch(Infraction).join( actor, on=((Infraction.actor_id == actor.user_id).alias('actor'))).where( Infraction.guild_id == event.guild.id) buff = StringIO() w = csv.writer(buff) for inf in q: w.writerow([ inf.id, inf.user_id, unicode(inf.user).encode('utf-8'), inf.actor_id, unicode(inf.actor).encode('utf-8'), unicode({i.index: i for i in Infraction.Types.attrs }[inf.type_]).encode('utf-8'), unicode(inf.reason).encode('utf-8'), ]) event.msg.reply('Ok, here is an archive of all infractions', attachments=[('infractions.csv', buff.getvalue())]) @Plugin.command('info', '<infraction:int>', group='infractions', level=CommandLevels.MOD) def infraction_info(self, event, infraction): try: user = User.alias() actor = User.alias() infraction = Infraction.select(Infraction, user, actor).join( user, on=((Infraction.user_id == user.user_id).alias('user')) ).switch(Infraction).join( actor, on=((Infraction.actor_id == actor.user_id).alias('actor') )).where((Infraction.id == infraction) & (Infraction.guild_id == event.guild.id)).get() except Infraction.DoesNotExist: raise CommandFail( 'cannot find an infraction with ID `{}`'.format(infraction)) type_ = {i.index: i for i in Infraction.Types.attrs}[infraction.type_] embed = MessageEmbed() if type_ in (Infraction.Types.MUTE, Infraction.Types.TEMPMUTE): embed.color = 0xfdfd96 elif type_ in (Infraction.Types.KICK, Infraction.Types.SOFTBAN): embed.color = 0xffb347 else: embed.color = 0xff6961 embed.title = str(type_).title() embed.set_thumbnail(url=infraction.user.get_avatar_url()) embed.add_field(name='User', value=unicode(infraction.user), inline=True) embed.add_field(name='Moderator', value=unicode(infraction.actor), inline=True) embed.add_field(name='Active', value='yes' if infraction.active else 'no', inline=True) if infraction.active and infraction.expires_at: embed.add_field(name='Expires', value=humanize.naturaldelta(infraction.expires_at - datetime.utcnow())) embed.add_field(name='Reason', value=infraction.reason or '_No Reason Given', inline=False) embed.timestamp = infraction.created_at.isoformat() event.msg.reply('', embed=embed) @Plugin.command('search', '[query:user|str...]', group='infractions', level=CommandLevels.MOD) def infraction_search(self, event, query=None): q = (Infraction.guild_id == event.guild.id) if query and isinstance(query, list) and isinstance( query[0], DiscoUser): query = query[0].id elif query: query = ' '.join(query) if query and (isinstance(query, int) or query.isdigit()): q &= ((Infraction.id == int(query)) | (Infraction.user_id == int(query)) | (Infraction.actor_id == int(query))) elif query: q &= (Infraction.reason**query) user = User.alias() actor = User.alias() infractions = Infraction.select(Infraction, user, actor).join( user, on=((Infraction.user_id == user.user_id).alias('user') )).switch(Infraction).join( actor, on=((Infraction.actor_id == actor.user_id ).alias('actor'))).where(q).order_by( Infraction.created_at.desc()).limit(6) tbl = MessageTable() tbl.set_header('ID', 'Created', 'Type', 'User', 'Moderator', 'Active', 'Reason') for inf in infractions: type_ = {i.index: i for i in Infraction.Types.attrs}[inf.type_] reason = inf.reason or '' if len(reason) > 256: reason = reason[:256] + '...' if inf.active: active = 'yes' if inf.expires_at: active += ' (expires in {})'.format( humanize.naturaldelta(inf.expires_at - datetime.utcnow())) else: active = 'no' tbl.add(inf.id, inf.created_at.isoformat(), str(type_), unicode(inf.user), unicode(inf.actor), active, reason) event.msg.reply(tbl.compile()) @Plugin.command('recent', aliases=['latest'], group='infractions', level=CommandLevels.MOD) def infractions_recent(self, event): # TODO: f*****g write this bruh pass @Plugin.command('duration', '<infraction:int> <duration:str>', group='infractions', level=CommandLevels.MOD) def infraction_duration(self, event, infraction, duration): try: inf = Infraction.get(id=infraction) except Infraction.DoesNotExist: raise CommandFail('invalid infraction (try `!infractions recent`)') if inf.actor_id != event.author.id and event.user_level < CommandLevels.ADMIN: raise CommandFail( 'only administrators can modify the duration of infractions created by other moderators' ) if not inf.active: raise CommandFail( 'that infraction is not active and cannot be updated') expires_dt = parse_duration(duration, inf.created_at) converted = False if inf.type_ in [ Infraction.Types.MUTE.index, Infraction.Types.BAN.index ]: inf.type_ = Infraction.Types.TEMPMUTE if inf.type_ == Infraction.Types.MUTE.index else Infraction.Types.TEMPBAN converted = True elif inf.type_ not in [ Infraction.Types.TEMPMUTE.index, Infraction.Types.TEMPBAN.index ]: raise CommandFail( 'cannot set the duration for that type of infraction') self.inf_task.set_next_schedule(expires_dt) inf.expires_at = expires_dt inf.save() if converted: raise CommandSuccess( 'ok, I\'ve made that infraction temporary, it will now expire on {}' .format(inf.expires_at.isoformat())) else: raise CommandSuccess( 'ok, I\'ve updated that infractions duration, it will now expire on {}' .format(inf.expires_at.isoformat())) @Plugin.command('reason', '<infraction:int> <reason:str...>', level=CommandLevels.MOD) def reason(self, event, infraction, reason): try: inf = Infraction.get(id=infraction) except Infraction.DoesNotExist: inf = None if inf is None or inf.guild_id != event.guild.id: event.msg.reply('Unknown infraction ID') return if not inf.actor_id: inf.actor_id = event.author.id if inf.actor_id != event.author.id and event.user_level < event.config.reason_edit_level: raise CommandFail( 'you do not have the permissions required to edit other moderators infractions' ) inf.reason = reason inf.save() raise CommandSuccess( 'I\'ve updated the reason for infraction #{}'.format(inf.id)) @Plugin.command('roles', level=CommandLevels.MOD) def roles(self, event): buff = '' for role in event.guild.roles.values(): role = S(u'{} - {}\n'.format(role.id, role.name), escape_codeblocks=True) if len(role) + len(buff) > 1990: event.msg.reply(u'```{}```'.format(buff)) buff = '' buff += role return event.msg.reply(u'```{}```'.format(buff)) @Plugin.command('restore', '<user:user>', level=CommandLevels.MOD, group='backups') def restore(self, event, user): member = event.guild.get_member(user) if member: self.restore_user(event, member) else: raise CommandFail('invalid user') @Plugin.command('clear', '<user_id:snowflake>', level=CommandLevels.MOD, group='backups') def backups_clear(self, event, user_id): deleted = bool(GuildMemberBackup.delete().where( (GuildMemberBackup.user_id == user_id) & (GuildMemberBackup.guild_id == event.guild.id)).execute()) if deleted: event.msg.reply( ':ok_hand: I\'ve cleared the member backup for that user') else: raise CommandFail( 'I couldn\t find any member backups for that user') def can_act_on(self, event, victim_id, throw=True): if event.author.id == victim_id: if not throw: return False raise CommandFail('cannot execute that action on yourself') victim_level = self.bot.plugins.get('CorePlugin').get_level( event.guild, victim_id) if event.user_level <= victim_level: if not throw: return False raise CommandFail('invalid permissions') return True @Plugin.command('mute', '<user:user|snowflake> [reason:str...]', level=CommandLevels.MOD) def mute(self, event, user, reason=None): member = event.guild.get_member(user) if member: self.can_act_on(event, member.id) if not event.config.mute_role: raise CommandFail('mute is not setup on this server') existed = False # If the user is already muted check if we can take this from a temp # to perma mute. if event.config.mute_role in member.roles: existed = Infraction.clear_active(event, member.id, [Infraction.Types.TEMPMUTE]) # The user is 100% muted and not tempmuted at this point, so lets bail if not existed: raise CommandFail(u'{} is already muted'.format( member.user)) Infraction.mute(self, event, member, reason) if event.config.confirm_actions: existed = u' [was temp-muted]' if existed else '' event.msg.reply( maybe_string( reason, u':ok_hand: {u} is now muted (`{o}`)' + existed, u':ok_hand: {u} is now muted' + existed, u=member.user, )) else: raise CommandFail('invalid user') @Plugin.command('tempmute', '<user:user|snowflake> <duration:str> [reason:str...]', level=CommandLevels.MOD) def tempmute(self, event, user, duration, reason=None): member = event.guild.get_member(user) if member: self.can_act_on(event, member.id) if not event.config.mute_role: raise CommandFail('mute is not setup on this server') if event.config.mute_role in member.roles: raise CommandFail(u'{} is already muted'.format(member.user)) expire_dt = parse_duration(duration) # Reset the infraction task so we make sure it runs after this new infraction self.inf_task.set_next_schedule(expire_dt) # Create the infraction Infraction.tempmute(self, event, member, reason, expire_dt) if event.config.confirm_actions: event.msg.reply( maybe_string( reason, u':ok_hand: {u} is now muted for {t} (`{o}`)', u':ok_hand: {u} is now muted for {t}', u=member.user, t=humanize.naturaldelta(expire_dt - datetime.utcnow()), )) else: raise CommandFail('invalid user') @Plugin.command('unmute', '<user:user|snowflake>', level=CommandLevels.MOD) def unmute(self, event, user, reason=None): # TOOD: eventually we should pull the role from the GuildMemberBackup if they arent in server member = event.guild.get_member(user) if member: self.can_act_on(event, member.id) if not event.config.mute_role: raise CommandFail('mute is not setup on this server') if event.config.mute_role not in member.roles: raise CommandFail(u'{} is not muted'.format(member.user)) Infraction.clear_active( event, member.id, [Infraction.Types.MUTE, Infraction.Types.TEMPMUTE]) self.bot.plugins.get('ModLogPlugin').create_debounce( event, member.user.id, 'unmuted', actor=unicode(event.author), roles=[event.config.mute_role]) member.remove_role(event.config.mute_role) if event.config.confirm_actions: event.msg.reply(u':ok_hand: {} is now unmuted'.format( member.user)) else: raise CommandFail('invalid user') @Plugin.command('kick', '<user:user|snowflake> [reason:str...]', level=CommandLevels.MOD) def kick(self, event, user, reason=None): member = event.guild.get_member(user) if member: self.can_act_on(event, member.id) Infraction.kick(self, event, member, reason) if event.config.confirm_actions: event.msg.reply( maybe_string( reason, u':ok_hand: kicked {u} (`{o}`)', u':ok_hand: kicked {u}', u=member.user, )) else: raise CommandFail('invalid user') @Plugin.command('mkick', parser=True, level=CommandLevels.MOD) @Plugin.parser.add_argument('users', type=long, nargs='+') @Plugin.parser.add_argument('-r', '--reason', default='', help='reason for modlog') def mkick(self, event, args): members = [] for user_id in args.users: member = event.guild.get_member(user_id) if not member: # TODO: this sucks, batch these raise CommandFail( 'failed to kick {}, user not found'.format(user_id)) if not self.can_act_on(event, member.id, throw=False): raise CommandFail( 'failed to kick {}, invalid permissions'.format(user_id)) members.append(member) msg = event.msg.reply('Ok, kick {} users for `{}`?'.format( len(members), args.reason or 'no reason')) msg.chain(False).\ add_reaction(GREEN_TICK_EMOJI).\ add_reaction(RED_TICK_EMOJI) try: mra_event = self.wait_for_event( 'MessageReactionAdd', message_id=msg.id, conditional=lambda e: (e.emoji.id in (GREEN_TICK_EMOJI_ID, RED_TICK_EMOJI_ID) and e. user_id == event.author.id)).get(timeout=10) except gevent.Timeout: return finally: msg.delete() if mra_event.emoji.id != GREEN_TICK_EMOJI_ID: return for member in members: Infraction.kick(self, event, member, args.reason) raise CommandSuccess('kicked {} users'.format(len(members))) @Plugin.command('ban', '<user:user|snowflake> [reason:str...]', level=CommandLevels.MOD) @Plugin.command('forceban', '<user:snowflake> [reason:str...]', level=CommandLevels.MOD) def ban(self, event, user, reason=None): member = None if isinstance(user, (int, long)): self.can_act_on(event, user) Infraction.ban(self, event, user, reason, guild=event.guild) else: member = event.guild.get_member(user) if member: self.can_act_on(event, member.id) Infraction.ban(self, event, member, reason, guild=event.guild) else: raise CommandFail('invalid user') if event.config.confirm_actions: event.msg.reply( maybe_string( reason, u':ok_hand: banned {u} (`{o}`)', u':ok_hand: banned {u}', u=member.user if member else user, )) @Plugin.command('softban', '<user:user|snowflake> [reason:str...]', level=CommandLevels.MOD) def softban(self, event, user, reason=None): """ Ban then unban a user from the server (with an optional reason for the modlog) """ member = event.guild.get_member(user) if member: self.can_act_on(event, member.id) Infraction.softban(self, event, member, reason) if event.config.confirm_actions: event.msg.reply( maybe_string( reason, u':ok_hand: soft-banned {u} (`{o}`)', u':ok_hand: soft-banned {u}', u=member.user, )) else: raise CommandFail('invald user') @Plugin.command('tempban', '<user:user|snowflake> <duration:str> [reason:str...]', level=CommandLevels.MOD) def tempban(self, event, duration, user, reason=None): member = event.guild.get_member(user) if member: self.can_act_on(event, member.id) expires_dt = parse_duration(duration) self.inf_task.set_next_schedule(expires_dt) Infraction.tempban(self, event, member, reason, expires_dt) if event.config.confirm_actions: event.msg.reply( maybe_string( reason, u':ok_hand: temp-banned {u} for {t} (`{o}`)', u':ok_hand: temp-banned {u} for {t}', u=member.user, t=humanize.naturaldelta(expires_dt - datetime.utcnow()), )) else: raise CommandFail('invalid user') @Plugin.command('archive here', '[size:int]', level=CommandLevels.MOD, context={'mode': 'all'}) @Plugin.command('archive all', '[size:int]', level=CommandLevels.MOD, context={'mode': 'all'}) @Plugin.command('archive user', '<user:user|snowflake> [size:int]', level=CommandLevels.MOD, context={'mode': 'user'}) @Plugin.command('archive channel', '<channel:channel|snowflake> [size:int]', level=CommandLevels.MOD, context={'mode': 'channel'}) def archive(self, event, size=50, mode=None, user=None, channel=None): if 0 > size >= 15000: raise CommandFail('too many messages must be between 1-15000') q = Message.select(Message.id).join(User).order_by( Message.id.desc()).limit(size) if mode in ('all', 'channel'): q = q.where((Message.channel_id == (channel or event.channel).id)) else: q = q.where((Message.author_id == (user if isinstance(user, (int, long)) else user.id)) & (Message.guild_id == event.guild.id)) archive = MessageArchive.create_from_message_ids([i.id for i in q]) event.msg.reply('OK, archived {} messages at {}'.format( len(archive.message_ids), archive.url)) @Plugin.command('clean cancel', level=CommandLevels.MOD) def clean_cacnel(self, event): if event.channel.id not in self.cleans: raise CommandFail('no clean is running in this channel') self.cleans[event.channel.id].kill() event.msg.reply('Ok, the running clean was cancelled') @Plugin.command('clean all', '[size:int]', level=CommandLevels.MOD, context={'mode': 'all'}) @Plugin.command('clean bots', '[size:int]', level=CommandLevels.MOD, context={'mode': 'bots'}) @Plugin.command('clean user', '<user:user> [size:int]', level=CommandLevels.MOD, context={'mode': 'user'}) def clean(self, event, user=None, size=25, typ=None, mode='all'): """ Removes messages """ if 0 > size >= 10000: raise CommandFail('too many messages must be between 1-10000') if event.channel.id in self.cleans: raise CommandFail('a clean is already running on this channel') query = Message.select(Message.id).where( (Message.deleted >> False) & (Message.channel_id == event.channel.id) & (Message.timestamp > (datetime.utcnow() - timedelta(days=13)))).join(User).order_by( Message.timestamp.desc()).limit(size) if mode == 'bots': query = query.where((User.bot >> True)) elif mode == 'user': query = query.where((User.user_id == user.id)) messages = [i[0] for i in query.tuples()] if len(messages) > 100: msg = event.msg.reply( 'Woah there, that will delete a total of {} messages, please confirm.' .format(len(messages))) msg.chain(False).\ add_reaction(GREEN_TICK_EMOJI).\ add_reaction(RED_TICK_EMOJI) try: mra_event = self.wait_for_event( 'MessageReactionAdd', message_id=msg.id, conditional=lambda e: (e.emoji.id in (GREEN_TICK_EMOJI_ID, RED_TICK_EMOJI_ID) and e.user_id == event.author.id)).get(timeout=10) except gevent.Timeout: return finally: msg.delete() if mra_event.emoji.id != GREEN_TICK_EMOJI_ID: return event.msg.reply( ':wastebasket: Ok please hold on while I delete those messages...' ).after(5).delete() def run_clean(): for chunk in chunks(messages, 100): self.client.api.channels_messages_delete_bulk( event.channel.id, chunk) self.cleans[event.channel.id] = gevent.spawn(run_clean) self.cleans[event.channel.id].join() del self.cleans[event.channel.id] @Plugin.command('add', '<user:user> <role:str> [reason:str...]', level=CommandLevels.MOD, context={'mode': 'add'}, group='role') @Plugin.command('rmv', '<user:user> <role:str> [reason:str...]', level=CommandLevels.MOD, context={'mode': 'remove'}, group='role') @Plugin.command('remove', '<user:user> <role:str> [reason:str...]', level=CommandLevels.MOD, context={'mode': 'remove'}, group='role') def role_add(self, event, user, role, reason=None, mode=None): role_obj = None if role.isdigit() and int(role) in event.guild.roles.keys(): role_obj = event.guild.roles[int(role)] else: # First try exact match exact_matches = [ i for i in event.guild.roles.values() if i.name.lower().replace(' ', '') == role.lower() ] if len(exact_matches) == 1: role_obj = exact_matches[0] else: # Otherwise we fuzz it up rated = sorted( [(fuzz.partial_ratio(role, r.name.replace(' ', '')), r) for r in event.guild.roles.values()], key=lambda i: i[0], reverse=True) if rated[0][0] > 40: if len(rated) == 1: role_obj = rated[0][1] elif rated[0][0] - rated[1][0] > 20: role_obj = rated[0][1] if not role_obj: raise CommandFail( 'too many matches for that role, try something more exact or the role ID' ) author_member = event.guild.get_member(event.author) highest_role = sorted( [event.guild.roles.get(r) for r in author_member.roles], key=lambda i: i.position, reverse=True) if not author_member.owner and ( not highest_role or highest_role[0].position < role_obj.position): raise CommandFail( 'you can only {} roles that are ranked lower than your highest role' .format(mode)) member = event.guild.get_member(user) if not member: raise CommandFail('invalid member') self.can_act_on(event, member.id) if mode == 'add' and role_obj.id in member.roles: raise CommandFail(u'{} already has the {} role'.format( member, role_obj.name)) elif mode == 'remove' and role_obj.id not in member.roles: return CommandFail(u'{} doesn\'t have the {} role'.format( member, role_obj.name)) self.bot.plugins.get('ModLogPlugin').create_debounce( event, member.user.id, mode + '_role', actor=event.author, reason=reason or 'no reason') if mode == 'add': member.add_role(role_obj.id) else: member.remove_role(role_obj.id) event.msg.reply(u':ok_hand: {} role {} to {}'.format( 'added' if mode == 'add' else 'removed', role_obj.name, member)) @Plugin.command('stats', '<user:user>', level=CommandLevels.MOD) def msgstats(self, event, user): # Query for the basic aggregate message statistics q = list( Message.select( fn.Count('*'), fn.Sum(fn.char_length(Message.content)), fn.Sum(fn.array_length(Message.emojis, 1)), fn.Sum(fn.array_length(Message.mentions, 1)), fn.Sum(fn.array_length(Message.attachments, 1)), ).where((Message.author_id == user.id)).tuples())[0] reactions_given = list( Reaction.select( fn.Count('*'), Reaction.emoji_id, Reaction.emoji_name, ).join(Message, on=(Message.id == Reaction.message_id)).where( (Reaction.user_id == user.id)).group_by( Reaction.emoji_id, Reaction.emoji_name).order_by( fn.Count('*').desc()).tuples()) # Query for most used emoji emojis = list( Message.raw( ''' SELECT gm.emoji_id, gm.name, count(*) FROM ( SELECT unnest(emojis) as id FROM messages WHERE author_id=%s ) q JOIN guildemojis gm ON gm.emoji_id=q.id GROUP BY 1, 2 ORDER BY 3 DESC LIMIT 1 ''', (user.id, )).tuples()) deleted = Message.select().where((Message.author_id == user.id) & (Message.deleted == 1)).count() embed = MessageEmbed() embed.fields.append( MessageEmbedField(name='Total Messages Sent', value=q[0] or '0', inline=True)) embed.fields.append( MessageEmbedField(name='Total Characters Sent', value=q[1] or '0', inline=True)) embed.fields.append( MessageEmbedField(name='Total Deleted Messages', value=deleted or '0', inline=True)) embed.fields.append( MessageEmbedField(name='Total Custom Emoji\'s', value=q[2] or '0', inline=True)) embed.fields.append( MessageEmbedField(name='Total Mentions', value=q[3] or '0', inline=True)) embed.fields.append( MessageEmbedField(name='Total Attachments', value=q[4] or '0', inline=True)) embed.fields.append( MessageEmbedField(name='Total Reactions', value=sum(i[0] for i in reactions_given), inline=True)) if reactions_given: emoji = reactions_given[0][ 2] if not reactions_given[0][1] else '<:{}:{}>'.format( reactions_given[0][2], reactions_given[0][1]) embed.fields.append( MessageEmbedField(name='Most Used Reaction', value=u'{} (used {} times)'.format( emoji, reactions_given[0][0], ), inline=True)) if emojis: embed.fields.append( MessageEmbedField( name='Most Used Emoji', value=u'<:{1}:{0}> (`{1}`, used {2} times)'.format( *emojis[0]))) embed.thumbnail = MessageEmbedThumbnail(url=user.avatar_url) embed.color = get_dominant_colors_user(user) event.msg.reply('', embed=embed) @Plugin.command('emojistats', '<mode:str> <sort:str>', level=CommandLevels.MOD) def emojistats_custom(self, event, mode, sort): if mode not in ('server', 'global'): raise CommandFail( 'invalid emoji mode, must be `server` or `global`') if sort not in ('least', 'most'): raise CommandFail('invalid emoji sort, must be `least` or `most`') order = 'DESC' if sort == 'most' else 'ASC' if mode == 'server': q = CUSTOM_EMOJI_STATS_SERVER_SQL.format(order, guild=event.guild.id) else: q = CUSTOM_EMOJI_STATS_GLOBAL_SQL.format(order, guild=event.guild.id) q = list(GuildEmoji.raw(q).tuples()) tbl = MessageTable() tbl.set_header('Count', 'Name', 'ID') for emoji_id, name, count in q: tbl.add(count, name, emoji_id) event.msg.reply(tbl.compile()) @Plugin.command('prune', '[uses:int]', level=CommandLevels.ADMIN, group='invites') def invites_prune(self, event, uses=1): invites = [ i for i in event.guild.get_invites() if i.uses <= uses and i.created_at < (datetime.utcnow() - timedelta(hours=1)) ] if not invites: return event.msg.reply( 'I didn\'t find any invites matching your criteria') msg = event.msg.reply( 'Ok, a total of {} invites created by {} users with {} total uses would be pruned.' .format(len(invites), len({i.inviter.id for i in invites}), sum(i.uses for i in invites))) msg.chain(False).\ add_reaction(GREEN_TICK_EMOJI).\ add_reaction(RED_TICK_EMOJI) try: mra_event = self.wait_for_event( 'MessageReactionAdd', message_id=msg.id, conditional=lambda e: (e.emoji.id in (GREEN_TICK_EMOJI_ID, RED_TICK_EMOJI_ID) and e. user_id == event.author.id)).get(timeout=10) except gevent.Timeout: msg.reply('Not executing invite prune') msg.delete() return msg.delete() if mra_event.emoji.id == GREEN_TICK_EMOJI_ID: msg = msg.reply('Pruning invites...') for invite in invites: invite.delete() msg.edit('Ok, invite prune completed') else: msg = msg.reply('Not pruning invites') @Plugin.command('clean', '<user:user|snowflake> [count:int] [emoji:str]', level=CommandLevels.MOD, group='reactions') def reactions_clean(self, event, user, count=10, emoji=None): if isinstance(user, DiscoUser): user = user.id if count > 50: raise CommandFail('cannot clean more than 50 reactions') lock = rdb.lock('clean-reactions-{}'.format(user)) if not lock.acquire(blocking=False): raise CommandFail('already running a clean on user') query = [ (Reaction.user_id == user), (Message.guild_id == event.guild.id), (Message.deleted == 0), ] if emoji: emoji_id = EMOJI_RE.findall(emoji) if emoji_id: query.append((Reaction.emoji_id == emoji_id[0])) else: # TODO: validation? query.append((Reaction.emoji_name == emoji)) try: reactions = list( Reaction.select( Reaction.message_id, Reaction.emoji_id, Reaction.emoji_name, Message.channel_id, ).join( Message, on=(Message.id == Reaction.message_id), ).where(reduce(operator.and_, query)).order_by( Reaction.message_id.desc()).limit(count).tuples()) if not reactions: raise CommandFail('no reactions to purge') msg = event.msg.reply('Hold on while I clean {} reactions'.format( len(reactions))) for message_id, emoji_id, emoji_name, channel_id in reactions: if emoji_id: emoji = '{}:{}'.format(emoji_name, emoji_id) else: emoji = emoji_name self.client.api.channels_messages_reactions_delete( channel_id, message_id, emoji, user) msg.edit('Ok, I cleaned {} reactions'.format(len(reactions), )) finally: lock.release() @Plugin.command('log', '<user:user|snowflake>', group='voice', level=CommandLevels.MOD) def voice_log(self, event, user): if isinstance(user, DiscoUser): user = user.id sessions = GuildVoiceSession.select( GuildVoiceSession.user_id, GuildVoiceSession.channel_id, GuildVoiceSession.started_at, GuildVoiceSession.ended_at).where( (GuildVoiceSession.user_id == user) & (GuildVoiceSession.guild_id == event.guild.id)).order_by( GuildVoiceSession.started_at.desc()).limit(10) tbl = MessageTable() tbl.set_header('Channel', 'Joined At', 'Duration') for session in sessions: tbl.add( unicode( self.state.channels.get(session.channel_id) or 'UNKNOWN'), '{} ({} ago)'.format( session.started_at.isoformat(), humanize.naturaldelta(datetime.utcnow() - session.started_at)), humanize.naturaldelta(session.ended_at - session.started_at) if session.ended_at else 'Active') event.msg.reply(tbl.compile())
class UtilitiesPlugin(Plugin): def load(self, ctx): super(UtilitiesPlugin, self).load(ctx) self.reminder_task = Eventual(self.trigger_reminders) self.spawn_later(10, self.queue_reminders) self.server_owners = { '166304313004523520': "airplane, PokeBlobs", # OGNovuh#0003 '175805198017626114': "Swagger's Hangout", # SwaggerSouls#4295 '127060170802069505': "Hotel Fitz", # Fitz#9588 '107941250228830208': "Jameskii", # Jameskii#0001 '197316087879172096': "Daniel's Discord", # danielharrison#0001 '226912511302041601': "xd Official Discord", # McCreamy#6793 '265986744019714050': "Dooocord", # TheDooo#4877 '202271067912404992': "Donut Operator's Discord", # DonutOperator#5512 } self.server_managers = { '158200017906171904': "Hotel Fitz", # Xentiran#0007 '191793155685744640': "Hotel Fitz, Jameskii, Swagger's Hangout", # Terminator966#0966 '227888061353164810': "Hotel Fitz", # emily#2900 '166304313004523520': "Jameskii", # OGNovuh#0003 '324645959265157120': "Swagger's Hangout", # Mushy The Wizard#2319 '285238487618420737': "Swagger's Hangout", # Boubie#0305 '244907038667177984': "Swagger's Hangout", # Kata#7886 '194903108495605760': "Swagger's Hangout", # Solaire#4156 '138842652585099264': "Swagger's Hangout", # Xeano#4444 '285712318464000000': "xd Official Discord", # Nuna#0001 '340753167098839051': "Donut Operator's Discord", # Valkyrie#5555 '339254723091890177': "Donut Operator's Discord", # AmazonPrimes#7409 } def queue_reminders(self): try: next_reminder = Reminder.select().order_by( Reminder.remind_at.asc() ).limit(1).get() except Reminder.DoesNotExist: return self.reminder_task.set_next_schedule(next_reminder.remind_at) @Plugin.command('coin', group='random', global_=True) def coin(self, event): """ Flip a coin """ event.msg.reply(random.choice(['heads', 'tails'])) @Plugin.command('number', '[end:int] [start:int]', group='random', global_=True) def random_number(self, event, end=10, start=0): """ Returns a random number """ # Because someone will be an idiot if end > 9223372036854775807: return event.msg.reply(':warning: ending number too big!') if end <= start: return event.msg.reply(':warning: ending number must be larger than starting number!') event.msg.reply(str(random.randint(start, end))) @Plugin.command('cat', global_=True) def cat(self, event): # Sometimes random.cat gives us gifs (smh) for _ in range(3): try: r = requests.get('http://aws.random.cat/meow') r.raise_for_status() except: continue url = r.json()['file'] if not url.endswith('.gif'): break else: return event.msg.reply('404 cat not found :(') r = requests.get(url) r.raise_for_status() event.msg.reply('', attachments=[('cat.jpg', r.content)]) @Plugin.command('duck', aliases=['quack'], global_=True) def duck(self, event): # Sometimes random.cat gives us gifs (smh) for _ in range(3): try: r = requests.get('https://random-d.uk/api/v1/random?type=jpg') r.raise_for_status() except: continue url = r.json()['url'] if not url.endswith('.gif'): break else: return event.msg.reply('404 duck not found :(') r = requests.get(url) r.raise_for_status() event.msg.reply('', attachments=[('duck.jpg', r.content)]) @Plugin.command('bunny', aliases=['bunbun', 'wabbit', 'fluff'], global_=True) def bunny(self, event): try: r = requests.get('https://api.bunnies.io/v2/loop/random/?media=png,gif') r.raise_for_status() except: return event.msg.reply('404 bunny not found :(') media = r.json()['media'] ext = 'png' if (media['gif']): url = media['gif'] ext='gif' elif(media['png']): url = media['png'] else: return event.msg.reply('404 bunny not found :(') r = requests.get(url) try: r.raise_for_status() except requests.HTTPError as e: self.log.error('Bunny fetch failed: {}'.format(str(e))) return event.msg.reply('404 bunny not found :(') try: event.msg.reply('', attachments=[('bunny.{}'.format(ext), r.content)]) except APIException: self.bunny(event) @Plugin.command('dog', global_=True) def dog(self, event): # Sometimes random.dog gives us gifs or mp4s (smh) for _ in range(3): try: r = requests.get('https://random.dog/woof.json') r.raise_for_status() except: continue url = r.json()['url'] if not url.endswith('.gif') or not url.endswith('.mp4'): break else: return event.msg.reply('404 dog not found :(') r = requests.get(url) r.raise_for_status() event.msg.reply('', attachments=[('dog.jpg', r.content)]) @Plugin.command('bird', aliases=['birb'], global_=True) def bird(self, event): # Sometimes random.birb gives us gifs or mp4s (smh) for _ in range(3): try: r = requests.get('https://random.birb.pw/tweet.json/') r.raise_for_status() except: continue name = r.json()['file'] if not name.endswith('.gif') or not name.endswith('.mp4'): break else: return event.msg.reply('404 bird not found :(') r = requests.get('https://random.birb.pw/img/' + name) r.raise_for_status() event.msg.reply('', attachments=[('bird.jpg', r.content)]) @Plugin.command('emoji', '<emoji:str>', global_=True) def emoji(self, event, emoji): if not EMOJI_RE.match(emoji): return event.msg.reply(u'Unknown emoji: `{}`'.format(S(emoji))) fields = [] name, eid = EMOJI_RE.findall(emoji)[0] fields.append('**ID:** {}'.format(eid)) fields.append('**Name:** {}'.format(S(name))) guild = self.state.guilds.find_one(lambda v: eid in v.emojis) if guild: fields.append('**Guild:** {} ({})'.format(S(guild.name), guild.id)) anim = emoji.startswith('<a:') fields.append('**Animated:** {}'.format('Yes' if anim else 'No')) ext = 'gif' if anim else 'png' url = 'https://discordapp.com/api/emojis/{}.{}'.format(eid, ext) r = requests.get(url) r.raise_for_status() return event.msg.reply('\n'.join(fields), attachments=[('emoji.'+ext, r.content)]) @Plugin.command('jumbo', '<emojis:str...>', global_=True) def jumbo(self, event, emojis): urls = [] first_emote = emojis.split(' ')[0] if first_emote.startswith('<a:'): if EMOJI_RE.match(first_emote): eid = EMOJI_RE.findall(first_emote)[0] url = 'https://discordapp.com/api/emojis/{}.gif'.format(eid[1]) r = requests.get(url) r.raise_for_status() return event.msg.reply('', attachments=[('emoji.gif', r.content)]) else: raise CommandFail('Invalid Animated Emote.') else: for emoji in emojis.split(' ')[:5]: if emoji.startswith('<a:'): continue if EMOJI_RE.match(emoji): _, eid = EMOJI_RE.findall(emoji)[0] urls.append('https://discordapp.com/api/emojis/{}.png'.format(eid)) else: urls.append(get_emoji_url(emoji)) width, height, images = 0, 0, [] for r in Pool(6).imap(requests.get, urls): try: r.raise_for_status() except requests.HTTPError: return img = Image.open(BytesIO(r.content)) height = img.height if img.height > height else height width += img.width + 10 images.append(img) image = Image.new('RGBA', (width, height)) width_offset = 0 for img in images: image.paste(img, (width_offset, 0)) width_offset += img.width + 10 combined = BytesIO() image.save(combined, 'png', quality=55) combined.seek(0) return event.msg.reply('', attachments=[('emoji.png', combined)]) @Plugin.command('ping', level=CommandLevels.ADMIN) def command_ping(self, event): return event.msg.reply(':ping_pong: stop pinging me asshole!!!!!') @Plugin.command('seen', '<user:user>', global_=True) def seen(self, event, user): try: msg = Message.select(Message.timestamp).where( Message.author_id == user.id ).order_by(Message.timestamp.desc()).limit(1).get() except Message.DoesNotExist: return event.msg.reply(u"I've never seen {}".format(user)) event.msg.reply(u'I last saw {} {} ago (at {})'.format( user, humanize.naturaldelta(datetime.utcnow() - msg.timestamp), msg.timestamp )) @Plugin.command('search', '<query:str...>', global_=True) def search(self, event, query): queries = [] if query.isdigit(): queries.append((User.user_id == query)) q = USER_MENTION_RE.findall(query) if len(q) and q[0].isdigit(): queries.append((User.user_id == q[0])) else: queries.append((User.username ** u'%{}%'.format(query.replace('%', '')))) if '#' in query: username, discrim = query.rsplit('#', 1) if discrim.isdigit(): queries.append(( (User.username == username) & (User.discriminator == int(discrim)))) users = User.select().where(reduce(operator.or_, queries)) if len(users) == 0: return event.msg.reply(u'No users found for query `{}`'.format(S(query, escape_codeblocks=True))) if len(users) == 1: if users[0].user_id in self.state.users: return self.info(event, self.state.users.get(users[0].user_id)) return event.msg.reply(u'Found the following users for your query: ```{}```'.format( u'\n'.join(map(lambda i: u'{} ({})'.format(unicode(i), i.user_id), users[:25])) )) @Plugin.command('server', '[guild_id:snowflake]', global_=True) def server(self, event, guild_id=None): guild = self.state.guilds.get(guild_id) if guild_id else event.guild if not guild: raise CommandFail('invalid server') content = [] content.append(u'**\u276F Server Information**') created_at = to_datetime(guild.id) content.append(u'Created: {} ago ({})'.format( humanize.naturaldelta(datetime.utcnow() - created_at), created_at.isoformat(), )) content.append(u'Members: {:,d}'.format(len(guild.members))) content.append(u'Features: {}'.format(', '.join(guild.features) or 'none')) content.append(u'\n**\u276F Counts**') text_count = sum(1 for c in guild.channels.values() if not c.is_voice) voice_count = len(guild.channels) - text_count content.append(u'Roles: {}'.format(len(guild.roles))) content.append(u'Text: {}'.format(text_count)) content.append(u'Voice: {}'.format(voice_count)) content.append(u'\n**\u276F Members**') status_counts = defaultdict(int) for member in guild.members.values(): if not member.user.presence: status = Status.OFFLINE else: status = member.user.presence.status status_counts[status] += 1 for status, count in sorted(status_counts.items(), key=lambda i: str(i[0]), reverse=True): content.append(u'<{}> - {}'.format( STATUS_EMOJI[status], format(count, ',d') )) embed = MessageEmbed() if guild.icon: embed.set_thumbnail(url=guild.icon_url) embed.color = get_dominant_colors_guild(guild) embed.description = '\n'.join(content) event.msg.reply('', embed=embed) # --------------Coded by Xenthys#0001 for Rawgoat-------------- def fetch_user(self, id, raise_on_error=True): try: r = self.bot.client.api.http(Routes.USERS_GET, dict(user=id)) return DiscoUser.create(self.bot.client.api.client,r.json()) except APIException: if raise_on_error: raise CommandFail('unknown user') return @Plugin.command('info', '[user:user|snowflake]') def info(self, event, user=None): if user is None: user = event.author user_id = 0 if isinstance(user, (int, long)): user_id = user user = self.state.users.get(user) if user and not user_id: user = self.state.users.get(user.id) if not user: if user_id: user = self.fetch_user(user_id) User.from_disco_user(user) else: raise CommandFail('unknown user') content = [] content.append(u'**\u276F User Information**') content.append(u'ID: {}'.format(user.id)) content.append(u'Profile: <@{}>'.format(user.id)) if user.presence: emoji, status = get_status_emoji(user.presence) content.append('Status: {} <{}>'.format(status, emoji)) game = user.presence.game if game and game.name: activity = ['Playing', 'Stream'][int(game.type)] if game.type < 2 else None if not game.type: if game.name == 'Spotify': activity = 'Listening to' else: activity = None if activity: content.append(u'{}: {}'.format(activity, u'[{}]({})'.format(game.name, game.url) if game.url else game.name )) created_dt = to_datetime(user.id) content.append('Created: {} ago ({})'.format( humanize.naturaldelta(datetime.utcnow() - created_dt), created_dt.isoformat() )) for i in self.server_owners: if i == str(user.id): content.append('Server Ownership: {}'.format(self.server_owners[i])) for i in self.server_managers: if i == str(user.id): content.append('Community Manager: {}'.format(self.server_managers[i])) if user.id == self.state.me.id: content.append('Documentation: https://aetherya.stream/') elif rdb.sismember('global_admins', user.id): content.append('Airplane Staff: Global Administrator') elif rdb.sismember('server_managers', user.id): content.append('Server Manager') elif rdb.sismember('server_owner', user.id): content.append('Server Owner') member = event.guild.get_member(user.id) if event.guild else None if member: content.append(u'\n**\u276F Member Information**') if member.nick: content.append(u'Nickname: {}'.format(member.nick)) content.append('Joined: {} ago ({})'.format( humanize.naturaldelta(datetime.utcnow() - member.joined_at), member.joined_at.isoformat(), )) if member.roles: roles = [] for r in member.roles: roles.append(member.guild.roles.get(r)) roles = sorted(roles, key=lambda r: r.position, reverse=True) total = len(member.roles) roles = roles[:20] content.append(u'Roles ({}): {}{}'.format( total, ' '.join(r.mention for r in roles), ' (+{})'.format(total-20) if total > 20 else '' )) # Execute a bunch of queries async newest_msg = Message.select(Message.timestamp).where( (Message.author_id == user.id) & (Message.guild_id == event.guild.id) ).order_by(Message.timestamp.desc()).limit(1).async() # oldest_msg = Message.select(Message.timestamp).where( # (Message.author_id == user.id) & # (Message.guild_id == event.guild.id) # ).order_by(Message.timestamp.asc()).limit(1).async() infractions = Infraction.select( Infraction.guild_id, fn.COUNT('*') ).where( (Infraction.user_id == user.id) & (Infraction.type_ != 6) & # Unban (~(Infraction.reason ** '[NOTE]%')) ).group_by(Infraction.guild_id).tuples().async() voice = GuildVoiceSession.select( GuildVoiceSession.user_id, fn.COUNT('*'), fn.SUM(GuildVoiceSession.ended_at - GuildVoiceSession.started_at) ).where( (GuildVoiceSession.user_id == user.id) & (~(GuildVoiceSession.ended_at >> None)) ).group_by(GuildVoiceSession.user_id).tuples().async() # Wait for them all to complete (we're still going to be as slow as the # slowest query, so no need to be smart about this.) try: wait_many(newest_msg, infractions, voice, timeout=3) except gevent.Timeout: pass tags = to_tags(guild_id=event.msg.guild.id) if newest_msg.value: content.append(u'\n **\u276F Activity**') statsd.timing('sql.duration.newest_msg', newest_msg.value._query_time, tags=tags) newest_msg = newest_msg.value.get() content.append('Last Message: {} ago ({})'.format( humanize.naturaldelta(datetime.utcnow() - newest_msg.timestamp), newest_msg.timestamp.isoformat(), )) # if oldest_msg.value: # statsd.timing('sql.duration.oldest_msg', oldest_msg.value._query_time, tags=tags) # oldest_msg = oldest_msg.value.get() # content.append('First Message: {} ago ({})'.format( # humanize.naturaldelta(datetime.utcnow() - oldest_msg.timestamp), # oldest_msg.timestamp.isoformat(), # )) if infractions.value: statsd.timing('sql.duration.infractions', infractions.value._query_time, tags=tags) infractions = list(infractions.value) total = sum(i[1] for i in infractions) content.append(u'\n**\u276F Infractions**') content.append('Total Infractions: {}'.format(total)) content.append('Unique Servers: {}'.format(len(infractions))) if voice.value: statsd.timing('plugin.utilities.info.sql.voice', voice.value._query_time, tags=tags) voice = list(voice.value) content.append(u'\n**\u276F Voice**') content.append(u'Sessions: {}'.format(voice[0][1])) content.append(u'Time: {}'.format(humanize.naturaldelta( voice[0][2] ))) embed = MessageEmbed() avatar = user.avatar if avatar: avatar = u'https://cdn.discordapp.com/avatars/{}/{}.{}'.format( user.id, avatar, u'gif' if avatar.startswith('a_') else u'png' ) else: avatar = u'https://cdn.discordapp.com/embed/avatars/{}.png'.format( int(user.discriminator) % 5 ) embed.set_author(name=u'{}#{}'.format( user.username, user.discriminator, ), icon_url=avatar) embed.set_thumbnail(url=avatar) embed.description = '\n'.join(content) try: embed.color = get_dominant_colors_user(user, avatar) except: pass event.msg.reply('', embed=embed) # --------------Coded by Xenthys#0001 for Rawgoat-------------- def trigger_reminders(self): reminders = Reminder.with_message_join().where( (Reminder.remind_at < (datetime.utcnow() + timedelta(seconds=1))) ) waitables = [] for reminder in reminders: waitables.append(self.spawn(self.trigger_reminder, reminder)) for waitable in waitables: waitable.join() self.queue_reminders() def trigger_reminder(self, reminder): message = reminder.message_id channel = self.state.channels.get(message.channel_id) if not channel: self.log.warning('Not triggering reminder, channel %s was not found!', message.channel_id) reminder.delete_instance() return msg = channel.send_message(u'<@{}> you asked me at {} ({} ago) to remind you about: {}'.format( message.author_id, reminder.created_at, humanize.naturaldelta(reminder.created_at - datetime.utcnow()), S(reminder.content) )) # Add the emoji options msg.add_reaction(SNOOZE_EMOJI) msg.add_reaction(GREEN_TICK_EMOJI) try: mra_event = self.wait_for_event( 'MessageReactionAdd', message_id=msg.id, conditional=lambda e: ( (e.emoji.name == SNOOZE_EMOJI or e.emoji.id == GREEN_TICK_EMOJI_ID) and e.user_id == message.author_id ) ).get(timeout=30) except gevent.Timeout: reminder.delete_instance() return finally: # Cleanup msg.delete_reaction(SNOOZE_EMOJI) msg.delete_reaction(GREEN_TICK_EMOJI) if mra_event.emoji.name == SNOOZE_EMOJI: reminder.remind_at = datetime.utcnow() + timedelta(minutes=20) reminder.save() msg.edit(u'Ok, I\'ve snoozed that reminder for 20 minutes.') return reminder.delete_instance() @Plugin.command('clear', group='r', global_=True) def cmd_remind_clear(self, event): count = Reminder.delete_for_user(event.author.id) return event.msg.reply(':ok_hand: I cleared {} reminders for you'.format(count)) @Plugin.command('add', '<duration:str> <content:str...>', group='r', global_=True) @Plugin.command('remind', '<duration:str> <content:str...>', global_=True) def cmd_remind(self, event, duration, content): if Reminder.count_for_user(event.author.id) > 30: return event.msg.reply(':warning: you an only have 15 reminders going at once!') remind_at = parse_duration(duration) if remind_at > (datetime.utcnow() + timedelta(seconds=5 * YEAR_IN_SEC)): return event.msg.reply(':warning: thats too far in the future, I\'ll forget!') r = Reminder.create( message_id=event.msg.id, remind_at=remind_at, content=content ) self.reminder_task.set_next_schedule(r.remind_at) event.msg.reply(':ok_hand: I\'ll remind you at {} ({})'.format( r.remind_at.isoformat(), humanize.naturaldelta(r.remind_at - datetime.utcnow()), )) @Plugin.command('messageinfo', '<mid:snowflake>', level=CommandLevels.MOD) def messageinfo(self, event, mid): try: msg = Message.select(Message).where( (Message.id == mid) ).get() except Message.DoesNotExist: raise CommandFail('the id specified does not exist in our message database.') message_content = msg.content author_id = msg.author.id guild_id = msg.guild_id channel_id = msg.channel_id deleted = msg.deleted num_edits = msg.num_edits if num_edits > 0: num_edits_bool = True else: num_edits_bool = False discrim = str(msg.author.discriminator) # if len(str(cached_name[1])) != 4: # while len(str(temp_str)) < 4: # temp_str = '0' + str(temp_str) cached_name = str(msg.author.username) + '#' + str(discrim) avatar_name = msg.author.avatar content = [] embed = MessageEmbed() member = event.guild.get_member(author_id) if not avatar_name: if member: avatar = default_color(str(member.user.default_avatar)) else: avatar = None elif avatar_name.startswith('a_'): avatar = u'https://cdn.discordapp.com/avatars/{}/{}.gif'.format(author_id, avatar_name) else: avatar = u'https://cdn.discordapp.com/avatars/{}/{}.png'.format(author_id, avatar_name) if member: embed.set_author(name='{} ({})'.format(member.user, member.id), icon_url=avatar) embed.set_thumbnail(url=avatar) else: if avatar: embed.set_author(name='{} ({})'.format(cached_name, author_id), icon_url=avatar) embed.set_thumbnail(url=avatar) else: embed.set_author(name='{} ({})'.format(cached_name, author_id)) # embed.title = "Message Content:" content.append(u'**\u276F Message Information:**') content.append(u'In channel: <#{}>'.format(channel_id)) content.append(u'Edited: **{}**'.format(num_edits_bool)) if deleted: content.append(u'Deleted: **{}**'.format(deleted)) content.append(u'Content: ```{}```'.format(message_content)) if member: content.append(u'\n**\u276F Member Information**') if member.nick: content.append(u'Nickname: {}'.format(member.nick)) content.append('Joined: {} ago ({})'.format( humanize.naturaldelta(datetime.utcnow() - member.joined_at), member.joined_at.isoformat(), )) if member.roles: roles = [] for r in member.roles: roles.append(member.guild.roles.get(r)) roles = sorted(roles, key=lambda r: r.position, reverse=True) total = len(member.roles) roles = roles[:20] content.append(u'Roles ({}): {}{}'.format( total, ' '.join(r.mention for r in roles), ' (+{})'.format(total-20) if total > 20 else '' )) embed.description = '\n'.join(content) # embed.url = 'https://discordapp.com/channels/{}/{}/{}'.format(guild_id, channel_id, mid) embed.timestamp = datetime.utcnow().isoformat() if not event.author.avatar: auth_avatar = default_color(str(member.user.default_avatar)) elif event.author.avatar.startswith('a_'): auth_avatar = u'https://cdn.discordapp.com/avatars/{}/{}.gif'.format(event.author.id, event.author.avatar) else: auth_avatar = u'https://cdn.discordapp.com/avatars/{}/{}.png'.format(event.author.id, event.author.avatar) embed.set_footer(text='Requested by {}#{} ({})'.format(event.author.username, event.author.discriminator, event.author.id), icon_url=auth_avatar) try: embed.color = get_dominant_colors_user(member.user, avatar) except: embed.color = '00000000' event.msg.reply('', embed=embed) @Plugin.command('manager add', '<user:user> <item:str...>', level=-1, context={'mode': 'add'}) @Plugin.command('manager remove', '<user:user> <item:str...>', level=-1, context={'mode': 'remove'}) def manager_info(self, event, user, mode, item): user = self.state.users.get(user) if mode == 'add': special_list = rdb.hget('ServerManagers', '{}'.format(user.id)) temp_list = [] temp_list.append(item) final_list = str(temp_list).strip('[]') new = str('{}, {}'.format(special_list, final_list)) rdb.hset('ServerManagers', '{}'.format(user.id), new) raise CommandSuccess('{} has been added to the list of server managers'.format(user)) if mode == 'remove': special_list = rdb.hget('ServerManagers', '{}'.format(user.id)) if special_list == None: raise CommandFail('User is not a manager on any Airplane protected servers.') temp_list = special_list.split(', ') found = False for x in temp_list: if x == item: found = True temp_list.remove(item) if found == False: raise CommandFail('something went wrong, please try again later') else: new = str(temp_list).strip('[]') rdb.hset('ServerManagers', '{}'.format(user.id), new) raise CommandSuccess('The server has been removed from the list of servers the user manages.')