async def nudge_channel(message, stripped, preferences): time_parser = TimeExtractor(stripped, preferences.timezone) try: t = time_parser.extract_displacement() except InvalidTime: await message.channel.send(embed=discord.Embed( description=preferences.language.get_string('nudge/invalid_time'))) else: if 2 ** 15 > t > -2 ** 15: channel, _ = Channel.get_or_create(message.channel) channel.nudge = t session.commit() await message.channel.send( embed=discord.Embed(description=preferences.language.get_string('nudge/success').format(t))) else: await message.channel.send( embed=discord.Embed(description=preferences.language.get_string('nudge/invalid_time')))
async def blacklist(message, _, preferences): target_channel = message.channel_mentions[0] if len(message.channel_mentions) > 0 else message.channel channel, _ = Channel.get_or_create(target_channel) channel.blacklisted = not channel.blacklisted if channel.blacklisted: await message.channel.send( embed=discord.Embed(description=preferences.language.get_string('blacklist/added'))) else: await message.channel.send( embed=discord.Embed(description=preferences.language.get_string('blacklist/removed'))) session.commit()
async def on_message(self, message): member, _ = Member.get_or_create( discordID=message.author.id, defaults={"name": message.author.display_name} ) name = ( message.channel.name if isinstance(message.channel, discord.TextChannel) else "DM" ) channel, _ = Channel.get_or_create( discordID=message.channel.id, defaults={"name": name} ) message = Message.create( discordID=message.id, member=member, channel=channel, message=message.content, # embed = str(message.embeds), )
async def pause_channel(self, message, stripped, preferences): channel, _ = Channel.get_or_create(message.channel) if len(stripped) > 0: # argument provided for time time_parser = TimeExtractor(stripped, preferences.timezone) try: t = time_parser.extract_displacement() except InvalidTime: await message.channel.send(embed=discord.Embed( description=preferences.language['pause/invalid_time'])) else: channel.paused = True channel.paused_until = datetime.now() + timedelta(seconds=t) display = channel.paused_until \ .astimezone(pytz.timezone(preferences.timezone)) \ .strftime('%Y-%m-%d, %H:%M:%S') await message.channel.send( embed=discord.Embed(description=preferences.language['pause/paused_until'].format(display))) else: # otherwise toggle the paused status and clear the time channel.paused = not channel.paused channel.paused_until = None if channel.paused: await message.channel.send( embed=discord.Embed(description=preferences.language['pause/paused_indefinite'])) else: await message.channel.send( embed=discord.Embed(description=preferences.language['pause/unpaused']))
async def todo_command(message, stripped, preferences, command): if command == 'todos': location, _ = Channel.get_or_create(message.channel) name = 'Channel' todos = location.todo_list.all() + preferences.guild.todo_list.filter(Todo.channel_id.is_(None)).all() channel = location guild = preferences.guild else: location = preferences.user name = 'Your' todos = location.todo_list.filter(Todo.guild_id.is_(None)).all() channel = None guild = None splits = stripped.split(' ') if len(splits) == 1 and splits[0] == '': msg = ['\n{}: {}'.format(i, todo.value) for i, todo in enumerate(todos, start=1)] if len(msg) == 0: msg.append(preferences.language.get_string('todo/add').format( prefix=preferences.prefix, command=command)) s = '' for item in msg: if len(item) + len(s) < 2048: s += item else: await message.channel.send(embed=discord.Embed(title='{} TODO'.format(name), description=s)) s = '' if len(s) > 0: await message.channel.send(embed=discord.Embed(title='{} TODO'.format(name), description=s)) elif len(splits) >= 2: if splits[0] == 'add': s = ' '.join(splits[1:]) todo = Todo(value=s, guild=guild, user=preferences.user, channel=channel) session.add(todo) await message.channel.send(preferences.language.get_string('todo/added').format(name=s)) elif splits[0] == 'remove': try: todo = session.query(Todo).filter(Todo.id == todos[int(splits[1]) - 1].id).first() session.query(Todo).filter(Todo.id == todos[int(splits[1]) - 1].id).delete( synchronize_session='fetch') await message.channel.send(preferences.language.get_string('todo/removed').format(todo.value)) except ValueError: await message.channel.send( preferences.language.get_string('todo/error_value').format( prefix=preferences.prefix, command=command)) except IndexError: await message.channel.send(preferences.language.get_string('todo/error_index')) else: await message.channel.send( preferences.language.get_string('todo/help').format(prefix=preferences.prefix, command=command)) else: if stripped == 'clear': todos.clear() await message.channel.send(preferences.language.get_string('todo/cleared')) else: await message.channel.send( preferences.language.get_string('todo/help').format(prefix=preferences.prefix, command=command)) session.commit()
async def create_reminder(self, message: discord.Message, location: int, text: str, time: int, interval: typing.Optional[int] = None, method: str = 'natural') -> ReminderInformation: ut: float = unix_time() if time > ut + MAX_TIME: return ReminderInformation(CreateReminderResponse.LONG_TIME) elif time < ut: if (ut - time) < 10: time = int(ut) else: return ReminderInformation(CreateReminderResponse.PAST_TIME) channel: typing.Optional[Channel] = None user: typing.Optional[User] = None creator: User = User.from_discord(message.author) # noinspection PyUnusedLocal discord_channel: typing.Optional[typing.Union[discord.TextChannel, DMChannelId]] = None # command fired inside a guild if message.guild is not None: discord_channel = message.guild.get_channel(location) if discord_channel is not None: # if not a DM reminder channel, _ = Channel.get_or_create(discord_channel) await channel.attach_webhook(discord_channel) time += channel.nudge else: user = await self.find_and_create_member(location, message.guild) if user is None: return ReminderInformation(CreateReminderResponse.INVALID_TAG) discord_channel = DMChannelId(user.dm_channel, user.user) # command fired in a DM; only possible target is the DM itself else: user = User.from_discord(message.author) discord_channel = DMChannelId(user.dm_channel, message.author.id) if interval is not None: if MIN_INTERVAL > interval: return ReminderInformation(CreateReminderResponse.SHORT_INTERVAL) elif interval > MAX_TIME: return ReminderInformation(CreateReminderResponse.LONG_INTERVAL) else: # noinspection PyArgumentList reminder = Reminder( message=Message(content=text), channel=channel or user.channel, time=time, enabled=True, method=method, interval=interval, set_by=creator.id) session.add(reminder) session.commit() else: # noinspection PyArgumentList reminder = Reminder( message=Message(content=text), channel=channel or user.channel, time=time, enabled=True, method=method, set_by=creator.id) session.add(reminder) session.commit() return ReminderInformation(CreateReminderResponse.OK, channel=discord_channel, time=time)
async def on_message(self, message): def _check_self_permissions(_channel): p = _channel.permissions_for(message.guild.me) return p.send_messages and p.embed_links async def _get_user(_message): _user = session.query(User).filter(User.user == message.author.id).first() if _user is None: dm_channel_id = (await message.author.create_dm()).id c = session.query(Channel).filter(Channel.channel == dm_channel_id).first() if c is None: c = Channel(channel=dm_channel_id) session.add(c) session.flush() _user = User(user=_message.author.id, dm_channel=c.id, name='{}#{}'.format( _message.author.name, _message.author.discriminator)) session.add(_user) session.flush() return _user if (message.author.bot and self.config.ignore_bots) or \ message.content is None or \ message.tts or \ len(message.attachments) > 0 or \ self.match_string is None: # either a bot or cannot be a command return elif message.guild is None: # command has been DMed. dont check for prefix :) split = message.content.split(' ') command_word = split[0].lower() if len(command_word) > 0: if command_word[0] == '$': command_word = command_word[1:] args = ' '.join(split[1:]).strip() if command_word in self.command_names: command = self.commands[command_word] if command.allowed_dm: # get user user = await _get_user(message) await command.func(message, args, Preferences(None, user)) elif _check_self_permissions(message.channel): # command sent in guild. check for prefix & call match = re.match( self.match_string, message.content, re.MULTILINE | re.DOTALL | re.IGNORECASE ) if match is not None: # matched command structure; now query for guild to compare prefix guild = session.query(Guild).filter(Guild.guild == message.guild.id).first() if guild is None: guild = Guild(guild=message.guild.id) session.add(guild) session.flush() # if none, suggests mention has been provided instead since pattern still matched if (prefix := match.group('prefix')) in (guild.prefix, None): # prefix matched, might as well get the user now since this is a very small subset of messages user = await _get_user(message) if guild not in user.guilds: guild.users.append(user) # create the nice info manager info = Preferences(guild, user) command_word = match.group('cmd').lower() stripped = match.group('args') or '' command = self.commands[command_word] # some commands dont get blacklisted e.g help, blacklist if command.blacklists: channel, just_created = Channel.get_or_create(message.channel) if channel.guild_id is None: channel.guild_id = guild.id await channel.attach_webhook(message.channel) if channel.blacklisted: await message.channel.send( embed=discord.Embed(description=info.language.get_string('blacklisted'))) return # blacklist checked; now do command permissions if command.check_permissions(message.author, guild): if message.guild.me.guild_permissions.manage_webhooks: await command.func(message, stripped, info) session.commit() else: await message.channel.send(info.language.get_string('no_perms_webhook')) else: await message.channel.send( info.language.get_string( str(command.permission_level)).format(prefix=prefix))
async def look(message, stripped, preferences): def relative_time(t): days, seconds = divmod(int(t - unix_time()), 86400) hours, seconds = divmod(seconds, 3600) minutes, seconds = divmod(seconds, 60) sections = [] for var, name in zip((days, hours, minutes, seconds), ('days', 'hours', 'minutes', 'seconds')): if var > 0: sections.append('{} {}'.format(var, name)) return ', '.join(sections) def absolute_time(t): return datetime.fromtimestamp(t, pytz.timezone(preferences.timezone)).strftime('%Y-%m-%d %H:%M:%S') r = re.search(r'(\d+)', stripped) limit: typing.Optional[int] = None if r is not None: limit = int(r.groups()[0]) if 'enabled' in stripped: show_disabled = False else: show_disabled = True if 'time' in stripped: time_func = absolute_time else: time_func = relative_time if message.guild is None: channel = preferences.user.channel new = False else: discord_channel = message.channel_mentions[0] if len(message.channel_mentions) > 0 else message.channel channel, new = Channel.get_or_create(discord_channel) if new: await message.channel.send(preferences.language.get_string('look/no_reminders')) else: reminder_query = channel.reminders.order_by(Reminder.time) if not show_disabled: reminder_query = reminder_query.filter(Reminder.enabled) if limit is not None: reminder_query = reminder_query.limit(limit) if reminder_query.count() > 0: if limit is not None: await message.channel.send(preferences.language.get_string('look/listing_limited').format( reminder_query.count())) else: await message.channel.send(preferences.language.get_string('look/listing')) s = '' for reminder in reminder_query: string = '\'{}\' *{}* **{}** {}\n'.format( reminder.message_content(), preferences.language.get_string('look/inter'), time_func(reminder.time), '' if reminder.enabled else '`disabled`') if len(s) + len(string) > 2000: await message.channel.send(s, allowed_mentions=NoMention) s = string else: s += string await message.channel.send(s, allowed_mentions=NoMention) else: await message.channel.send(preferences.language.get_string('look/no_reminders'))
async def look(message, stripped, preferences): r = re.search(r'(\d+)', stripped) limit: typing.Optional[int] = None if r is not None: limit = int(r.groups()[0]) if 'enabled' in stripped: show_disabled = False else: show_disabled = True if message.guild is None: channel = preferences.user.channel new = False else: discord_channel = message.channel_mentions[0] if len(message.channel_mentions) > 0 else message.channel channel, new = Channel.get_or_create(discord_channel) if new: await message.channel.send(preferences.language.get_string('look/no_reminders')) else: reminder_query = channel.reminders.order_by(Reminder.time) if not show_disabled: reminder_query = reminder_query.filter(Reminder.enabled) if limit is not None: reminder_query = reminder_query.limit(limit) if reminder_query.count() > 0: if limit is not None: await message.channel.send(preferences.language.get_string('look/listing_limited').format( reminder_query.count())) else: await message.channel.send(preferences.language.get_string('look/listing')) s = '' for reminder in reminder_query: string = '\'{}\' *{}* **{}** {}\n'.format( reminder.message_content(), preferences.language.get_string('look/inter'), datetime.fromtimestamp(reminder.time, pytz.timezone(preferences.timezone)).strftime( '%Y-%m-%d %H:%M:%S'), '' if reminder.enabled else '`disabled`') if len(s) + len(string) > 2000: await message.channel.send(s, allowed_mentions=NoMention) s = string else: s += string await message.channel.send(s, allowed_mentions=NoMention) else: await message.channel.send(preferences.language.get_string('look/no_reminders'))
class BotClient(discord.AutoShardedClient): def __init__(self, *args, **kwargs): self.start_time: float = unix_time() self.commands: typing.Dict[str, Command] = { 'help': Command('help', self.help, blacklists=False), 'info': Command('info', self.info), 'donate': Command('donate', self.donate), 'prefix': Command('prefix', self.change_prefix, False, PermissionLevels.RESTRICTED), 'blacklist': Command('blacklist', self.blacklist, False, PermissionLevels.RESTRICTED, blacklists=False), # TODO: remodel restriction table with FKs for role table 'restrict': Command('restrict', self.restrict, False, PermissionLevels.RESTRICTED), 'timezone': Command('timezone', self.set_timezone), 'lang': Command('lang', self.set_language), 'clock': Command('clock', self.clock), 'offset': Command('offset', self.offset_reminders, True, PermissionLevels.RESTRICTED), 'nudge': Command('nudge', self.nudge_channel, True, PermissionLevels.RESTRICTED), 'natural': Command('natural', self.natural, True, PermissionLevels.MANAGED), 'n': Command('natural', self.natural, True, PermissionLevels.MANAGED), 'remind': Command('remind', self.remind_cmd, True, PermissionLevels.MANAGED), 'r': Command('remind', self.remind_cmd, True, PermissionLevels.MANAGED), 'interval': Command('interval', self.interval_cmd, True, PermissionLevels.MANAGED), # TODO: remodel timer table with FKs for guild table 'timer': Command('timer', self.timer, False, PermissionLevels.MANAGED), 'del': Command('del', self.delete, True, PermissionLevels.MANAGED), # TODO: allow looking at reminder attributes in full by name 'look': Command('look', self.look, True, PermissionLevels.MANAGED), 'todos': Command('todos', self.todo, False, PermissionLevels.MANAGED), 'todo': Command('todo', self.todo), 'ping': Command('ping', self.time_stats) } self.match_string = None self.command_names = set(self.commands.keys()) self.joined_names = '|'.join(self.command_names) # used in restrict command for filtration self.max_command_length = max(len(x) for x in self.command_names) self.config: Config = Config(filename='config.ini') self.executor: concurrent.futures.ThreadPoolExecutor = concurrent.futures.ThreadPoolExecutor() self.c_session: typing.Optional[aiohttp.ClientSession] = None super(BotClient, self).__init__(*args, **kwargs) async def do_blocking(self, method): # perform a long running process within a threadpool a, _ = await asyncio.wait([self.loop.run_in_executor(self.executor, method)]) return [x.result() for x in a][0] async def find_and_create_member(self, member_id: int, context_guild: typing.Optional[discord.Guild]) \ -> typing.Optional[User]: u: User = session.query(User).filter(User.user == member_id).first() if u is None and context_guild is not None: m = context_guild.get_member(member_id) or self.get_user(member_id) if m is not None: c = Channel(channel=(await m.create_dm()).id) session.add(c) session.flush() u = User(user=m.id, name='{}'.format(m), dm_channel=c.id) session.add(u) session.commit() return u async def is_patron(self, member_id) -> bool: if self.config.patreon_enabled: url = 'https://discordapp.com/api/v6/guilds/{}/members/{}'.format(self.config.patreon_server, member_id) head = { 'authorization': 'Bot {}'.format(self.config.token), 'content-type': 'application/json' } async with self.c_session.get(url, headers=head) as resp: if resp.status == 200: member = await resp.json() roles = [int(x) for x in member['roles']] else: return False return self.config.patreon_role in roles else: return True @staticmethod async def welcome(guild, *_): for channel in guild.text_channels: if channel.permissions_for(guild.me).send_messages and not channel.is_nsfw(): await channel.send('Thank you for adding reminder-bot! To begin, type `$help`!') break else: continue async def on_error(self, *a, **k): session.rollback() raise async def on_ready(self): print('Logged in as') print(self.user.name) print(self.user.id) self.match_string = \ r'(?:(?:<@ID>\s+)|(?:<@!ID>\s+)|(?P<prefix>\S{1,5}?))(?P<cmd>COMMANDS)(?:$|\s+(?P<args>.*))' \ .replace('ID', str(self.user.id)).replace('COMMANDS', self.joined_names) self.c_session: aiohttp.client.ClientSession = aiohttp.ClientSession() if self.config.patreon_enabled: print('Patreon is enabled. Will look for servers {}'.format(self.config.patreon_server)) print('Local timezone set to *{}*'.format(self.config.local_timezone)) async def on_guild_join(self, guild): await self.send() await self.welcome(guild) # noinspection PyMethodMayBeStatic async def on_guild_channel_delete(self, channel): session.query(Channel).filter(Channel.channel == channel.id).delete(synchronize_session='fetch') async def send(self): if self.config.dbl_token: guild_count = len(self.guilds) dump = json_dump({ 'server_count': guild_count }) head = { 'authorization': self.config.dbl_token, 'content-type': 'application/json' } url = 'https://discordbots.org/api/bots/stats' async with self.c_session.post(url, data=dump, headers=head) as resp: print('returned {0.status} for {1}'.format(resp, dump)) # noinspection PyBroadException async def on_message(self, message): def _check_self_permissions(_channel): p = _channel.permissions_for(message.guild.me) return p.send_messages and p.embed_links async def _get_user(_message): _user = session.query(User).filter(User.user == message.author.id).first() if _user is None: dm_channel_id = (await message.author.create_dm()).id c = session.query(Channel).filter(Channel.channel == dm_channel_id).first() if c is None: c = Channel(channel=dm_channel_id) session.add(c) session.flush() _user = User(user=_message.author.id, dm_channel=c.id, name='{}#{}'.format( _message.author.name, _message.author.discriminator)) session.add(_user) session.flush() return _user elif message.guild is None: # command has been DMed. dont check for prefix :) split = message.content.split(' ') command_word = split[0].lower() if command_word[0] == '$': command_word = command_word[1:] args = ' '.join(split[1:]).strip() if command_word in self.command_names: command = self.commands[command_word] if command.allowed_dm: # get user user = await _get_user(message) await command.func(message, args, Preferences(None, user)) session.commit() elif _check_self_permissions(message.channel): # command sent in guild. check for prefix & call match = re.match( self.match_string, message.content, re.MULTILINE | re.DOTALL | re.IGNORECASE ) if match is not None: # matched command structure; now query for guild to compare prefix guild = session.query(Guild).filter(Guild.guild == message.guild.id).first() if guild is None: guild = Guild(guild=message.guild.id) session.add(guild) session.flush() # if none, suggests mention has been provided instead since pattern still matched if (prefix := match.group('prefix')) in (guild.prefix, None): # prefix matched, might as well get the user now since this is a very small subset of messages user = await _get_user(message) if guild not in user.guilds: guild.users.append(user) # create the nice info manager info = Preferences(guild, user) command_word = match.group('cmd').lower() stripped = match.group('args') or '' command = self.commands[command_word] # some commands dont get blacklisted e.g help, blacklist if command.blacklists: channel, just_created = Channel.get_or_create(message.channel) if channel.guild_id is None: channel.guild_id = guild.id if channel.blacklisted: await message.channel.send( embed=discord.Embed(description=info.language.get_string('blacklisted'))) return # blacklist checked; now do command permissions if command.check_permissions(message.author, guild): if message.guild.me.guild_permissions.manage_webhooks: await command.func(message, stripped, info) session.commit() else: await message.channel.send(info.language.get_string('no_perms_webhook')) else: await message.channel.send( info.language.get_string( str(command.permission_level)).format(prefix=prefix))