async def on_ready(self): """Display startup information""" print('Spirit v{}'.format(constants.VERSION)) print('Username: {}'.format(self.bot.user.name)) print('------') # Get guilds in database with DBase() as db: db = db.get_guilds() db_guilds = [] to_delete = [] for row in db: guild = self.bot.get_guild(row[0]) if guild: db_guilds.append(guild) else: to_delete.append(row[0]) # Add guilds for guild in self.bot.guilds: if guild not in db_guilds: with DBase() as db: db.add_guild(guild.id) # Remove guilds for guild_id in to_delete: with DBase() as db: db.remove_guild(guild_id)
async def seteventrole(self, ctx, *, event_role): """Set the lowest role that is able to create events (Manage Server only) By default, creating events requires the user to have Administrator permissions. But if an event role is set, then any user that is of the event role or higher may create events. **Note:** Mentioning the role directly with this command will not work. You must provide only the name of the role without mentioning it. The role name is also case sensitive! """ manager = MessageManager(self.bot, ctx.author, ctx.channel, [ctx.message]) guild_event_role = None for role in ctx.guild.roles: if role.name in (event_role, "@{}".format(event_role)): guild_event_role = role if not guild_event_role: await manager.say( "I couldn't find a role called **{}** on this server.\n". format(event_role) + "Note that you must provide only the name of the role. " + "Mentioning it with the @ sign won't work. The role name is also " + "case sensitive!") return await manager.clear() with DBase() as db: db.set_event_role_id(ctx.guild.id, guild_event_role.id) await manager.say("The event role has been set to: **{}**".format( format_role_name(guild_event_role))) return await manager.clear()
async def togglecleanup(self, ctx): """ Toggle command message cleanup on/off (Manage Server only) When enabled, command message spam will be deleted a few seconds after a command has been invoked. This feature is designed to keep bot related spam to a minimum. Only non important messages will be deleted if this is enabled; messages like the help message or the roster, for example, will not be removed. """ manager = MessageManager(self.bot, ctx.author, ctx.channel, [ctx.message]) with DBase() as db: db.toggle_cleanup(ctx.guild.id) rows = db.get_cleanup(ctx.guild.id) if len(rows) and len(rows[0]): cleanup = rows[0][0] else: raise ValueError("Could not retrieve 'cleanup' from database") status = 'enabled' if cleanup else 'disabled' await manager.say("Command message cleanup is now *{}*".format(status)) return await manager.clear()
async def set_attendance(self, member, guild, attending, title, message): """Send updated event attendance info to db and update the event""" with DBase() as db: db.add_user(member.id) db.update_attendance(member.id, guild.id, attending, title, datetime.now()) with DBase() as db: rows = db.get_event(guild.id, title) if len(rows) and len(rows[0]): event = rows[0] else: raise ValueError("Could not retrieve event") return # Update event message in place for a more seamless user experience event_embed = self.create_event_embed(guild, event[0], event[1], event[2], event[3], event[4], event[5], event[6], event[7]) await message.edit(embed=event_embed)
def __init__(self, token, bungie_api_key, bungie_client_id): super().__init__(command_prefix=_prefix_callable) self.token = token self.db = DBase('credentials.json') self.destiny = pydest.Pydest(bungie_api_key) self.bungie_client_id = bungie_client_id self.uptime = datetime.datetime.utcnow() self.command_count = 0
def get_event_role(guild): """Return the event role, if it exists""" with DBase() as db: rows = db.get_event_role_id(guild.id) if len(rows) and len(rows[0]): for role in guild.roles: if role.id == rows[0][0]: return role
async def show(self, ctx): """ Display the roster The roster includes the name, Destiny 2 class, and timezone of server members. Note that only users who have set a role or timezone will be displayed on the roster. """ manager = MessageManager(self.bot, ctx.author, ctx.channel, [ctx.message]) roster_groups = [] with DBase() as db: roster = db.get_roster(ctx.guild.id) if len(roster) != 0: text = "```\n" for row in roster: # Add a single entry to the roster message member = ctx.guild.get_member(row[0]) if member: name = member.display_name formatted_name = (name[:16] + '..') if len(name) > 16 else name role = row[1] if row[1] else "---" time_zone = row[2] if row[2] else "---" text += '{:18} {:6} {:7}\n'.format(formatted_name, time_zone, role) # If the message is too big, place it into a group if len(text) > 2000: text += "```" roster_groups.append(text) text = "```\n" # Add any remaining entries into a roster group if len(text) > 5: text += "```" roster_groups.append(text) # Send the initial roster message embed_msg = discord.Embed(color=constants.BLUE) embed_msg.title="{} Roster".format(ctx.guild.name) embed_msg.description = roster_groups[0] await manager.say(embed_msg, embed=True, delete=False) # Send additional roster messages if the roster is too long for group in roster_groups[1:]: embed_msg = discord.Embed(color=constants.BLUE) embed_msg.title="{} Roster (continued)".format(ctx.guild.name) embed_msg.description = group await manager.say(embed_msg, embed=True, delete=False) else: await manager.say("No roster exists yet. Use '{}roster settimezone' or '{}roster ".format(ctx.prefix, ctx.prefix) + "setclass' to add the first entry!") await manager.clear()
async def _prefix_callable(bot, message): """Get current command prefix""" base = ['<@{}> '.format(bot.user.id)] if isinstance(message.channel, discord.abc.PrivateChannel): base.append('!') else: with DBase() as db: custom_prefix = db.get_prefix(message.guild.id) if len(custom_prefix) > 0 and len(custom_prefix[0]) > 0: base.append(custom_prefix[0][0]) return base
async def delete_event(self, guild, title, member, channel): """Delete an event and update the events channel on success""" event_delete_role = get_event_delete_role(guild) with DBase() as db: rows = db.get_event_creator(guild.id, title) if len(rows) and len(rows[0]): creator_id = rows[0][0] if member.permissions_in(channel).manage_guild or (member.id == creator_id) or (event_delete_role and member.top_role >= event_delete_role): with DBase() as db: deleted = db.delete_event(guild.id, title) if deleted: await self.list_events(guild) return True else: try: await member.send("You don't have permission to delete that event.") except: pass
async def list_events(self, guild): """Clear the event channel and display all upcoming events""" events_channel = await self.get_events_channel(guild) await events_channel.purge(limit=999, check=delete_all) with DBase() as db: events = db.get_events(guild.id) if len(events) > 0: for row in events: event_embed = self.create_event_embed(guild, row[0], row[1], row[2], row[3], row[4], row[5], row[6], row[7]) msg = await events_channel.send(embed=event_embed) await msg.add_reaction("\N{WHITE HEAVY CHECK MARK}") await msg.add_reaction("\N{CROSS MARK}") else: await events_channel.send("There are no upcoming events.")
async def setprefix(self, ctx, new_prefix): """ Change the server's command prefix (Manage Server only) """ manager = MessageManager(self.bot, ctx.author, ctx.channel, [ctx.message]) if len(new_prefix) > 5: await manager.say("Prefix must be less than 6 characters.") return await manager.clear() with DBase() as db: db.set_prefix(ctx.guild.id, new_prefix) await manager.say("Command prefix has been changed to " + new_prefix) return await manager.clear()
async def setclass(self, ctx, role): """ Add your Destiny 2 class to the roster Class must be one of Titan, Warlock, or Hunter """ manager = MessageManager(self.bot, ctx.author, ctx.channel, [ctx.message]) role = role.lower().title() if role == "Titan" or role == "Warlock" or role == "Hunter": with DBase() as db: db.add_user(ctx.author.id) db.update_role(ctx.author.id, role, ctx.guild.id) await manager.say("Your class has been updated!") else: await manager.say("Class must be one of: Titan, Hunter, Warlock") await manager.clear()
async def clear(self): """Delete messages marked for deletion""" def check(message): if (message.author in (self.user, self.bot.user) and message.id in [m.id for m in self.messages]): return True if not isinstance(self.channel, discord.abc.PrivateChannel): with DBase() as db: rows = db.get_cleanup(self.channel.guild.id) if len(rows) and len(rows[0]): cleanup = rows[0][0] else: raise ValueError("Could not retrieve 'cleanup' from database") if cleanup: await asyncio.sleep(constants.SPAM_DELAY) await self.channel.purge(limit=999, check=check)
async def settimezone(self, ctx, *, time_zone): """ Add your timezone to the roster For a full list of supported timezones, check out the bot's support server """ manager = MessageManager(self.bot, ctx.author, ctx.channel, [ctx.message]) time_zone = time_zone.upper() time_zone = "".join(time_zone.split()) if time_zone in constants.TIME_ZONES: with DBase() as db: db.add_user(ctx.author.id) db.update_timezone(ctx.author.id, time_zone, ctx.guild.id) await manager.say("Your time zone has been updated!") else: await manager.say("Unsupported time zone. For a list of supported timezones, " + "check out the bot's support server.") await manager.clear()
async def loadout(self, ctx): """Display your last played character's loadout In order to use this command, you must first register your Destiny 2 account with the bot via the register command. """ manager = MessageManager(self.bot, ctx.author, ctx.channel, [ctx.message]) await ctx.channel.trigger_typing() # Check if user has registered their D2 account with the bot with DBase() as db: entries = db.get_d2_info(ctx.author.id) if len(entries ) > 0 and entries[0][0] != None and entries[0][1] != None: platform = entries[0][0] membership_id = entries[0][1] else: await manager.say( "You must first register your Destiny 2 account with the " + "`{}register` command.".format(ctx.prefix)) return await manager.clear() res = await self.destiny.api.get_profile( platform, membership_id, ['characters', 'characterEquipment', 'profiles']) if res['ErrorCode'] != 1: await manager.say( "Sorry, I can't seem to retrive your Guardian right now.") return await manager.clear() # Determine which character was last played chars_last_played = [] for character_id in res['Response']['characters']['data']: last_played_str = res['Response']['characters']['data'][ character_id]['dateLastPlayed'] date_format = '%Y-%m-%dT%H:%M:%SZ' last_played = datetime.strptime(last_played_str, date_format) chars_last_played.append((character_id, last_played)) last_played_char_id = max(chars_last_played, key=lambda t: t[1])[0] last_played_char = res['Response']['characters']['data'].get( last_played_char_id) ####################################### # ------ Decode Character Info ------ # ####################################### role_dict = await self.destiny.decode_hash( last_played_char['classHash'], 'DestinyClassDefinition') role = role_dict['displayProperties']['name'] gender_dict = await self.destiny.decode_hash( last_played_char['genderHash'], 'DestinyGenderDefinition') gender = gender_dict['displayProperties']['name'] race_dict = await self.destiny.decode_hash( last_played_char['raceHash'], 'DestinyRaceDefinition') race = race_dict['displayProperties']['name'] char_name = res['Response']['profile']['data']['userInfo'][ 'displayName'] level = last_played_char['levelProgression']['level'] light = last_played_char['light'] emblem_url = 'https://www.bungie.net' + last_played_char['emblemPath'] stats = [] for stat_hash in ('2996146975', '392767087', '1943323491'): stat_dict = await self.destiny.decode_hash( stat_hash, 'DestinyStatDefinition') stat_name = stat_dict['displayProperties']['name'] if stat_hash in last_played_char['stats'].keys(): stats.append( (stat_name, last_played_char['stats'].get(stat_hash))) else: stats.append((stat_name, 0)) ####################################### # ------ Decode Equipment Info ------ # ####################################### weapons = [['Kinetic', '-'], ['Energy', '-'], ['Power', '-']] weapons_index = 0 armor = [['Helmet', '-'], ['Gauntlets', '-'], ['Chest', '-'], ['Legs', '-'], ['Class Item', '-']] armor_index = 0 equipped_items = res['Response']['characterEquipment']['data'][ last_played_char_id]['items'] for item in equipped_items: item_dict = await self.destiny.decode_hash( item['itemHash'], 'DestinyInventoryItemDefinition') item_name = "{}".format(item_dict['displayProperties']['name']) if weapons_index < 3: weapons[weapons_index][1] = item_name weapons_index += 1 elif armor_index < 5: armor[armor_index][1] = item_name armor_index += 1 ################################# # ------ Formulate Embed ------ # ################################# char_info = "Level {} {} {} {} |\N{SMALL BLUE DIAMOND}{}\n".format( level, race, gender, role, light) char_info += "{} {} • ".format(stats[0][1], stats[0][0]) char_info += "{} {} • ".format(stats[1][1], stats[1][0]) char_info += "{} {}".format(stats[2][1], stats[2][0]) weapons_info = "" for weapon in weapons: weapons_info += '**{}:** {} \n'.format(weapon[0], weapon[1]) armor_info = "" for item in armor: armor_info += '**{}:** {}\n'.format(item[0], item[1]) e = discord.Embed(colour=constants.BLUE) e.set_author(name=char_name, icon_url=constants.PLATFORM_URLS.get(platform)) e.description = char_info e.set_thumbnail(url=emblem_url) e.add_field(name='Weapons', value=weapons_info, inline=True) e.add_field(name='Armor', value=armor_info, inline=True) await manager.say(e, embed=True, delete=False) await manager.clear()
async def event(self, ctx): """ Create an event in the events channel After invoking the event command, the bot will ask you to enter the event details. Once the event is created, it will appear in the upcoming-events channel. The upcoming-events channel is designed with the assumption that it isn't used for anything but displaying events; non event messages will be deleted. Users will be able to accept and decline the event by adding reactions. If a maximum number of attendees is set and the event is full, additional attendees will be placed in a standby section. If a spot opens up, the user at the top of the standby section will be automatically moved into the event. By default, everyone can make events. However, a minimum role requirement to create events can be defined in the settings. See `help settings seteventrole` for more information. The event creator and those with the Manage Sever permission can delete events by reacting to the event message with \U0001f480. """ manager = MessageManager(self.bot, ctx.author, ctx.channel, [ctx.message]) event_role = get_event_role(ctx.guild) member_permissions = ctx.author.permissions_in(ctx.channel) if event_role: if ctx.author.top_role < event_role: event_role_str = format_role_name(event_role) await manager.say("You must be of role **{}** or higher to do that.".format(event_role)) return await manager.clear() await manager.say('Event creation instructions have been messaged to you') res = await manager.say_and_wait("Enter event title:", dm=True) if not res: return await manager.clear() title = res.content description = "" res = await manager.say_and_wait("Enter event description (type 'none' for no description):", dm=True) if not res: return await manager.clear() if res.content.upper() != 'NONE': description = res.content max_members = 0 while not max_members: res = await manager.say_and_wait("Enter the maximum numbers of attendees (type 'none' for no maximum):", dm=True) if not res: return await manager.clear() if res.content.upper() == 'NONE': break elif is_int(res.content) and int(res.content) > 0: max_members = int(res.content) else: await manager.say("That is not a a valid entry.", dm=True) start_time = None while not start_time: res = await manager.say_and_wait("Enter event time (YYYY-MM-DD HH:MM AM/PM):", dm=True) if not res: return await manager.clear() start_time_format = '%Y-%m-%d %I:%M %p' try: start_time = datetime.strptime(res.content, start_time_format) except ValueError: await manager.say("Invalid event time!", dm=True) time_zone = None while not time_zone: res = await manager.say_and_wait("Enter the time zone (PST, EST, etc):", dm=True) if not res: return await manager.clear() user_timezone = "".join(res.content.upper().split()) if user_timezone not in constants.TIME_ZONES: await manager.say("Unsupported time zone", dm=True) else: time_zone = user_timezone with DBase() as db: affected_rows = db.create_event(title, start_time, time_zone, ctx.guild.id, description, max_members, ctx.author.id) if affected_rows == 0: await manager.say("An event with that name already exists!", dm=True) return await manager.clear() event_channel = await self.get_events_channel(ctx.guild) await manager.say("Event created! The " + event_channel.mention + " channel will be updated momentarily.", dm=True) await self.list_events(ctx.guild) await manager.clear()
async def on_guild_join(self, guild): """Add guild and it's members to database""" with DBase() as db: db.add_guild(guild.id)
async def on_guild_remove(self, guild): """Remove guild from database""" with DBase() as db: db.remove_guild(guild.id)
async def on_member_remove(self, user): """Remove user from database when they leave the guild""" with DBase() as db: db.remove_user(user.id)
def __init__(self, token): super().__init__(command_prefix=_prefix_callable) self.token = token self.db = DBase('credentials.json') self.uptime = datetime.datetime.utcnow() self.command_count = 0
async def help(self, ctx, str_cmd=None, str_subcmd=None): """Display command information""" manager = MessageManager(self.bot, ctx.author, ctx.channel, [ctx.message]) # Determine which prefix to display in the help message if isinstance(ctx.channel, discord.abc.PrivateChannel): prefix = '!' else: # Don't want to display @botname as the prefix in the help message # It's way too long! if ctx.prefix != '<@{}> '.format(self.bot.user.id): prefix = ctx.prefix else: with DBase() as db: custom_prefix = db.get_prefix(ctx.guild.id) if len(custom_prefix) > 0 and len(custom_prefix[0]) > 0: prefix = custom_prefix[0][0] else: raise AttributeError( "Could not retrieve command prefix") # User passed a command and a subcommand if str_cmd and str_subcmd: cmd = self.bot.get_command(str_cmd) if cmd is None: await manager.say( "There are no commands called '{}'".format(str_cmd)) return await manager.clear() # Check for subcommand if hasattr(cmd, 'commands'): for sub_cmd in cmd.commands: if sub_cmd.name == str_subcmd: help = self.help_embed_single(prefix, sub_cmd) await manager.say(help, embed=True, delete=False) break else: await manager.say( "'{}' doesn't have a subcommand called '{}'".format( str_cmd, str_subcmd)) else: await manager.say( "'{}' does not have any subcommands".format(str_cmd)) # User passed in a single command elif str_cmd: cmd = self.bot.get_command(str_cmd) if cmd is None: await manager.say( "There are no commands called '{}'".format(str_cmd)) return await manager.clear() # Check if command has subcommands if hasattr(cmd, 'commands'): sub_cmds = [] for sub_cmd in cmd.commands: sub_cmds.append(sub_cmd) help = self.help_embed_group(prefix, cmd, sub_cmds) else: help = self.help_embed_single(prefix, cmd) await manager.say(help, embed=True, delete=False) # No command passed, print help for all commands else: help = self.help_embed_all(prefix, self.bot.commands) await manager.say(help, embed=True, delete=False) await manager.clear()
async def register(self, ctx): """Register your Destiny 2 account with the bot This command will let the bot know which Destiny 2 profile to associate with your Discord profile. Registering is a prerequisite to using any commands to require knowledge of your public Destiny 2 profile. """ manager = MessageManager(self.bot, ctx.author, ctx.channel, [ctx.message]) if not isinstance(ctx.channel, discord.abc.PrivateChannel): await manager.say( "Registration instructions have been messaged to you") await manager.say( "Registering your Destiny 2 account with me will allow " + "you to invoke commands that use information from your " + "public Destiny 2 profile.", dm=True) platform = None while not platform: res = await manager.say_and_wait( "Enter your platform (**xbox** or **playstation**):", dm=True) if not res: return await manager.clear() platforms = {'PC': 4, 'XBOX': 1, 'PLAYSTATION': 2} platform = platforms.get(res.content.upper()) if not platform: await manager.say("Invalid platform. Try again.", dm=True) act = await manager.say_and_wait("Enter your exact **account name**:", dm=True) if not act: return await manager.clear() try: res = await self.destiny.api.search_destiny_player( platform, act.content) except ValueError as e: await manager.say( "Invalid account name. If this seems wrong, please contact the developer." ) return await manager.clear() act_exists = False if res['ErrorCode'] == 1 and len(res['Response']) == 1: act_exists = True membership_id = res['Response'][0]['membershipId'] elif res['ErrorCode'] == 1 and len(res['Response']) > 1: for entry in res['Response']: if act.content == entry['displayName']: act_exists = True membership_id = entry['membershipId'] break if not act_exists: await manager.say( "An account with that name doesn't seem to exist.", dm=True) else: await manager.say("Account successfully registered!", dm=True) with DBase() as db: db.add_user(ctx.author.id) db.update_registration(platform, membership_id, ctx.author.id) return await manager.clear()