async def update_db(loop): current = await get_list() # list of comics currently in the database db = database.XKCD_Database() r = requests.get("https://xkcd.com/archive/" ) # Grab the archive page, a list of all xkcd comics raw_names = re.findall( r'[0-9]{0,4}/" title="[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}">[^<>]+<', r.content.decode(), ) # Grab sections of html containing names xkcds = [] for name in raw_names: strip = xkcd.from_raw_name( name) # Create an xkcd object from the raw data if (not strip.name in current): # We only need to add comics we don't have xkcds.append(strip) with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: calls = [ loop.run_in_executor(executor, strip.get_uri_alt) for strip in xkcds ] strips = await asyncio.gather(*calls) for strip in strips: # Run calls await db.insert_xkcd(strip) # Add to db utilities.log_message(f"Added new xkcd comic {strip.name}.") utilities.log_message("xkcd database up to date!")
async def set_dm(self, guild): # guild: the Guild object of the relevant server dm_role = await self.get_dm_role(guild) if not dm_role: utilities.log_message(f"Missing role permissions in {guild.name}.") return campaign = await self.get_active_campaign(guild.id) for player in campaign.players: member = await utilities.get_member(guild, player) if (not utilities.is_guild_owner(guild, member.id) and dm_role in member.roles): await member.remove_roles(dm_role) if not campaign.dm: return try: dm = await utilities.get_member(guild, campaign.dm) if not utilities.is_guild_owner(guild, dm.id): await dm.add_roles(dm_role) except AttributeError: # didn't find dm for some reason utilities.log_message( f"Failed to find DM for campaign {campaign.name} in " f"{guild.name}.")
def __init__(self, client, loop): config = utilities.load_config() config["client"] = client self.client = client self.db = database.Discord_Database() self.commands = {} for i in Bot.INSTRUCTIONS: try: cmd = i(config) for c in cmd.commands: self.commands[c] = cmd except AssertionError: utilities.log_message(f"{i} disabled.") self.patterns = [] for p in Bot.PATTERNS: try: self.patterns.append(p(config)) except AssertionError: utilities.log_message(f"{p} disabled.") self.reaction_handlers = [] for h in self.patterns + list(self.commands.values()): if h.monitors_reactions: self.reaction_handlers.append(h) self.token = config["token"] loop.run_until_complete(database.init_db(config["db_file"]))
async def handle(self, message): self.delete_message = True self.will_send = True user = message.author mention = user.display_name server = message.guild channel = message.channel string = message.content.lower().strip() for c in self.commands: if string.startswith(c): command = c break string = string.replace(command, "", 1).strip() if "stats" in string: await self.handle_stats(string, user, server, mention, channel) return try: rolls = roll.get_rolls(string) assert len(rolls) > 0 except Exception as e: self.delete_message = False await channel.send(f"{self.failstr}. {e}") return if server and user: for r in rolls: for dice_str, values in r.roll_info(): await self.db.insert_roll(dice_str, ",".join(map(str, values)), user, server) e = build_embed(rolls, mention, string) if command == "--roll": try: await channel.send(embed=e) except discord.errors.HTTPException as e: await channel.send( "Ran into an error. The message may have been too long.") elif command in ["--dmroll", "--gmroll"]: dm = await self.get_dm(message.guild.members) if dm is None: self.delete_message = False await channel.send( f'Couldn\'t find a member with the role "{self.dm_role}".') try: await dm.send(embed=e) if user != dm: await user.send(embed=e) except discord.errors.HTTPException as e: self.delete_message = False await channel.send("Ran into an error.") utilities.log_message(f"Error sending roll: {e}") return
async def handle_reaction(self, reaction, _): if reaction.message.id not in self.sent: return emoji = utilities.get_emoji_name(reaction.emoji) embed_style = None for key in ScryfallHandler.EMBED_EMOJI_MAPPING.keys(): if emoji in ScryfallHandler.EMBED_EMOJI_MAPPING[key]: embed_style = key break index = (int(emoji[-1:]) - 1 if emoji.startswith("keycap_") and 0 < int(emoji[-1]) < 6 else None) remove_message = emoji in ScryfallHandler.REMOVE_EMOJIS utilities.log_message(f'Scryfall message reacted to with "{emoji}".') if remove_message: await reaction.message.delete() utilities.log_message("Deleted message.") return message, sent = self.sent[reaction.message.id] if isinstance(sent, CardList) and index is not None: try: card = sent.select_option(index) except IndexError: return if type(card) == Card: await reaction.message.edit(embed=card.get_embed()) self.log_sent(reaction.message, card) elif type(card) == DoubleFacedCard: await reaction.message.delete() await self.send(card, reaction.message.channel) utilities.log_message("Option selected from scryfall card list.") await reaction.clear() return if embed_style is None: return elif embed_style == sent.embed_style: await reaction.clear() return if isinstance(sent, Card): await reaction.message.edit(embed=sent.get_embed(embed_style)) else: utilities.log_message(f"Strange scryfall sent type: {type(sent)}") return if type(sent) in [DoubleFacedCard, BackFace]: for message, content in self.sent.values(): if content is sent.other_face: await message.edit(embed=content.get_embed(embed_style)) await reaction.clear() utilities.log_message("Scryfall embed size edited.")
async def make_connection(self, file): self.file = file run_migration = os.path.isfile(file) self.connection = await aiosqlite.connect(self.file) if run_migration: await self.migrate() for command in self.startup_commands: await self.execute(command, trans_type=TransTypes.COMMIT) utilities.log_message("Established connection and set up database.")
def get_result(self): if self.result is None: self.resolve() if self.result is None: utilities.log_message( "Failed to find card while searching scryfall for " f'"{self.query}".') return ("Oops, something went wrong when I was looking for " + f'"{utilities.capitalise(self.query)}". Let Owen know!') return self.result
def log_message(self, message): guild_string = message.guild if guild_string is None: guild_string = "me" if message.content: utilities.log_message( message.author.display_name + f' sent "{message.content}" to {guild_string}.') else: utilities.log_message(message.author.display_name + f" sent an attachment to {guild_string}.")
async def get_random_xkcd(self): newest = (await database.execute("SELECT max(id) FROM xkcds;", trans_type=TransTypes.GETONE))[0] comic = random.randint(newest - await self.xkcd_count(), newest) try: return self.interpret_xkcd(await database.execute( "SELECT id, name, uri, alt FROM xkcds WHERE id = ?;", (str(comic), ), TransTypes.GETONE, )) except TypeError: utilities.log_message(f"Missing xkcd #{comic}.") return await self.get_random_xkcd()
async def _handle(self, guild, campaign, _arg, _target): out = f"Members of campaign {campaign.name}" if campaign.day >= 0 and campaign.time >= 0: hour = str(campaign.time // 3600) minute = str(campaign.time % 3600 // 60) while len(minute) < 2: minute += "0" out += (" (" + utilities.number_to_weekday(campaign.day) + f" at {hour}:{minute})") out += ":\n\t" dm_string = "" if campaign.dm: try: dm_name = (await utilities.get_member(guild, campaign.dm)).name dm_string = "DM: " + dm_name if campaign.dm in campaign.players: nick = campaign.nicks[campaign.players.index(campaign.dm)] dm_string += f" ({nick})" if nick else "" except Exception as e: dm_string = "" utilities.log_message(f"Failed to add DM name: {e}") out += (dm_string if dm_string else "No DM") + "\n\t" member_names = [] if campaign.players: for p, n in zip(campaign.players, campaign.nicks): if p == campaign.dm: continue try: name = (await utilities.get_member(guild, p)).name name += f" ({n})" if n else "" member_names.append(name) except Exception as e: utilities.log_message(f"Failed to add name: {e}") if member_names: out += "\n\t".join(member_names) else: out += "No players" return out
async def execute(self, command, args=None, trans_type=TransTypes.GETALL): try: if args is not None: cursor = await self.connection.execute(command, args) else: cursor = await self.connection.execute(command) if trans_type == TransTypes.GETALL: return await cursor.fetchall() elif trans_type == TransTypes.GETONE: return await cursor.fetchone() elif trans_type == TransTypes.COMMIT: await self.save() except Exception as e: utilities.log_message(f"Database error: {e}") utilities.log_message(f"Ocurred on command: {command}")
def __init__(self, config): assert config["dnd_campaign"] super().__init__(config, commands=["--dnd"]) self.dm_role = config["dm_role"] self.campaigns = {} self.db = database.Campaign_Database() self.help_message = utilities.load_help()["dnd"] config["client"].loop.create_task(self.notify(config["client"])) self.options = {} for i in CampaignSwitcher.INSTRUCTIONS: try: option = i(config) option.meta = self for c in option.commands: self.options[c] = option except AssertionError: utilities.log_message(f"Campaign option {i} disabled.")
async def handle(self, message): self.delete_message = False argument = self.remove_command_string(message.content) if argument: try: await message.channel.send( wordart.handle_wordart_request(argument, self.default_emoji)) self.delete_message = True except discord.HTTPException as e: utilities.log_message(f"Error attempting to send wordart: {e}") await message.channel.send( "Ran into an error sending this wordart. The message " + "was probably too long, usually around 4 characters " + "is the maximum.") else: await message.channel.send( "Usage: `--wa <message>` to create word art. " + "Messages must be very short: around 4 characters.")
async def notify(self, client, period=60, delta=1800): await client.wait_until_ready() while not client.is_closed(): reminders = await self.db.get_reminders(period, delta) for name, channel, players in reminders: try: mention_string = " ".join( [f"<@{p}>" for p in parse_player_string(players)]) channel = discord.utils.find(lambda c: c.id == channel, client.get_all_channels()) await channel.send( f"A game for {name} begins in " + f"{int(round(delta / 60, 0))} minutes.\n\n" + mention_string) except Exception as e: utilities.log_message( "Ran into an issue sending " + f"notification for campaign {name}: {e}") await asyncio.sleep(period)
def _update_screen(self): self.screen.fill(self.settings.bg_color) self.earth.blitme() for factory in self.earth.factories: factory.blitme() for ship in self.earth.ships: ship.blitme() for enemy in self.encounter.enemies: enemy.blitme() for bullet in self.bullets: bullet.blitme() self.screen.blit(update_fps(self), (3, 0)) self.screen.blit( log_message(self, self.logger.log_message, self.logger.color), (800, 0)) pygame.display.flip()
async def migrate(self): (from_version, ) = await self.execute("PRAGMA user_version;", trans_type=TransTypes.GETONE) if from_version == Database.VERSION: utilities.log_message("Database schema up to date!") elif from_version == 0: utilities.log_message( f"Database at version 0: updating to {Database.VERSION}") await self.execute("PRAGMA foreign_keys = OFF;") await self.execute( "CREATE TABLE _new_campaigns(" "id INTEGER PRIMARY KEY AUTOINCREMENT, " "name TEXT COLLATE NOCASE, server INTEGER, " "dm INTEGER, players TEXT, nicks TEXT, active INTEGER, " "day INTEGER, time INTEGER, notify INTEGER, channel INTEGER, " "FOREIGN KEY(dm) REFERENCES users(id), " "FOREIGN KEY(server) REFERENCES servers(id), " "UNIQUE(name, server));") await self.execute("INSERT INTO _new_campaigns(" "name, server, dm, players, nicks, " "active, day, time, notify, channel" ") SELECT * FROM campaigns;") await self.execute("DROP TABLE campaigns;") await self.execute( "ALTER TABLE _new_campaigns RENAME TO campaigns;") await self.execute("PRAGMA foreign_keys = ON;") await self.execute( "ALTER TABLE rolls ADD COLUMN campaign INTEGER REFERENCES " "campaigns(id) ON DELETE SET NULL;") await self.save() utilities.log_message("Database migration successful!") else: utilities.log_message( "Don't know how to update database from version " f"{from_version} to version {Database.VERSION}. Exiting.") exit(1)
async def handle(self, message): campaign = await self.meta.get_active_campaign(message.guild.id) arg = re.sub(self.regex, "", message.content, count=1, flags=re.IGNORECASE).strip() nick = arg if message.mentions: if len(message.mentions) > 1: return ("This command requires a single mention. e.g. " "`--dnd setnick <mention> <name>`.") target = message.mentions[0] if not target.id in campaign.players: return ( f"{target.display_name} is not in {campaign.name} " "so I cannot set their nickname. The DM can add them " "`--dnd add <mention>` or they can join with `--dnd join`." ) if (message.author.id in [campaign.dm, target.id] or campaign.dm == None): for uid in message.raw_mentions: nick = nick.replace(f"<@{uid}>", "") nick = nick.replace(f"<@!{uid}>", "") nick = nick.strip() else: return "Only the campaign dm can set other players nicknames." else: target = message.author if not target.id in campaign.players: return (f"You are not in {campaign.name}, so I cannot set " "your nickname. Join with `--dnd join`.") if len(nick) == 0: return ("Usage: `--dnd nick <nickname>` or " "`--dnd setnick <mention> <nickname>`.") if self.nick_regex.match(nick) is None: return ("A nickname must be 1-32 non-special characters. " f'"{nick}" is inadmissable.') if not message.author.id in campaign.players: return ("You must join the campaign with `--dnd join` " "before you can set a nickname.") if utilities.is_guild_owner(message.guild, target.id): if target.id == message.author.id: return ("You are the server owner which means I can't " "set your nickname.") else: return (f"{target.display_name} is the guild owner which " "means I can't set their nickname") try: await target.edit(nick=nick) except discord.Forbidden as e: utilities.log_message( f"Failed to set nick of {target.display_name} in " f"{message.guild.name} due to: {e}") return ("I was unable to set your nickname. Either I lack the " "permission to do so or you are the server owner.") campaign.set_nick(target.id, nick) await self.meta.db.add_campaign(campaign) return (f"Set the nickname for {target.name} " f"in {campaign.name} to {nick}.")
def __init__(self, config): assert config["dnd_spells"] super().__init__(config, commands=["--spell"]) self.sb = spellbook.Spellbook(config["spellbook_url"]) utilities.log_message("Successfully downloaded spellbook.")
async def on_ready(): utilities.log_message(f"Logged in as {client.user.name}," + f" ID: {client.user.id}") utilities.log_message("==== BEGIN LOG ====")
async def save(self): try: await self.connection.commit() except Exception as e: utilities.log_message(f"Database error: {e}")
def load_wa_alphabet(): try: with open("resources/alphabet.json", "r") as f: wa_alphabet.update(json.load(f)) except FileNotFoundError: utilities.log_message("Failed to load wordart alphabet.")
async def handle_command(self, message): if message.author == client.user or message.author.bot: return if message.guild is not None: await self.db.insert_user(message.author) await self.db.insert_server(message.guild) self.log_message(message) cmd = None if message.content.startswith("--"): match = re.search(r"^--[a-zA-Z]+", message.content.lower()) if match is None: await message.channel.send( "Commands are called via `--<command>`. Try `--all` " + "to see a list of commands or `--help` for further assistance." ) return cmd_str = match.group(0) if cmd_str == "--all": await message.channel.send("\n".join(self.commands)) return elif cmd_str in self.commands: cmd = self.commands[match.group(0)] else: suggestions = difflib.get_close_matches(cmd_str, self.commands) if suggestions: await message.channel.send( f"Command `{cmd_str}` doesn't exist. " + f"Perhaps you meant `{suggestions[0]}`?") else: await message.channel.send( f"Command `{cmd_str}` doesn't exist. Try `--all` " + "to see a list of commands.") return else: for pattern in self.patterns: if re.search(pattern.regex, message.content): cmd = pattern if cmd is None: return try: resp = await cmd.handle(message) except Exception as e: utilities.log_message("Ran into issue handling command " + f"{message.content}: {e}") utilities.log_message(f"Stack trace:\n{traceback.format_exc()}") await message.channel.send("Ran into an issue with that command.") return if not cmd.will_send: try: if type(resp) is str: await message.channel.send(resp) elif type(resp) is discord.Embed: await message.channel.send(embed=resp) elif type(resp) is list: for e in resp: if type(e) is str: await message.channel.send(e) elif type(e) is discord.Embed: await message.channel.send(embed=e) else: utilities.log_message( "List from command " f'"{message.content}" contained strange type: ' f"{type(resp)}.") await message.channel.send( "Ran into an issue with that command. ") else: utilities.log_message( "Got strange type from command " + f'"{message.content}": {type(resp)}.') await message.channel.send( "Ran into an issue with that command.") except Exception as e: utilities.log_message(f"Ran into issue sending response: {e}") utilities.log_message( f"Stack trace:\n{traceback.format_exc()}") await message.channel.send("Failed to send response.") if cmd.delete_message: try: await message.delete() utilities.log_message("Deleted command message.") except discord.errors.Forbidden: utilities.log_message("Couldn't delete command message; " + "insufficient permissions.") except discord.errors.NotFound: utilities.log_message("Couldn't find message to delete. " + "Already gone?")
def start_db_thread(interval, client): thread = threading.Thread(target=update_db_schedule, args=[interval, client]) thread.start() utilities.log_message("Started XKCD update thread.")
async def handle(self, message): self.scrub_votes() self.scrub_cooldown() if not message.mentions: return "Usage: `--kick <mention>` e.g. `--kick @BadPerson`." if len(message.mentions) > 1: return "I can only kick one person at a time!" target = message.mentions[0] if type(target) != discord.Member: return ("Sorry, I can't find voice information for " f"{target.display_name}.") if self.on_cooldown(target): return (f"{target.display_name} has been kicked too recently. " "I only kick people at most once every " f"{self.interval // 60} minutes.") if target.voice is None: return f"{target.display_name} isn't in a voice channel." if message.author.voice is None: return ("You can't kick people you aren't " "in a voice channel with!") channel = target.voice.channel if channel is None or channel != message.author.voice.channel: return ("You can't kick people you aren't " "in a voice channel with!") voice_members = sum([1 if not m.bot else 0 for m in channel.members]) if voice_members < 3: return ("Sorry, I don't kick people from voice channels " "with less than 3 members.") required_votes = int(voice_members / 2 + 1) self.add_vote(target, message.author, channel) vote_count = self.vote_count(target, channel) if vote_count >= required_votes: try: await target.move_to(None, reason="Democracy is a beautiful thing.") self.cooldown.append((target, time.time())) except discord.Forbidden: return "Tragically, I don't have permission to do that." if self.limit_channel: try: await channel.edit( reason="Prevent kicked user rejoining.", user_limit=voice_members - 1, ) except discord.Forbidden: utilities.log_message("Failed to set member limit.") self.scrub_votes(target) return discord.Embed( title="User kicked.", description=f"The council has spoken. {target.mention} has" " been disconnected.", ).set_thumbnail(url=Kick.THUMBNAIL_URL) else: needed_votes = required_votes - vote_count return f"Vote received! {needed_votes} more votes required."