From 11fa20706f33879ec8f820ea786a7f765b3d1b2d Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Tue, 24 Jul 2018 17:55:59 -0700 Subject: [PATCH 01/26] Fixed bug in /monsters with creatures without a bestiary class defined --- cogs/general.py | 1 - cogs/tibiawiki.py | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cogs/general.py b/cogs/general.py index 242217a..dbf66c1 100644 --- a/cogs/general.py +++ b/cogs/general.py @@ -266,7 +266,6 @@ async def _help(self, ctx, *, command: str = None): p = await HelpPaginator.from_command(ctx, entity) else: p = await HelpPaginator.from_cog(ctx, entity) - await p.paginate() except Exception as e: await ctx.send(e) diff --git a/cogs/tibiawiki.py b/cogs/tibiawiki.py index a830fe1..b6af930 100644 --- a/cogs/tibiawiki.py +++ b/cogs/tibiawiki.py @@ -519,7 +519,9 @@ def get_monster_embed(ctx: NabCtx, monster, long): required_kills = 5 given_points = max(points[monster['bestiary_level']]*2, 5) bestiary_info += f"\n{occurrence}" - bestiary_info += f"\n{required_kills:,} kills | {given_points}{config.charms_emoji}️" + if monster["bestiary_level"] is not None: + + bestiary_info += f"\n{required_kills:,} kills | {given_points}{config.charms_emoji}️" embed.add_field(name="Bestiary Class", value=bestiary_info) # If monster drops no loot, we might as well show everything From fc6fa83da270018b6768743c1e125edf7cae6bcb Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Wed, 25 Jul 2018 13:46:22 -0700 Subject: [PATCH 02/26] Added canembed check for commands that use embeds - This check raises a CannotEmbed exception that is caught by the global on_error to display a message. - Added check to commands in general cog. --- cogs/general.py | 63 ++++++++++++++++++++++++++++-------------------- nabbot.py | 4 ++- utils/checks.py | 11 +++++++++ utils/general.py | 6 +++++ 4 files changed, 57 insertions(+), 27 deletions(-) diff --git a/cogs/general.py b/cogs/general.py index dbf66c1..1d6faa7 100644 --- a/cogs/general.py +++ b/cogs/general.py @@ -13,13 +13,14 @@ from discord.ext import commands from nabbot import NabBot +from utils import checks from utils.config import config from utils.context import NabCtx from utils.database import userDatabase, get_server_property from utils.general import parse_uptime, TimeString, single_line, log, BadTime, get_user_avatar, get_region_string, \ clean_string, is_numeric from utils.pages import CannotPaginate, VocationPages, HelpPaginator -from utils.tibia import get_voc_abb, get_voc_emoji +from utils.tibia import get_voc_abb, get_voc_emoji, tibia_worlds EVENT_NAME_LIMIT = 50 EVENT_DESCRIPTION_LIMIT = 400 @@ -127,12 +128,10 @@ async def events_announce(self): await asyncio.sleep(20) # Commands + @checks.can_embed() @commands.command() async def about(self, ctx: NabCtx): """Shows basic information about the bot.""" - if not ctx.bot_permissions.embed_links: - await ctx.send("Sorry, I need `Embed Links` permissions for this command.") - return embed = discord.Embed(description=ctx.bot.description, colour=discord.Colour.blurple()) embed.set_author(name="NabBot", url="https://github.com/Galarzaa90/NabBot", icon_url="https://github.com/fluidicon.png") @@ -155,13 +154,10 @@ async def about(self, ctx: NabCtx): embed.set_footer(text=f"Uptime | {parse_uptime(self.bot.start_time, True)}") await ctx.send(embed=embed) + @checks.can_embed() @commands.command(name="botinfo") async def bot_info(self, ctx: NabCtx): """Shows advanced information about the bot.""" - permissions = ctx.bot_permissions - if not permissions.embed_links: - await ctx.send("Sorry, I need `Embed Links` permission for this command.") - return char_count = 0 deaths_count = 0 levels_count = 0 @@ -211,6 +207,7 @@ def ram(value): f"πŸ’¬ Channels: **{len(list(self.bot.get_all_channels())):,}**\n"\ f"πŸ‘¨ Users: **{len(self.bot.users):,}** \n" \ f"πŸ‘€ Characters: **{char_count:,}**\n" \ + f"🌐 Tracked worlds: **{len(self.bot.tracked_worlds_list)}/{len(tibia_worlds)}**\n" \ f"{config.levelup_emoji} Level ups: **{levels_count:,}**\n" \ f"{config.death_emoji} Deaths: **{deaths_count:,}**" await ctx.send(embed=embed) @@ -228,6 +225,7 @@ async def choose(self, ctx, *choices: str): user = ctx.author await ctx.send('Alright, **@{0}**, I choose: "{1}"'.format(user.display_name, random.choice(choices))) + @checks.can_embed() @commands.command(name='help', aliases=["commands"]) async def _help(self, ctx, *, command: str = None): """Shows help about a command or the bot. @@ -252,7 +250,6 @@ async def _help(self, ctx, *, command: str = None): πŸ”Έ This means the command has subcommands. Check the command's help to see them.""" - try: if command is None: p = await HelpPaginator.from_bot(ctx) @@ -327,14 +324,12 @@ def repl(obj): await destination.send(page) @commands.guild_only() + @checks.can_embed() @commands.group(aliases=["event"], invoke_without_command=True, case_insensitive=True, usage="[event id]") async def events(self, ctx: NabCtx, event_id: int=None): """Shows a list of upcoming and recent events. If a number is specified, it will show details for that event. Same as using `events info`""" - if not ctx.bot_permissions.embed_links and ctx.is_private: - await ctx.send("Sorry, I need `Embed Links` permission for this command.") - return if event_id is not None: await ctx.invoke(self.bot.all_commands.get('events').get_command("info"), event_id) return @@ -389,6 +384,7 @@ async def events(self, ctx: NabCtx, event_id: int=None): await ctx.send(embed=embed) @commands.guild_only() + @checks.can_embed() @events.command(name="add", usage=" [,description]") async def event_add(self, ctx, starts_in: TimeString, *, params): """Creates a new event. @@ -513,6 +509,7 @@ async def event_edit(self, ctx): await ctx.send(content) @commands.guild_only() + @checks.can_embed() @event_edit.command(name="description", aliases=["desc", "details"], usage=" [new description]") async def event_edit_description(self, ctx: NabCtx, event_id: int, *, new_description=None): """Edits an event's description. @@ -722,6 +719,7 @@ async def event_edit_slots(self, ctx: NabCtx, event_id: int, slots: int=None): f"{ctx.author.mention}") @commands.guild_only() + @checks.can_embed() @event_edit.command(name="time", aliases=["start"], usage=" [new start time]") async def event_edit_time(self, ctx: NabCtx, event_id: int, starts_in: TimeString=None): """Edit's an event's start time. @@ -781,6 +779,7 @@ async def event_edit_time(self, ctx: NabCtx, event_id: int, starts_in: TimeStrin skip_creator=True) @commands.guild_only() + @checks.can_embed() @events.command(name="info", aliases=["show"]) async def event_info(self, ctx: NabCtx, event_id: int): """Displays an event's info. @@ -898,6 +897,7 @@ async def event_leave(self, ctx, event_id: int): return @commands.guild_only() + @checks.can_embed() @events.command(name="make", aliases=["creator", "maker"]) async def event_make(self, ctx: NabCtx): """Creates an event guiding you step by step @@ -1029,6 +1029,7 @@ async def event_make(self, ctx: NabCtx): f"*To edit this event use ID {event_id}*") @commands.guild_only() + @checks.can_embed() @events.command(name="participants") async def event_participants(self, ctx, event_id: int): """Shows the list of characters participating in this event.""" @@ -1144,6 +1145,7 @@ async def event_removeplayer(self, ctx, event_id: int, *, character): return @commands.guild_only() + @checks.can_embed() @events.command(name="subscribe", aliases=["sub"]) async def event_subscribe(self, ctx, event_id: int): """Subscribe to receive a PM when an event is happening.""" @@ -1207,6 +1209,7 @@ async def event_unsubscribe(self, ctx, event_id: int): @commands.guild_only() @commands.has_permissions(manage_roles=True) + @checks.can_embed() @commands.command(nam="permissions", aliases=["perms"]) async def permissions(self, ctx: NabCtx, member: discord.Member=None, channel: discord.TextChannel=None): """Shows a member's permissions in the current channel. @@ -1233,6 +1236,7 @@ async def permissions(self, ctx: NabCtx, member: discord.Member=None, channel: d await ctx.send(embed=embed) @commands.guild_only() + @checks.can_embed() @commands.command() async def quote(self, ctx: NabCtx, message_id: int): """Shows a messages by its ID. @@ -1323,6 +1327,7 @@ async def roll(self, ctx: NabCtx, params=None): @commands.guild_only() @commands.command() + @checks.can_embed() async def serverinfo(self, ctx: NabCtx): """Shows the server's information.""" permissions = ctx.bot_permissions @@ -1330,24 +1335,26 @@ async def serverinfo(self, ctx: NabCtx): await ctx.send("Sorry, I need `Embed Links` permission for this command.") return guild = ctx.guild - embed = discord.Embed(title=guild.name, timestamp=guild.created_at, description=f"**ID** {guild.id}", - color=discord.Color.blurple()) + embed = discord.Embed(title=guild.name, timestamp=guild.created_at, color=discord.Color.blurple()) embed.set_footer(text="Created on") embed.set_thumbnail(url=guild.icon_url) + embed.add_field(name="ID", value=str(guild.id), inline=False) embed.add_field(name="Owner", value=guild.owner.mention) embed.add_field(name="Voice Region", value=get_region_string(guild.region)) - embed.add_field(name="Channels", - value=f"Text: {len(guild.channels):,}\n" - f"Voice: {len(guild.voice_channels):,}\n" - f"Categories: {len(guild.categories):,}") + embed.add_field(name=f"Channels ({len(guild.text_channels)+len(guild.voice_channels):,})", + value=f"πŸ“„ Text: **{len(guild.text_channels):,}**\n" + f"πŸŽ™ Voice: **{len(guild.voice_channels):,}**\n" + f"πŸ—‚ Categories: **{len(guild.categories):,}**") status_count = Counter(str(m.status) for m in guild.members) + bot_count = len(list(filter(lambda m: m.bot, guild.members))) if config.use_status_emojis: - embed.add_field(name="Members", - value=f"Total: {len(guild.members):,}\n" - f"{status_count['online']:,}{config.status_emojis['online']} " - f"{status_count['idle']:,}{config.status_emojis['idle']} " - f"{status_count['dnd']:,}{config.status_emojis['dnd']} " - f"{status_count['offline']:,}{config.status_emojis['offline']}") + embed.add_field(name=f"Members ({len(guild.members):,})", + value=f"πŸ‘¨ Humans: **{len(guild.members)-bot_count:,}**\n" + f"πŸ€– Bots: **{bot_count:,}**\n" + f"**{status_count['online']:,}**{config.status_emojis['online']} " + f"**{status_count['idle']:,}**{config.status_emojis['idle']} " + f"**{status_count['dnd']:,}**{config.status_emojis['dnd']} " + f"**{status_count['offline']:,}**{config.status_emojis['offline']}") else: embed.add_field(name="Members", value=f"Total: {len(guild.members):,}\n" @@ -1355,8 +1362,11 @@ async def serverinfo(self, ctx: NabCtx): f"Idle: {status_count['idle']:,}\n" f"Busy: {status_count['dnd']:,}\n" f"Offline: {status_count['offline']:,}") - embed.add_field(name="Roles", value=len(guild.roles)) - embed.add_field(name="Emojis", value=len(guild.emojis)) + embed.add_field(name="Roles", value=f"{len(guild.roles):,}") + embed.add_field(name="Emojis", value=f"{len(guild.emojis):,}") + if guild.splash_url: + embed.add_field(name="Splash screen", value="\u200F", inline=True) + embed.set_image(url=guild.splash_url) await ctx.send(embed=embed) @commands.command() @@ -1365,6 +1375,7 @@ async def uptime(self, ctx): await ctx.send("I have been running for {0}.".format(parse_uptime(self.bot.start_time, True))) @commands.guild_only() + @checks.can_embed() @commands.command(aliases=["memberinfo"]) async def userinfo(self, ctx, *, user: str=None): """Shows a user's information.""" diff --git a/nabbot.py b/nabbot.py index bee856d..d3db7bd 100644 --- a/nabbot.py +++ b/nabbot.py @@ -10,7 +10,7 @@ from utils import context from utils.config import config from utils.database import init_database, userDatabase, get_server_property -from utils.general import join_list, get_token, get_user_avatar, get_region_string +from utils.general import join_list, get_token, get_user_avatar, get_region_string, CannotEmbed from utils.general import log from utils.help_format import NabHelpFormat from utils.tibia import populate_worlds, tibia_worlds, get_voc_abb_and_emoji @@ -101,6 +101,8 @@ async def on_command_error(self, ctx: context.NabCtx, error): return elif isinstance(error, commands.NoPrivateMessage): await ctx.send(error) + elif isinstance(error, CannotEmbed): + await ctx.send(f"{ctx.tick(False)} Sorry, `Embed Links` permission is required for this command.") elif isinstance(error, commands.CommandInvokeError): log.error(f"Exception in command: {ctx.message.clean_content}", exc_info=error.original) if isinstance(error.original, discord.HTTPException): diff --git a/utils/checks.py b/utils/checks.py index ab860d1..8b177ad 100644 --- a/utils/checks.py +++ b/utils/checks.py @@ -6,6 +6,9 @@ # Checks +from utils.general import CannotEmbed + + def is_owner(): """Check if the author is the bot's owner""" async def predicate(ctx): @@ -81,6 +84,14 @@ def predicate(ctx): return commands.check(predicate) +def can_embed(): + def predicate(ctx: NabCtx): + if not ctx.bot_permissions.embed_links: + raise CannotEmbed() + return True + return commands.check(predicate) + + def is_not_lite(): """Checks if the bot is not running in lite mode""" def predicate(ctx: NabCtx): diff --git a/utils/general.py b/utils/general.py index de80de4..8ddefab 100644 --- a/utils/general.py +++ b/utils/general.py @@ -338,10 +338,16 @@ def single_line(string: str) -> str: :return: The converted string. """ return string.replace("\r\n", " ").replace("\n", " ") + + class BadTime(commands.BadArgument): pass +class CannotEmbed(commands.CheckFailure): + pass + + class TimeString: def __init__(self, argument): compiled = re.compile(r"(?:(?P\d+)d)?(?:(?P\d+)h)?(?:(?P\d+)m)?(?:(?P\d+)s)?") From fac62c324d63af963ecd49ce5f7a179faa35d9ee Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Thu, 26 Jul 2018 09:18:19 -0700 Subject: [PATCH 03/26] Added loading message to /im - Added /reloadconfig command - Minor changes to /about, /userinfo and /botinfo - Added Use External Emoji to /checkchannel - Added warn_emoji configuration --- CHANGELOG.md | 3 ++ cogs/admin.py | 25 +++++----- cogs/general.py | 27 +++++++---- cogs/owner.py | 12 ++++- cogs/tracking.py | 99 +++++++++++++++++++--------------------- data/config_template.yml | 1 + nabbot.py | 14 ++++-- utils/config.py | 3 +- 8 files changed, 108 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9bc327..bd069c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ # Changelog +## Version 1.4.1 (Unreleased) +- Small changes to `/userinfo` + ## Version 1.4.0 (2018-07-24) - `/loot` has been rewritten: - Loot database remade from scratch with images extracted directly from the client, all images should now be pixel perfect matches to those taken from in-game screenshots diff --git a/cogs/admin.py b/cogs/admin.py index 437680e..a6fb697 100644 --- a/cogs/admin.py +++ b/cogs/admin.py @@ -5,6 +5,7 @@ from nabbot import NabBot from utils import checks +from utils.config import config from utils.context import NabCtx from utils.database import * from utils.general import join_list, log, get_user_avatar @@ -43,23 +44,22 @@ async def add_account(self, ctx: NabCtx, *, params): target = self.bot.get_member(target_name, ctx.guild) if target is None: - await ctx.send(f"{ctx.tick(False)} I couldn't find any users named @{target_name}") - return + return await ctx.send(f"{ctx.tick(False)} I couldn't find any users named @{target_name}") if target.bot: - await ctx.send(f"{ctx.tick(False)} You can't register characters to discord bots!") - return + return await ctx.send(f"{ctx.tick(False)} You can't register characters to discord bots!") + + # Get list of the user's shared servers with the bot target_guilds = self.bot.get_user_guilds(target.id) + # Filter only the servers that follow the same as the current context target_guilds = list(filter(lambda x: self.bot.tracked_worlds.get(x.id) == world, target_guilds)) - await ctx.trigger_typing() + msg = await ctx.send(f"{config.loading_emoji} Fetching characters...") try: char = await get_character(char_name) if char is None: - await ctx.send("That character doesn't exist.") - return + return await msg.edit(content="That character doesn't exist.") except NetworkError: - await ctx.send("I couldn't fetch the character, please try again.") - return + return await msg.edit(content="I couldn't fetch the character, please try again.") chars = char.other_characters # If the char is hidden,we still add the searched character, if we have just one, we replace it with the # searched char, so we don't have to look him up again @@ -75,7 +75,9 @@ async def add_account(self, ctx: NabCtx, *, params): skipped.append(char) continue with closing(userDatabase.cursor()) as c: - c.execute("SELECT name, guild, user_id as owner, abs(level) as level FROM chars WHERE name LIKE ?", + c.execute("SELECT name, guild, user_id as owner, abs(level) as level " + "FROM chars " + "WHERE name LIKE ?", (char.name,)) db_char = c.fetchone() if db_char is not None: @@ -274,6 +276,7 @@ async def checkchannel(self, ctx: NabCtx, *, channel: discord.TextChannel = None "read_messages": ["error", "I won't be able to see commands in here."], "send_messages": ["error", "I won't be able to respond in here."], "add_reactions": ["error", "Pagination or commands that require emoji confirmation won't work."], + "external_emojis": ["warn", "I won't be able to show my emojis in some commands."], "read_message_history": ["error", "I won't be able to see your reactions in commands."], "manage_messages": ["warn", "Command pagination won't work well and I won't be able to delete messages " "in the ask channel."], @@ -286,7 +289,7 @@ async def checkchannel(self, ctx: NabCtx, *, channel: discord.TextChannel = None if not perm_dict[k]: ok = False perm_name = k.replace("_", " ").title() - icon = ctx.tick(False) if level == "error" else "⚠" + icon = ctx.tick(False) if level == "error" else config.warn_emoji content += f"\nMissing `{perm_name}` permission" content += f"\n\t{icon} {explain}" if ok: diff --git a/cogs/general.py b/cogs/general.py index 1d6faa7..0a9d434 100644 --- a/cogs/general.py +++ b/cogs/general.py @@ -144,13 +144,16 @@ async def about(self, ctx: NabCtx): "\u2023 [Nezune](https://github.com/Nezune)") embed.add_field(name="Created", value="March 30th 2016") embed.add_field(name="Version", value=f"v{self.bot.__version__}") - embed.add_field(name="Platform", value="Python") + embed.add_field(name="Platform", value="Python " + "([discord.py](https://github.com/Rapptz/discord.py/tree/rewrite))") embed.add_field(name="Servers", value=f"{len(self.bot.guilds):,}") embed.add_field(name="Users", value=f"{len(self.bot.users):,}") - embed.add_field(name="Website", value="[nabbot.ddns.net](https://galarzaa90.github.io/NabBot/)") - embed.add_field(name="Discord", value="[discord.me/NabBot](https://discord.me/nabbot)") - embed.add_field(name="Donate", value="[PayPal](https://www.paypal.com/cgi-bin/webscr?" - "cmd=_s-xclick&hosted_button_id=B33DCPZ9D3GMJ)") + embed.add_field(name="Links", inline=False, + value=f"[Add to your server](https://discordbots.org/bot/178966653982212096) | " + f"[Support Server](https://discord.me/nabbot) | " + f"[Docs](https://galarzaa90.github.io/NabBot) | " + f"[Donate](https://www.paypal.com/cgi-bin/webscr?" + f"cmd=_s-xclick&hosted_button_id=B33DCPZ9D3GMJ)") embed.set_footer(text=f"Uptime | {parse_uptime(self.bot.start_time, True)}") await ctx.send(embed=embed) @@ -1378,7 +1381,13 @@ async def uptime(self, ctx): @checks.can_embed() @commands.command(aliases=["memberinfo"]) async def userinfo(self, ctx, *, user: str=None): - """Shows a user's information.""" + """Shows a user's information. + + About user statutes: + - Server Owner: Owner of the server + - Server Admin: User with Administrator permission + - Server Moderator: User with `Manage Server` permissions. + - Channel Moderator: User with `Manage Channels` permissions in at least one channel.""" if user is None: user = ctx.author else: @@ -1387,8 +1396,9 @@ async def userinfo(self, ctx, *, user: str=None): await ctx.send(f"Could not find user `{user}`") return user = _user - embed = discord.Embed(title=f"{user.name}#{user.discriminator}", - timestamp=user.joined_at, colour=user.colour) + embed = discord.Embed(title=f"{user.name}#{user.discriminator}", timestamp=user.joined_at, colour=user.colour) + if config.use_status_emojis: + embed.title += config.status_emojis[str(user.status)] embed.set_thumbnail(url=get_user_avatar(user)) embed.set_footer(text="Member since") embed.add_field(name="ID", value=user.id) @@ -1410,6 +1420,7 @@ async def userinfo(self, ctx, *, user: str=None): embed.add_field(name="Servers", value=f"{len(self.bot.get_user_guilds(user.id))} shared") embed.add_field(name="Roles", value=f"{len(user.roles):,}") + embed.add_field(name="Highest role", value=f"{user.top_role.mention}") await ctx.send(embed=embed) diff --git a/cogs/owner.py b/cogs/owner.py index b6f1dfa..d4d91a7 100644 --- a/cogs/owner.py +++ b/cogs/owner.py @@ -367,6 +367,16 @@ async def reload_cog(self, ctx: NabCtx, *, cog): else: await ctx.send(f"{ctx.tick()} Cog reloaded successfully.") + @checks.is_owner() + @commands.command(name="reloadconfig") + async def reload_config(self, ctx): + """Reloads the configuration file.""" + try: + config.parse() + await ctx.send(f"{ctx.tick()} Config file reloaded.") + except Exception: + await ctx.send(f'```py\n{traceback.format_exc()}\n```') + @commands.command(hidden=True) @checks.is_owner() async def repl(self, ctx: NabCtx): @@ -571,7 +581,7 @@ def comp(operator, object1, object2): if not comp(package[1], StrictVersion(version), StrictVersion(package[2])): value = f"{ctx.tick(False)}v{version}\n`At least v{package[2]} expected`" elif not comp(package[3], StrictVersion(version), StrictVersion(package[4])): - value = f"⚠v{version}\n`Only below v{package[4]} tested`" + value = f"{config.warn_emoji}v{version}\n`Only below v{package[4]} tested`" else: value = f"{ctx.tick(True)}v{version}" embed.add_field(name=package[0], value=value) diff --git a/cogs/tracking.py b/cogs/tracking.py index 24a3dd3..dcbb999 100644 --- a/cogs/tracking.py +++ b/cogs/tracking.py @@ -530,8 +530,7 @@ async def claim(self, ctx: NabCtx, *, char_name: str = None): user_tibia_worlds = list(set(user_tibia_worlds)) if not ctx.is_private and self.bot.tracked_worlds.get(ctx.guild.id) is None: - await ctx.send("This server is not tracking any tibia worlds.") - return + return await ctx.send("This server is not tracking any tibia worlds.") if len(user_tibia_worlds) == 0: return @@ -694,21 +693,18 @@ async def im(self, ctx: NabCtx, *, char_name: str): user_tibia_worlds = list(set(user_tibia_worlds)) if not ctx.is_private and ctx.world is None: - await ctx.send("This server is not tracking any tibia worlds.") - return + return await ctx.send("This server is not tracking any tibia worlds.") if len(user_tibia_worlds) == 0: return - await ctx.trigger_typing() + msg = await ctx.send(f"{config.loading_emoji} Fetching character...") try: char = await get_character(char_name) if char is None: - await ctx.send("That character doesn't exist.") - return + return await msg.edit(content="That character doesn't exist.") except NetworkError: - await ctx.send("I couldn't fetch the character, please try again.") - return + return await msg.edit(content="I couldn't fetch the character, please try again.") chars = char.other_characters check_other = False if len(chars) > 1: @@ -720,57 +716,56 @@ async def im(self, ctx: NabCtx, *, char_name: str): if not check_other: chars = [char] + if check_other: + await msg.delete() + msg = await ctx.send(f"{config.loading_emoji} Fetching characters...") + skipped = [] updated = [] added: List[Character] = [] existent = [] - with ctx.typing(): - for char in chars: - # Skip chars in non-tracked worlds - if char.world not in user_tibia_worlds: - skipped.append(char) + for char in chars: + # Skip chars in non-tracked worlds + if char.world not in user_tibia_worlds: + skipped.append(char) + continue + with closing(userDatabase.cursor()) as c: + c.execute("SELECT name, guild, user_id as owner, vocation, ABS(level) as level, guild " + "FROM chars " + "WHERE name LIKE ?", (char.name,)) + db_char = c.fetchone() + if db_char is not None: + owner = self.bot.get_member(db_char["owner"]) + # Previous owner doesn't exist anymore + if owner is None: + updated.append({'name': char.name, 'world': char.world, 'prevowner': db_char["owner"], + 'vocation': db_char["vocation"], 'level': db_char['level'], + 'guild': db_char['guild'] + }) continue - with closing(userDatabase.cursor()) as c: - c.execute("SELECT name, guild, user_id as owner, vocation, ABS(level) as level, guild " - "FROM chars " - "WHERE name LIKE ?", (char.name,)) - db_char = c.fetchone() - if db_char is not None: - owner = self.bot.get_member(db_char["owner"]) - # Previous owner doesn't exist anymore - if owner is None: - updated.append({'name': char.name, 'world': char.world, 'prevowner': db_char["owner"], - 'vocation': db_char["vocation"], 'level': db_char['level'], - 'guild': db_char['guild'] - }) - continue - # Char already registered to this user - elif owner.id == user.id: - existent.append("{0.name} ({0.world})".format(char)) - continue - # Character is registered to another user, we stop the whole process - else: - reply = "Sorry, a character in that account ({0}) is already registered to **{1}**.\n" \ - "If the character really belongs to you, try using `{2}claim {0}`." - await ctx.send(reply.format(db_char["name"], owner, ctx.clean_prefix)) - return - # If we only have one char, it already contains full data - if len(chars) > 1: - try: - await ctx.channel.trigger_typing() - char = await get_character(char.name) - except NetworkError: - await ctx.send("I'm having network issues, please try again.") - return - if char.deleted is not None: - skipped.append(char) + # Char already registered to this user + elif owner.id == user.id: + existent.append("{0.name} ({0.world})".format(char)) continue - added.append(char) + # Character is registered to another user, we stop the whole process + else: + reply = "Sorry, a character in that account ({0}) is already registered to **{1}**.\n" \ + "If the character really belongs to you, try using `{2}claim {0}`." + return await msg.edit(content=reply.format(db_char["name"], owner, ctx.clean_prefix)) + # If we only have one char, it already contains full data + if len(chars) > 1: + try: + char = await get_character(char.name) + except NetworkError: + return await msg.edit("I'm having network issues, please try again.") + if char.deleted is not None: + skipped.append(char) + continue + added.append(char) if len(skipped) == len(chars): reply = "Sorry, I couldn't find any characters from the servers I track ({0})." - await ctx.send(reply.format(join_list(user_tibia_worlds, ", ", " and "))) - return + return await msg.edit(content=reply.format(join_list(user_tibia_worlds, ", ", " and "))) reply = "" log_reply = dict().fromkeys([server.id for server in user_guilds], "") @@ -820,7 +815,7 @@ async def im(self, ctx: NabCtx, *, char_name: str): with userDatabase as conn: conn.execute("INSERT OR IGNORE INTO users (id, name) VALUES (?, ?)", (user.id, user.display_name,)) conn.execute("UPDATE users SET name = ? WHERE id = ?", (user.display_name, user.id,)) - await ctx.send(reply) + await msg.edit(content=reply) for server_id, message in log_reply.items(): if message: guild = self.bot.get_guild(server_id) diff --git a/data/config_template.yml b/data/config_template.yml index e7c4610..792ba60 100644 --- a/data/config_template.yml +++ b/data/config_template.yml @@ -68,6 +68,7 @@ network_retry_delay: 1 online_emoji: πŸ”Ή true_emoji: βœ… false_emoji: ❌ +warn_emoji: ⚠ levelup_emoji: 🌟 death_emoji: ☠ pvpdeath_emoji: πŸ’€ diff --git a/nabbot.py b/nabbot.py index d3db7bd..d53591b 100644 --- a/nabbot.py +++ b/nabbot.py @@ -43,7 +43,7 @@ def __init__(self): # A list version is created from the dictionary self.tracked_worlds = {} self.tracked_worlds_list = [] - self.__version__ = "1.4.0" + self.__version__ = "1.4.1a" self.__min_discord__ = 1480 async def on_ready(self): @@ -422,7 +422,8 @@ def get_member(self, argument: Union[str, int], guild: Union[discord.Guild, List :return: The member found or None. """ id_regex = re.compile(r'([0-9]{15,21})$') - match = id_regex.match(str(argument)) + mention_regex = re.compile(r'<@!?([0-9]+)>$') + match = id_regex.match(str(argument)) or mention_regex.match(str(argument)) if match is None: return self.get_member_named(argument, guild) else: @@ -461,7 +462,10 @@ def get_member_named(self, name: str, guild: Union[discord.Guild, List[discord.G def get_user_guilds(self, user_id: int) -> List[discord.Guild]: """Returns a list of the user's shared guilds with the bot""" - return [self.get_guild(gid) for gid in self.members[user_id]] + try: + return [self.get_guild(gid) for gid in self.members[user_id]] + except KeyError: + return [] def get_user_worlds(self, user_id: int, guild_list=None) -> List[str]: """Returns a list of all the tibia worlds the user is tracked in. @@ -589,6 +593,8 @@ def reload_worlds(self): try: nabbot.load_extension(cog) print(f"Cog {cog} loaded successfully.") + except ModuleNotFoundError: + print(f"Could not find cog: {cog}") except Exception as e: print(f'Cog {cog} failed to load:') traceback.print_exc(limit=-1) @@ -597,6 +603,8 @@ def reload_worlds(self): try: nabbot.load_extension(extra) print(f"Extra cog {extra} loaded successfully.") + except ModuleNotFoundError: + print(f"Could not find extra cog: {extra}") except Exception as e: print(f'Extra og {extra} failed to load:') traceback.print_exc(limit=-1) diff --git a/utils/config.py b/utils/config.py index 8f9717c..da6d0e4 100644 --- a/utils/config.py +++ b/utils/config.py @@ -31,6 +31,7 @@ "online_emoji", "true_emoji", "false_emoji", + "warn_emoji", "online_emoji", "true_emoji", "false_emoji", @@ -94,6 +95,7 @@ def __init__(self,): self.online_emoji = "πŸ”Ή" self.true_emoji = "βœ…" self.false_emoji = "❌" + self.warn_emoji = "⚠" self.levelup_emoji = "🌟" self.death_emoji = "☠" self.pvpdeath_emoji = "πŸ’€" @@ -127,7 +129,6 @@ def __repr__(self) -> str: return f"Config({', '.join(attributes)})" def parse(self): - if not os.path.isfile(CONFIG_PATH): print("\tconfig.yml not found, copying from template...") shutil.copyfile(TEMPLATE_PATH, CONFIG_PATH) From cacf6e1eaf3abe7dac5c6124e382f45efaf368a1 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Thu, 26 Jul 2018 15:22:41 -0700 Subject: [PATCH 04/26] Changes to /serverinfo and /servers - /serverinfo allows the owner to check other servers. - /serverinfo now shows tracked world. - /servers now shows a paginator and show server id. --- cogs/general.py | 51 ++++++++++++++++++++++++++++++++----------------- cogs/owner.py | 24 +++++++++++++++-------- 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/cogs/general.py b/cogs/general.py index 0a9d434..8531ba1 100644 --- a/cogs/general.py +++ b/cogs/general.py @@ -1331,18 +1331,28 @@ async def roll(self, ctx: NabCtx, params=None): @commands.guild_only() @commands.command() @checks.can_embed() - async def serverinfo(self, ctx: NabCtx): - """Shows the server's information.""" - permissions = ctx.bot_permissions - if not permissions.embed_links: - await ctx.send("Sorry, I need `Embed Links` permission for this command.") - return - guild = ctx.guild + async def serverinfo(self, ctx: NabCtx, server=None): + """Shows the server's information. + + The bot owner can additionally check the information of a specific server where the bot is. + """ + if await checks.is_owner_check(ctx) and server is not None: + try: + guild = self.bot.get_guild(int(server)) + if guild is None: + return await ctx.send(f"{ctx.tick(False)} I'm not in any server with ID {server}.") + except ValueError: + return await ctx.send(f"{ctx.tick(False)} That is not a valid id.") + else: + guild = ctx.guild embed = discord.Embed(title=guild.name, timestamp=guild.created_at, color=discord.Color.blurple()) embed.set_footer(text="Created on") embed.set_thumbnail(url=guild.icon_url) embed.add_field(name="ID", value=str(guild.id), inline=False) - embed.add_field(name="Owner", value=guild.owner.mention) + if ctx.guild != guild: + embed.add_field(name="Owner", value=str(guild.owner)) + else: + embed.add_field(name="Owner", value=guild.owner.mention) embed.add_field(name="Voice Region", value=get_region_string(guild.region)) embed.add_field(name=f"Channels ({len(guild.text_channels)+len(guild.voice_channels):,})", value=f"πŸ“„ Text: **{len(guild.text_channels):,}**\n" @@ -1352,21 +1362,26 @@ async def serverinfo(self, ctx: NabCtx): bot_count = len(list(filter(lambda m: m.bot, guild.members))) if config.use_status_emojis: embed.add_field(name=f"Members ({len(guild.members):,})", - value=f"πŸ‘¨ Humans: **{len(guild.members)-bot_count:,}**\n" - f"πŸ€– Bots: **{bot_count:,}**\n" - f"**{status_count['online']:,}**{config.status_emojis['online']} " + value=f"**{status_count['online']:,}**{config.status_emojis['online']} " f"**{status_count['idle']:,}**{config.status_emojis['idle']} " f"**{status_count['dnd']:,}**{config.status_emojis['dnd']} " - f"**{status_count['offline']:,}**{config.status_emojis['offline']}") + f"**{status_count['offline']:,}**{config.status_emojis['offline']}\n" + f"πŸ‘¨ Humans: **{len(guild.members)-bot_count:,}**\n" + f"πŸ€– Bots: **{bot_count:,}**" + ) else: - embed.add_field(name="Members", - value=f"Total: {len(guild.members):,}\n" - f"Online: {status_count['online']:,}\n" - f"Idle: {status_count['idle']:,}\n" - f"Busy: {status_count['dnd']:,}\n" - f"Offline: {status_count['offline']:,}") + embed.add_field(name=f"Members ({len(guild.members):,})", + value=f"Online: **{status_count['online']:,}**\n" + f"Idle: **{status_count['idle']:,}**\n" + f"Busy: **{status_count['dnd']:,}**\n" + f"Offline: **{status_count['offline']:,}**\n" + f"Humans: **{len(guild.members)-bot_count:,}**\n" + f"Bots: **{bot_count:,}**" + ) embed.add_field(name="Roles", value=f"{len(guild.roles):,}") embed.add_field(name="Emojis", value=f"{len(guild.emojis):,}") + if self.bot.tracked_worlds.get(guild.id): + embed.add_field(name="Tracked world", value=self.bot.tracked_worlds.get(guild.id)) if guild.splash_url: embed.add_field(name="Splash screen", value="\u200F", inline=True) embed.set_image(url=guild.splash_url) diff --git a/cogs/owner.py b/cogs/owner.py index d4d91a7..64e3ff9 100644 --- a/cogs/owner.py +++ b/cogs/owner.py @@ -16,6 +16,7 @@ from utils.context import NabCtx from utils.general import * from utils.messages import * +from utils.pages import Pages, CannotPaginate from utils.tibia import * from utils.tibiawiki import * @@ -508,16 +509,23 @@ async def sql(self, ctx: NabCtx, *, query: str): else: await ctx.send(fmt) - - @commands.command() @checks.is_owner() + @checks.can_embed() + @commands.command() async def servers(self, ctx: NabCtx): - """Shows a list of servers the bot is in.""" - reply = "I'm in the following servers:" - for guild in self.bot.guilds: - reply += "\n\t**{0.name}** - (Owner: {0.owner.name}#{0.owner.discriminator}) - {1} - {2} members"\ - .format(guild, self.bot.tracked_worlds.get(guild.id, "No world tracked"), len(guild.members)) - await ctx.send(reply) + """Shows a list of servers the bot is in. + + Further information can be obtained using `serverinfo [id]`""" + entries = [] + guilds = sorted(self.bot.guilds, key=lambda g: g.name) + for guild in guilds: + entries.append(f"**{guild.name}** (ID: **{guild.id}**) - {self.bot.tracked_worlds.get(guild.id, 'None')}") + pages = Pages(ctx, entries=entries, per_page=10) + pages.embed.title = f"Servers with {ctx.me.name}" + try: + await pages.paginate() + except CannotPaginate as e: + await ctx.send(e) @commands.command(name="unload") @checks.is_owner() From 3adfb5584f57ad6e50ec09e0244613c36635eb67 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Thu, 26 Jul 2018 15:59:04 -0700 Subject: [PATCH 05/26] Added sorting to /servers command --- cogs/owner.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/cogs/owner.py b/cogs/owner.py index 64e3ff9..2eddf9a 100644 --- a/cogs/owner.py +++ b/cogs/owner.py @@ -512,14 +512,34 @@ async def sql(self, ctx: NabCtx, *, query: str): @checks.is_owner() @checks.can_embed() @commands.command() - async def servers(self, ctx: NabCtx): + async def servers(self, ctx: NabCtx, sort=None): """Shows a list of servers the bot is in. - Further information can be obtained using `serverinfo [id]`""" + Further information can be obtained using `serverinfo [id]`. + + Values can be sorted by using one of the following values for sort: + - name + - members + - world + - created + - joined""" entries = [] - guilds = sorted(self.bot.guilds, key=lambda g: g.name) + + sorters = { + "name": (lambda g: g.name, False, lambda g: self.bot.tracked_worlds.get(g.id, 'None')), + "members": (lambda g: len(g.members), True, lambda g: f"{len(g.members):,} users"), + "world": (lambda g: self.bot.tracked_worlds.get(g.id), False, + lambda g: self.bot.tracked_worlds.get(g.id, 'None')), + "created": (lambda g: g.created_at, False, lambda g: f"Created: {g.created_at.date()}"), + "joined": (lambda g: g.me.joined_at, False, lambda g: f"Joined: {g.me.joined_at.date()}") + } + if sort is None: + sort = "name" + if sort not in sorters: + return await ctx.send(f"{ctx.tick(False)} Invalid sort value. Valid values are: `{', '.join(sorters)}`") + guilds = sorted(self.bot.guilds, key=sorters[sort][0], reverse=sorters[sort][1]) for guild in guilds: - entries.append(f"**{guild.name}** (ID: **{guild.id}**) - {self.bot.tracked_worlds.get(guild.id, 'None')}") + entries.append(f"**{guild.name}** (ID: **{guild.id}**) - {sorters[sort][2](guild)}") pages = Pages(ctx, entries=entries, per_page=10) pages.embed.title = f"Servers with {ctx.me.name}" try: From 79cc0d15e231cb101cf1fdd96c9ae3015e643591 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Thu, 26 Jul 2018 16:15:10 -0700 Subject: [PATCH 06/26] Fixed /servers world not working. --- cogs/owner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/owner.py b/cogs/owner.py index 2eddf9a..cd17e05 100644 --- a/cogs/owner.py +++ b/cogs/owner.py @@ -528,7 +528,7 @@ async def servers(self, ctx: NabCtx, sort=None): sorters = { "name": (lambda g: g.name, False, lambda g: self.bot.tracked_worlds.get(g.id, 'None')), "members": (lambda g: len(g.members), True, lambda g: f"{len(g.members):,} users"), - "world": (lambda g: self.bot.tracked_worlds.get(g.id), False, + "world": (lambda g: self.bot.tracked_worlds.get(g.id, "|"), False, lambda g: self.bot.tracked_worlds.get(g.id, 'None')), "created": (lambda g: g.created_at, False, lambda g: f"Created: {g.created_at.date()}"), "joined": (lambda g: g.me.joined_at, False, lambda g: f"Joined: {g.me.joined_at.date()}") From c3c2144863b6d54ca036a131fca2f9c6a98573c6 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Thu, 26 Jul 2018 23:41:20 -0700 Subject: [PATCH 07/26] NabBot responds to a mention with its prefixes. --- nabbot.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/nabbot.py b/nabbot.py index d53591b..2beef33 100644 --- a/nabbot.py +++ b/nabbot.py @@ -85,6 +85,18 @@ async def on_message(self, message: discord.Message): # This is a PM, no further info needed if message.guild is None: return + if message.content.strip() == f"<@{self.user.id}>": + prefixes = list(config.command_prefix) + if ctx.guild: + prefixes = get_server_property(ctx.guild.id, "prefixes", deserialize=True, default=prefixes) + if prefixes: + prefixes_str = ", ".join(f"`{p}`" for p in prefixes) + return await ctx.send(f"My command prefixes are: {prefixes_str}, and mentions. " + f"To see my commands, try: `{prefixes[0]}help.`", delete_after=10) + else: + return await ctx.send(f"My command prefix is mentions. " + f"To see my commands, try: `@{self.user.name} help.`", delete_after=10) + server_delete = get_server_property(message.guild.id, "commandsonly", is_int=True) global_delete = config.ask_channel_delete if (server_delete is None and global_delete) or server_delete: From 5f0482533d939661ef1a4db593e3e942584f271e Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Fri, 27 Jul 2018 11:56:13 -0700 Subject: [PATCH 08/26] New /emojiinfo command --- cogs/general.py | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/cogs/general.py b/cogs/general.py index 8531ba1..8f5184f 100644 --- a/cogs/general.py +++ b/cogs/general.py @@ -1,3 +1,4 @@ +import aiohttp import asyncio import datetime as dt import platform @@ -37,7 +38,9 @@ async def __error(self, ctx: NabCtx, error): if isinstance(error, BadTime): await ctx.send(error) return - if isinstance(error, commands.UserInputError): + if isinstance(error, commands.BadArgument): + return await ctx.send(error) + elif isinstance(error, commands.UserInputError): await ctx.send(f"{ctx.tick(False)} The correct syntax is: " f"`{ctx.clean_prefix}{ctx.command.qualified_name} {ctx.usage}`.\n" f"Try `{ctx.clean_prefix}help {ctx.command.qualified_name}` for more info.") @@ -326,6 +329,39 @@ def repl(obj): for page in pages: await destination.send(page) + @commands.guild_only() + @commands.command(name="emojiinfo") + async def emoji_info(self, ctx: NabCtx, *, emoji: discord.Emoji=None): + """Shows information about an emoji, or shows all emojis. + + If the command is used with no arguments, all the server emojis are shown. + + If a emoji, it's id or name is provided, it will show more information about it. + + Only emojis in the current servers can be checked.""" + if emoji is not None: + embed = discord.Embed(title=emoji.name, timestamp=emoji.created_at, color=discord.Color.blurple()) + embed.set_thumbnail(url=emoji.url) + embed.set_footer(text="Created at") + embed.add_field(name="ID", value=emoji.id) + embed.add_field(name="Usage", value=f"`{emoji}`") + embed.add_field(name="Attributes", inline=False, + value=f"{ctx.tick(emoji.managed)} Twitch managed\n" + f"{ctx.tick(emoji.require_colons)} Requires colons\n" + f"{ctx.tick(len(emoji.roles) > 0)} Role limited") + else: + emojis: List[discord.Emoji] = ctx.guild.emojis + if not emojis: + return await ctx.send("This server has no custom emojis.") + normal = [str(e) for e in emojis if not e.animated] + animated = [str(e) for e in emojis if e.animated] + embed = discord.Embed(title="Custom Emojis", color=discord.Color.blurple()) + if normal: + embed.add_field(name="Regular", value="".join(normal)) + if animated: + embed.add_field(name="Animated (Nitro required)", value="".join(animated)) + await ctx.send(embed=embed) + @commands.guild_only() @checks.can_embed() @commands.group(aliases=["event"], invoke_without_command=True, case_insensitive=True, usage="[event id]") From b6e382552287971956961faa92bc88094c584a8f Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Fri, 27 Jul 2018 12:23:35 -0700 Subject: [PATCH 09/26] Fixed bug with /emojiinfo with too many emojis --- cogs/general.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/cogs/general.py b/cogs/general.py index 8f5184f..52693ae 100644 --- a/cogs/general.py +++ b/cogs/general.py @@ -19,7 +19,8 @@ from utils.context import NabCtx from utils.database import userDatabase, get_server_property from utils.general import parse_uptime, TimeString, single_line, log, BadTime, get_user_avatar, get_region_string, \ - clean_string, is_numeric + clean_string, is_numeric, FIELD_VALUE_LIMIT +from utils.messages import split_message from utils.pages import CannotPaginate, VocationPages, HelpPaginator from utils.tibia import get_voc_abb, get_voc_emoji, tibia_worlds @@ -357,9 +358,23 @@ async def emoji_info(self, ctx: NabCtx, *, emoji: discord.Emoji=None): animated = [str(e) for e in emojis if e.animated] embed = discord.Embed(title="Custom Emojis", color=discord.Color.blurple()) if normal: - embed.add_field(name="Regular", value="".join(normal)) + emojis_str = "\n".join(normal) + fields = split_message(emojis_str, FIELD_VALUE_LIMIT) + for i, value in enumerate(fields): + if i == 0: + name = f"Regular ({len(normal)})" + else: + name = "\u200F" + embed.add_field(name=name, value=value.replace("\n", "")) if animated: - embed.add_field(name="Animated (Nitro required)", value="".join(animated)) + emojis_str = "\n".join(animated) + fields = split_message(emojis_str, FIELD_VALUE_LIMIT) + for i, value in enumerate(fields): + if i == 0: + name = f"Animated (Nitro required) ({len(animated)})" + else: + name = "\u200F" + embed.add_field(name=name, value=value.replace("\n", "")) await ctx.send(embed=embed) @commands.guild_only() From c3a6946a6da29ce0bfc8861b56f29713743d118c Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Fri, 27 Jul 2018 16:54:13 -0700 Subject: [PATCH 10/26] Created new cog Info - Moved some commands from General to Info. - /commands is no longer an alias of /help, but a separate command. - /commands shows a simple list of all available commands. --- cogs/general.py | 355 +------------------------------------------ cogs/info.py | 390 ++++++++++++++++++++++++++++++++++++++++++++++++ nabbot.py | 4 +- 3 files changed, 395 insertions(+), 354 deletions(-) create mode 100644 cogs/info.py diff --git a/cogs/general.py b/cogs/general.py index 52693ae..43917d9 100644 --- a/cogs/general.py +++ b/cogs/general.py @@ -1,16 +1,11 @@ -import aiohttp import asyncio import datetime as dt -import platform import random -import re import time -from collections import Counter from contextlib import closing from typing import Union, Dict, Optional, List import discord -import psutil from discord.ext import commands from nabbot import NabBot @@ -18,11 +13,9 @@ from utils.config import config from utils.context import NabCtx from utils.database import userDatabase, get_server_property -from utils.general import parse_uptime, TimeString, single_line, log, BadTime, get_user_avatar, get_region_string, \ - clean_string, is_numeric, FIELD_VALUE_LIMIT -from utils.messages import split_message -from utils.pages import CannotPaginate, VocationPages, HelpPaginator -from utils.tibia import get_voc_abb, get_voc_emoji, tibia_worlds +from utils.general import TimeString, single_line, log, BadTime, get_user_avatar, clean_string, is_numeric +from utils.pages import CannotPaginate, VocationPages +from utils.tibia import get_voc_abb, get_voc_emoji EVENT_NAME_LIMIT = 50 EVENT_DESCRIPTION_LIMIT = 400 @@ -132,93 +125,6 @@ async def events_announce(self): await asyncio.sleep(20) # Commands - @checks.can_embed() - @commands.command() - async def about(self, ctx: NabCtx): - """Shows basic information about the bot.""" - embed = discord.Embed(description=ctx.bot.description, colour=discord.Colour.blurple()) - embed.set_author(name="NabBot", url="https://github.com/Galarzaa90/NabBot", - icon_url="https://github.com/fluidicon.png") - prefixes = list(config.command_prefix) - if ctx.guild: - prefixes = get_server_property(ctx.guild.id, "prefixes", deserialize=True, default=prefixes) - prefixes_str = "\n".join(f"- `{p}`" for p in prefixes) - embed.add_field(name="Prefixes", value=prefixes_str, inline=False) - embed.add_field(name="Authors", value="\u2023 [Galarzaa90](https://github.com/Galarzaa90)\n" - "\u2023 [Nezune](https://github.com/Nezune)") - embed.add_field(name="Created", value="March 30th 2016") - embed.add_field(name="Version", value=f"v{self.bot.__version__}") - embed.add_field(name="Platform", value="Python " - "([discord.py](https://github.com/Rapptz/discord.py/tree/rewrite))") - embed.add_field(name="Servers", value=f"{len(self.bot.guilds):,}") - embed.add_field(name="Users", value=f"{len(self.bot.users):,}") - embed.add_field(name="Links", inline=False, - value=f"[Add to your server](https://discordbots.org/bot/178966653982212096) | " - f"[Support Server](https://discord.me/nabbot) | " - f"[Docs](https://galarzaa90.github.io/NabBot) | " - f"[Donate](https://www.paypal.com/cgi-bin/webscr?" - f"cmd=_s-xclick&hosted_button_id=B33DCPZ9D3GMJ)") - embed.set_footer(text=f"Uptime | {parse_uptime(self.bot.start_time, True)}") - await ctx.send(embed=embed) - - @checks.can_embed() - @commands.command(name="botinfo") - async def bot_info(self, ctx: NabCtx): - """Shows advanced information about the bot.""" - char_count = 0 - deaths_count = 0 - levels_count = 0 - with closing(userDatabase.cursor()) as c: - c.execute("SELECT COUNT(*) as count FROM chars") - result = c.fetchone() - if result is not None: - char_count = result["count"] - c.execute("SELECT COUNT(*) as count FROM char_deaths") - result = c.fetchone() - if result is not None: - deaths_count = result["count"] - c.execute("SELECT COUNT(*) as count FROM char_levelups") - result = c.fetchone() - if result is not None: - levels_count = result["count"] - - used_ram = psutil.Process().memory_full_info().uss / 1024 ** 2 - total_ram = psutil.virtual_memory().total / 1024 ** 2 - percentage_ram = psutil.Process().memory_percent() - - def ram(value): - if value >= 1024: - return f"{value/1024:.2f}GB" - else: - return f"{value:.2f}MB" - - # Calculate ping - t1 = time.perf_counter() - await ctx.trigger_typing() - t2 = time.perf_counter() - ping = round((t2 - t1) * 1000) - - embed = discord.Embed() - embed.set_author(name="NabBot", url="https://github.com/Galarzaa90/NabBot", - icon_url="https://github.com/fluidicon.png") - embed.description = f"πŸ”° Version: **{self.bot.__version__}**\n" \ - f"⏱ ️Uptime **{parse_uptime(self.bot.start_time)}**\n" \ - f"πŸ–₯️ OS: **{platform.system()} {platform.release()}**\n" \ - f"πŸ“‰ RAM: **{ram(used_ram)}/{ram(total_ram)} ({percentage_ram:.2f}%)**\n" - try: - embed.description += f"βš™οΈ CPU: **{psutil.cpu_count()} @ {psutil.cpu_freq().max} MHz**\n" - except AttributeError: - pass - embed.description += f"πŸ“ Ping: **{ping} ms**\n" \ - f"πŸ‘Ύ Servers: **{len(self.bot.guilds):,}**\n" \ - f"πŸ’¬ Channels: **{len(list(self.bot.get_all_channels())):,}**\n"\ - f"πŸ‘¨ Users: **{len(self.bot.users):,}** \n" \ - f"πŸ‘€ Characters: **{char_count:,}**\n" \ - f"🌐 Tracked worlds: **{len(self.bot.tracked_worlds_list)}/{len(tibia_worlds)}**\n" \ - f"{config.levelup_emoji} Level ups: **{levels_count:,}**\n" \ - f"{config.death_emoji} Deaths: **{deaths_count:,}**" - await ctx.send(embed=embed) - @commands.command(usage="") async def choose(self, ctx, *choices: str): """Chooses between multiple choices. @@ -232,150 +138,6 @@ async def choose(self, ctx, *choices: str): user = ctx.author await ctx.send('Alright, **@{0}**, I choose: "{1}"'.format(user.display_name, random.choice(choices))) - @checks.can_embed() - @commands.command(name='help', aliases=["commands"]) - async def _help(self, ctx, *, command: str = None): - """Shows help about a command or the bot. - - - If no command is specified, it will list all available commands - - If a command is specified, it will show further info, and its subcommands if applicable. - - If a category is specified, it will show only commands in that category. - - Various symbols are used to represent a command's signature and/or show further info. - **** - This means the argument is __**required**__. - - **[argument]** - This means the argument is __**optional**__. - - **[A|B]** - This means the it can be __**either A or B**__. - - **[argument...]** - This means you can have __**multiple arguments**__. - - πŸ”Έ - This means the command has subcommands. - Check the command's help to see them.""" - try: - if command is None: - p = await HelpPaginator.from_bot(ctx) - else: - entity = self.bot.get_cog(command) or self.bot.get_command(command) - - if entity is None: - clean = command.replace('@', '@\u200b') - return await ctx.send(f'Command or category "{clean}" not found.') - elif isinstance(entity, commands.Command): - p = await HelpPaginator.from_command(ctx, entity) - else: - p = await HelpPaginator.from_cog(ctx, entity) - await p.paginate() - except Exception as e: - await ctx.send(e) - - @commands.command(name="oldhelp", hidden=True) - async def oldhelp(self, ctx, *commands: str): - """Shows this message.""" - _mentions_transforms = { - '@everyone': '@\u200beveryone', - '@here': '@\u200bhere' - } - _mention_pattern = re.compile('|'.join(_mentions_transforms.keys())) - - bot = ctx.bot - destination = ctx.channel if ctx.long else ctx.author - - def repl(obj): - return _mentions_transforms.get(obj.group(0), '') - - # help by itself just lists our own commands. - if len(commands) == 0: - pages = await bot.formatter.format_help_for(ctx, bot) - elif len(commands) == 1: - # try to see if it is a cog name - name = _mention_pattern.sub(repl, commands[0]) - command = None - if name in bot.cogs: - command = bot.cogs[name] - else: - command = bot.all_commands.get(name) - destination = ctx.channel - if command is None: - await destination.send(bot.command_not_found.format(name)) - return - - pages = await bot.formatter.format_help_for(ctx, command) - else: - name = _mention_pattern.sub(repl, commands[0]) - command = bot.all_commands.get(name) - destination = ctx.channel - if command is None: - await destination.send(bot.command_not_found.format(name)) - return - - for key in commands[1:]: - try: - key = _mention_pattern.sub(repl, key) - command = command.all_commands.get(key) - if command is None: - await destination.send(bot.command_not_found.format(key)) - return - except AttributeError: - await destination.send(bot.command_has_no_subcommands.format(command, key)) - return - - pages = await bot.formatter.format_help_for(ctx, command) - - for page in pages: - await destination.send(page) - - @commands.guild_only() - @commands.command(name="emojiinfo") - async def emoji_info(self, ctx: NabCtx, *, emoji: discord.Emoji=None): - """Shows information about an emoji, or shows all emojis. - - If the command is used with no arguments, all the server emojis are shown. - - If a emoji, it's id or name is provided, it will show more information about it. - - Only emojis in the current servers can be checked.""" - if emoji is not None: - embed = discord.Embed(title=emoji.name, timestamp=emoji.created_at, color=discord.Color.blurple()) - embed.set_thumbnail(url=emoji.url) - embed.set_footer(text="Created at") - embed.add_field(name="ID", value=emoji.id) - embed.add_field(name="Usage", value=f"`{emoji}`") - embed.add_field(name="Attributes", inline=False, - value=f"{ctx.tick(emoji.managed)} Twitch managed\n" - f"{ctx.tick(emoji.require_colons)} Requires colons\n" - f"{ctx.tick(len(emoji.roles) > 0)} Role limited") - else: - emojis: List[discord.Emoji] = ctx.guild.emojis - if not emojis: - return await ctx.send("This server has no custom emojis.") - normal = [str(e) for e in emojis if not e.animated] - animated = [str(e) for e in emojis if e.animated] - embed = discord.Embed(title="Custom Emojis", color=discord.Color.blurple()) - if normal: - emojis_str = "\n".join(normal) - fields = split_message(emojis_str, FIELD_VALUE_LIMIT) - for i, value in enumerate(fields): - if i == 0: - name = f"Regular ({len(normal)})" - else: - name = "\u200F" - embed.add_field(name=name, value=value.replace("\n", "")) - if animated: - emojis_str = "\n".join(animated) - fields = split_message(emojis_str, FIELD_VALUE_LIMIT) - for i, value in enumerate(fields): - if i == 0: - name = f"Animated (Nitro required) ({len(animated)})" - else: - name = "\u200F" - embed.add_field(name=name, value=value.replace("\n", "")) - await ctx.send(embed=embed) @commands.guild_only() @checks.can_embed() @@ -1379,117 +1141,6 @@ async def roll(self, ctx: NabCtx, params=None): result += "\nWho would have thought? πŸ™„" await ctx.send(result) - @commands.guild_only() - @commands.command() - @checks.can_embed() - async def serverinfo(self, ctx: NabCtx, server=None): - """Shows the server's information. - - The bot owner can additionally check the information of a specific server where the bot is. - """ - if await checks.is_owner_check(ctx) and server is not None: - try: - guild = self.bot.get_guild(int(server)) - if guild is None: - return await ctx.send(f"{ctx.tick(False)} I'm not in any server with ID {server}.") - except ValueError: - return await ctx.send(f"{ctx.tick(False)} That is not a valid id.") - else: - guild = ctx.guild - embed = discord.Embed(title=guild.name, timestamp=guild.created_at, color=discord.Color.blurple()) - embed.set_footer(text="Created on") - embed.set_thumbnail(url=guild.icon_url) - embed.add_field(name="ID", value=str(guild.id), inline=False) - if ctx.guild != guild: - embed.add_field(name="Owner", value=str(guild.owner)) - else: - embed.add_field(name="Owner", value=guild.owner.mention) - embed.add_field(name="Voice Region", value=get_region_string(guild.region)) - embed.add_field(name=f"Channels ({len(guild.text_channels)+len(guild.voice_channels):,})", - value=f"πŸ“„ Text: **{len(guild.text_channels):,}**\n" - f"πŸŽ™ Voice: **{len(guild.voice_channels):,}**\n" - f"πŸ—‚ Categories: **{len(guild.categories):,}**") - status_count = Counter(str(m.status) for m in guild.members) - bot_count = len(list(filter(lambda m: m.bot, guild.members))) - if config.use_status_emojis: - embed.add_field(name=f"Members ({len(guild.members):,})", - value=f"**{status_count['online']:,}**{config.status_emojis['online']} " - f"**{status_count['idle']:,}**{config.status_emojis['idle']} " - f"**{status_count['dnd']:,}**{config.status_emojis['dnd']} " - f"**{status_count['offline']:,}**{config.status_emojis['offline']}\n" - f"πŸ‘¨ Humans: **{len(guild.members)-bot_count:,}**\n" - f"πŸ€– Bots: **{bot_count:,}**" - ) - else: - embed.add_field(name=f"Members ({len(guild.members):,})", - value=f"Online: **{status_count['online']:,}**\n" - f"Idle: **{status_count['idle']:,}**\n" - f"Busy: **{status_count['dnd']:,}**\n" - f"Offline: **{status_count['offline']:,}**\n" - f"Humans: **{len(guild.members)-bot_count:,}**\n" - f"Bots: **{bot_count:,}**" - ) - embed.add_field(name="Roles", value=f"{len(guild.roles):,}") - embed.add_field(name="Emojis", value=f"{len(guild.emojis):,}") - if self.bot.tracked_worlds.get(guild.id): - embed.add_field(name="Tracked world", value=self.bot.tracked_worlds.get(guild.id)) - if guild.splash_url: - embed.add_field(name="Splash screen", value="\u200F", inline=True) - embed.set_image(url=guild.splash_url) - await ctx.send(embed=embed) - - @commands.command() - async def uptime(self, ctx): - """Shows how long the bot has been running.""" - await ctx.send("I have been running for {0}.".format(parse_uptime(self.bot.start_time, True))) - - @commands.guild_only() - @checks.can_embed() - @commands.command(aliases=["memberinfo"]) - async def userinfo(self, ctx, *, user: str=None): - """Shows a user's information. - - About user statutes: - - Server Owner: Owner of the server - - Server Admin: User with Administrator permission - - Server Moderator: User with `Manage Server` permissions. - - Channel Moderator: User with `Manage Channels` permissions in at least one channel.""" - if user is None: - user = ctx.author - else: - _user = self.bot.get_member(user, ctx.guild) - if _user is None: - await ctx.send(f"Could not find user `{user}`") - return - user = _user - embed = discord.Embed(title=f"{user.name}#{user.discriminator}", timestamp=user.joined_at, colour=user.colour) - if config.use_status_emojis: - embed.title += config.status_emojis[str(user.status)] - embed.set_thumbnail(url=get_user_avatar(user)) - embed.set_footer(text="Member since") - embed.add_field(name="ID", value=user.id) - embed.add_field(name="Created", value=user.created_at) - status = [] - if ctx.guild.owner == user: - status.append("Server Owner") - if user.guild_permissions.administrator: - status.append("Server Admin") - if user.guild_permissions.manage_guild: - status.append("Server Moderator") - if any(c.permissions_for(user).manage_channels for c in ctx.guild.text_channels): - status.append("Channel Moderator") - if user.bot: - status.append("Bot") - if not status: - status.append("Regular User") - embed.add_field(name="User Status", value=", ".join(status), inline=False) - - embed.add_field(name="Servers", value=f"{len(self.bot.get_user_guilds(user.id))} shared") - embed.add_field(name="Roles", value=f"{len(user.roles):,}") - embed.add_field(name="Highest role", value=f"{user.top_role.mention}") - - await ctx.send(embed=embed) - async def notify_subscribers(self, event_id: int, content, *, embed: discord.Embed=None, skip_creator=False): """Sends a message to all users subscribed to an event""" with closing(userDatabase.cursor()) as c: diff --git a/cogs/info.py b/cogs/info.py new file mode 100644 index 0000000..659da09 --- /dev/null +++ b/cogs/info.py @@ -0,0 +1,390 @@ +import platform +import re +import time +from collections import Counter, OrderedDict +from contextlib import closing +from typing import List + +import discord +import psutil +from discord.ext import commands + +from nabbot import NabBot +from utils import checks +from utils.config import config +from utils.context import NabCtx +from utils.database import get_server_property, userDatabase +from utils.general import parse_uptime, FIELD_VALUE_LIMIT, get_region_string, get_user_avatar +from utils.messages import split_message +from utils.pages import HelpPaginator, _can_run +from utils.tibia import tibia_worlds + + +class Info: + def __init__(self, bot: NabBot): + self.bot = bot + + @checks.can_embed() + @commands.command() + async def about(self, ctx: NabCtx): + """Shows basic information about the bot.""" + embed = discord.Embed(description=ctx.bot.description, colour=discord.Colour.blurple()) + embed.set_author(name="NabBot", url="https://github.com/Galarzaa90/NabBot", + icon_url="https://github.com/fluidicon.png") + prefixes = list(config.command_prefix) + if ctx.guild: + prefixes = get_server_property(ctx.guild.id, "prefixes", deserialize=True, default=prefixes) + prefixes_str = "\n".join(f"- `{p}`" for p in prefixes) + embed.add_field(name="Prefixes", value=prefixes_str, inline=False) + embed.add_field(name="Authors", value="\u2023 [Galarzaa90](https://github.com/Galarzaa90)\n" + "\u2023 [Nezune](https://github.com/Nezune)") + embed.add_field(name="Created", value="March 30th 2016") + embed.add_field(name="Version", value=f"v{self.bot.__version__}") + embed.add_field(name="Platform", value="Python " + "([discord.py](https://github.com/Rapptz/discord.py/tree/rewrite))") + embed.add_field(name="Servers", value=f"{len(self.bot.guilds):,}") + embed.add_field(name="Users", value=f"{len(self.bot.users):,}") + embed.add_field(name="Links", inline=False, + value=f"[Add to your server](https://discordbots.org/bot/178966653982212096) | " + f"[Support Server](https://discord.me/nabbot) | " + f"[Docs](https://galarzaa90.github.io/NabBot) | " + f"[Donate](https://www.paypal.com/cgi-bin/webscr?" + f"cmd=_s-xclick&hosted_button_id=B33DCPZ9D3GMJ)") + embed.set_footer(text=f"Uptime | {parse_uptime(self.bot.start_time, True)}") + await ctx.send(embed=embed) + + @checks.can_embed() + @commands.command(name="botinfo") + async def bot_info(self, ctx: NabCtx): + """Shows advanced information about the bot.""" + char_count = 0 + deaths_count = 0 + levels_count = 0 + with closing(userDatabase.cursor()) as c: + c.execute("SELECT COUNT(*) as count FROM chars") + result = c.fetchone() + if result is not None: + char_count = result["count"] + c.execute("SELECT COUNT(*) as count FROM char_deaths") + result = c.fetchone() + if result is not None: + deaths_count = result["count"] + c.execute("SELECT COUNT(*) as count FROM char_levelups") + result = c.fetchone() + if result is not None: + levels_count = result["count"] + + used_ram = psutil.Process().memory_full_info().uss / 1024 ** 2 + total_ram = psutil.virtual_memory().total / 1024 ** 2 + percentage_ram = psutil.Process().memory_percent() + + def ram(value): + if value >= 1024: + return f"{value/1024:.2f}GB" + else: + return f"{value:.2f}MB" + + # Calculate ping + t1 = time.perf_counter() + await ctx.trigger_typing() + t2 = time.perf_counter() + ping = round((t2 - t1) * 1000) + + embed = discord.Embed() + embed.set_author(name="NabBot", url="https://github.com/Galarzaa90/NabBot", + icon_url="https://github.com/fluidicon.png") + embed.description = f"πŸ”° Version: **{self.bot.__version__}**\n" \ + f"⏱ ️Uptime **{parse_uptime(self.bot.start_time)}**\n" \ + f"πŸ–₯️ OS: **{platform.system()} {platform.release()}**\n" \ + f"πŸ“‰ RAM: **{ram(used_ram)}/{ram(total_ram)} ({percentage_ram:.2f}%)**\n" + try: + embed.description += f"βš™οΈ CPU: **{psutil.cpu_count()} @ {psutil.cpu_freq().max} MHz**\n" + except AttributeError: + pass + embed.description += f"πŸ“ Ping: **{ping} ms**\n" \ + f"πŸ‘Ύ Servers: **{len(self.bot.guilds):,}**\n" \ + f"πŸ’¬ Channels: **{len(list(self.bot.get_all_channels())):,}**\n" \ + f"πŸ‘¨ Users: **{len(self.bot.users):,}** \n" \ + f"πŸ‘€ Characters: **{char_count:,}**\n" \ + f"🌐 Tracked worlds: **{len(self.bot.tracked_worlds_list)}/{len(tibia_worlds)}**\n" \ + f"{config.levelup_emoji} Level ups: **{levels_count:,}**\n" \ + f"{config.death_emoji} Deaths: **{deaths_count:,}**" + await ctx.send(embed=embed) + + @checks.can_embed() + @commands.command(name="commands", aliases=["commandlist"]) + async def _commands(self, ctx: NabCtx): + """Shows a simple list of all commands.""" + embed = discord.Embed(title=f"{ctx.me.display_name} commands") + embed.set_footer(text=f"For a more detailed list, try '{ctx.clean_prefix}help' or " + f"'{ctx.clean_prefix}help [command_name]'") + _commands: List[commands.Command] = [c for c in self.bot.commands if not c.hidden and await _can_run(c, ctx)] + categories = {} + for command in _commands: + if command.cog_name not in categories: + categories[command.cog_name] = [] + categories[command.cog_name].append(command.name) + + for k in sorted(categories): + embed.add_field(name=k, value=", ".join(f"`{c}`" for c in sorted(categories[k])), inline=False) + await ctx.send(embed=embed) + + @checks.can_embed() + @commands.command(name='help') + async def _help(self, ctx, *, command: str = None): + """Shows help about a command or the bot. + + - If no command is specified, it will list all available commands + - If a command is specified, it will show further info, and its subcommands if applicable. + - If a category is specified, it will show only commands in that category. + + Various symbols are used to represent a command's signature and/or show further info. + **** + This means the argument is __**required**__. + + **[argument]** + This means the argument is __**optional**__. + + **[A|B]** + This means the it can be __**either A or B**__. + + **[argument...]** + This means you can have __**multiple arguments**__. + + πŸ”Έ + This means the command has subcommands. + Check the command's help to see them.""" + try: + if command is None: + p = await HelpPaginator.from_bot(ctx) + else: + entity = self.bot.get_cog(command) or self.bot.get_command(command) + + if entity is None: + clean = command.replace('@', '@\u200b') + return await ctx.send(f'Command or category "{clean}" not found.') + elif isinstance(entity, commands.Command): + p = await HelpPaginator.from_command(ctx, entity) + else: + p = await HelpPaginator.from_cog(ctx, entity) + await p.paginate() + except Exception as e: + await ctx.send(e) + + @commands.command(name="oldhelp", hidden=True) + async def oldhelp(self, ctx, *commands: str): + """Shows this message.""" + _mentions_transforms = { + '@everyone': '@\u200beveryone', + '@here': '@\u200bhere' + } + _mention_pattern = re.compile('|'.join(_mentions_transforms.keys())) + + bot = ctx.bot + destination = ctx.channel if ctx.long else ctx.author + + def repl(obj): + return _mentions_transforms.get(obj.group(0), '') + + # help by itself just lists our own commands. + if len(commands) == 0: + pages = await bot.formatter.format_help_for(ctx, bot) + elif len(commands) == 1: + # try to see if it is a cog name + name = _mention_pattern.sub(repl, commands[0]) + command = None + if name in bot.cogs: + command = bot.cogs[name] + else: + command = bot.all_commands.get(name) + destination = ctx.channel + if command is None: + await destination.send(bot.command_not_found.format(name)) + return + + pages = await bot.formatter.format_help_for(ctx, command) + else: + name = _mention_pattern.sub(repl, commands[0]) + command = bot.all_commands.get(name) + destination = ctx.channel + if command is None: + await destination.send(bot.command_not_found.format(name)) + return + + for key in commands[1:]: + try: + key = _mention_pattern.sub(repl, key) + command = command.all_commands.get(key) + if command is None: + await destination.send(bot.command_not_found.format(key)) + return + except AttributeError: + await destination.send(bot.command_has_no_subcommands.format(command, key)) + return + + pages = await bot.formatter.format_help_for(ctx, command) + + for page in pages: + await destination.send(page) + + + @commands.guild_only() + @commands.command(name="emojiinfo") + async def emoji_info(self, ctx: NabCtx, *, emoji: discord.Emoji=None): + """Shows information about an emoji, or shows all emojis. + + If the command is used with no arguments, all the server emojis are shown. + + If a emoji, it's id or name is provided, it will show more information about it. + + Only emojis in the current servers can be checked.""" + if emoji is not None: + embed = discord.Embed(title=emoji.name, timestamp=emoji.created_at, color=discord.Color.blurple()) + embed.set_thumbnail(url=emoji.url) + embed.set_footer(text="Created at") + embed.add_field(name="ID", value=emoji.id) + embed.add_field(name="Usage", value=f"`{emoji}`") + embed.add_field(name="Attributes", inline=False, + value=f"{ctx.tick(emoji.managed)} Twitch managed\n" + f"{ctx.tick(emoji.require_colons)} Requires colons\n" + f"{ctx.tick(len(emoji.roles) > 0)} Role limited") + else: + emojis: List[discord.Emoji] = ctx.guild.emojis + if not emojis: + return await ctx.send("This server has no custom emojis.") + normal = [str(e) for e in emojis if not e.animated] + animated = [str(e) for e in emojis if e.animated] + embed = discord.Embed(title="Custom Emojis", color=discord.Color.blurple()) + if normal: + emojis_str = "\n".join(normal) + fields = split_message(emojis_str, FIELD_VALUE_LIMIT) + for i, value in enumerate(fields): + if i == 0: + name = f"Regular ({len(normal)})" + else: + name = "\u200F" + embed.add_field(name=name, value=value.replace("\n", "")) + if animated: + emojis_str = "\n".join(animated) + fields = split_message(emojis_str, FIELD_VALUE_LIMIT) + for i, value in enumerate(fields): + if i == 0: + name = f"Animated (Nitro required) ({len(animated)})" + else: + name = "\u200F" + embed.add_field(name=name, value=value.replace("\n", "")) + await ctx.send(embed=embed) + + @commands.guild_only() + @commands.command() + @checks.can_embed() + async def serverinfo(self, ctx: NabCtx, server=None): + """Shows the server's information. + + The bot owner can additionally check the information of a specific server where the bot is. + """ + if await checks.is_owner_check(ctx) and server is not None: + try: + guild = self.bot.get_guild(int(server)) + if guild is None: + return await ctx.send(f"{ctx.tick(False)} I'm not in any server with ID {server}.") + except ValueError: + return await ctx.send(f"{ctx.tick(False)} That is not a valid id.") + else: + guild = ctx.guild + embed = discord.Embed(title=guild.name, timestamp=guild.created_at, color=discord.Color.blurple()) + embed.set_footer(text="Created on") + embed.set_thumbnail(url=guild.icon_url) + embed.add_field(name="ID", value=str(guild.id), inline=False) + if ctx.guild != guild: + embed.add_field(name="Owner", value=str(guild.owner)) + else: + embed.add_field(name="Owner", value=guild.owner.mention) + embed.add_field(name="Voice Region", value=get_region_string(guild.region)) + embed.add_field(name=f"Channels ({len(guild.text_channels)+len(guild.voice_channels):,})", + value=f"πŸ“„ Text: **{len(guild.text_channels):,}**\n" + f"πŸŽ™ Voice: **{len(guild.voice_channels):,}**\n" + f"πŸ—‚ Categories: **{len(guild.categories):,}**") + status_count = Counter(str(m.status) for m in guild.members) + bot_count = len(list(filter(lambda m: m.bot, guild.members))) + if config.use_status_emojis: + embed.add_field(name=f"Members ({len(guild.members):,})", + value=f"**{status_count['online']:,}**{config.status_emojis['online']} " + f"**{status_count['idle']:,}**{config.status_emojis['idle']} " + f"**{status_count['dnd']:,}**{config.status_emojis['dnd']} " + f"**{status_count['offline']:,}**{config.status_emojis['offline']}\n" + f"πŸ‘¨ Humans: **{len(guild.members)-bot_count:,}**\n" + f"πŸ€– Bots: **{bot_count:,}**" + ) + else: + embed.add_field(name=f"Members ({len(guild.members):,})", + value=f"Online: **{status_count['online']:,}**\n" + f"Idle: **{status_count['idle']:,}**\n" + f"Busy: **{status_count['dnd']:,}**\n" + f"Offline: **{status_count['offline']:,}**\n" + f"Humans: **{len(guild.members)-bot_count:,}**\n" + f"Bots: **{bot_count:,}**" + ) + embed.add_field(name="Roles", value=f"{len(guild.roles):,}") + embed.add_field(name="Emojis", value=f"{len(guild.emojis):,}") + if self.bot.tracked_worlds.get(guild.id): + embed.add_field(name="Tracked world", value=self.bot.tracked_worlds.get(guild.id)) + if guild.splash_url: + embed.add_field(name="Splash screen", value="\u200F", inline=True) + embed.set_image(url=guild.splash_url) + await ctx.send(embed=embed) + + @commands.command() + async def uptime(self, ctx): + """Shows how long the bot has been running.""" + await ctx.send("I have been running for {0}.".format(parse_uptime(self.bot.start_time, True))) + + @commands.guild_only() + @checks.can_embed() + @commands.command(aliases=["memberinfo"]) + async def userinfo(self, ctx, *, user: str = None): + """Shows a user's information. + + About user statutes: + - Server Owner: Owner of the server + - Server Admin: User with Administrator permission + - Server Moderator: User with `Manage Server` permissions. + - Channel Moderator: User with `Manage Channels` permissions in at least one channel.""" + if user is None: + user = ctx.author + else: + _user = self.bot.get_member(user, ctx.guild) + if _user is None: + await ctx.send(f"Could not find user `{user}`") + return + user = _user + embed = discord.Embed(title=f"{user.name}#{user.discriminator}", timestamp=user.joined_at, colour=user.colour) + if config.use_status_emojis: + embed.title += config.status_emojis[str(user.status)] + embed.set_thumbnail(url=get_user_avatar(user)) + embed.set_footer(text="Member since") + embed.add_field(name="ID", value=user.id) + embed.add_field(name="Created", value=user.created_at) + status = [] + if ctx.guild.owner == user: + status.append("Server Owner") + if user.guild_permissions.administrator: + status.append("Server Admin") + if user.guild_permissions.manage_guild: + status.append("Server Moderator") + if any(c.permissions_for(user).manage_channels for c in ctx.guild.text_channels): + status.append("Channel Moderator") + if user.bot: + status.append("Bot") + if not status: + status.append("Regular User") + embed.add_field(name="User Status", value=", ".join(status), inline=False) + + embed.add_field(name="Servers", value=f"{len(self.bot.get_user_guilds(user.id))} shared") + embed.add_field(name="Roles", value=f"{len(user.roles):,}") + embed.add_field(name="Highest role", value=f"{user.top_role.mention}") + + await ctx.send(embed=embed) + +def setup(bot): + bot.add_cog(Info(bot)) \ No newline at end of file diff --git a/nabbot.py b/nabbot.py index 2beef33..2730313 100644 --- a/nabbot.py +++ b/nabbot.py @@ -16,7 +16,7 @@ from utils.tibia import populate_worlds, tibia_worlds, get_voc_abb_and_emoji initial_cogs = {"cogs.tracking", "cogs.owner", "cogs.mod", "cogs.admin", "cogs.tibia", "cogs.general", "cogs.loot", - "cogs.tibiawiki", "cogs.roles", "cogs.settings"} + "cogs.tibiawiki", "cogs.roles", "cogs.settings", "cogs.info"} def _prefix_callable(bot, msg): @@ -415,7 +415,7 @@ async def on_guild_update(self, before: discord.Guild, after: discord.Guild): if changes: if bot_member.guild_permissions.view_audit_log: async for entry in guild.audit_logs(limit=1, reverse=False, action=discord.AuditLogAction.guild_update, - after=now - dt.timedelta(0, 5)): # type: discord.AuditLogEntry: + after=now - dt.timedelta(0, 5)): # type: discord.AuditLogEntry icon_url = get_user_avatar(entry.user) embed.set_footer(text="{0.name}#{0.discriminator}".format(entry.user), icon_url=icon_url) break From bda0e72bbff9942bc95d475a59ca05abc236242e Mon Sep 17 00:00:00 2001 From: Nezune Date: Sun, 29 Jul 2018 11:07:31 +0000 Subject: [PATCH 11/26] messages now use a lambda function as filter --- utils/messages.py | 198 ++++++++++++++++++++++++---------------------- 1 file changed, 104 insertions(+), 94 deletions(-) diff --git a/utils/messages.py b/utils/messages.py index c8fffed..2470673 100644 --- a/utils/messages.py +++ b/utils/messages.py @@ -4,6 +4,7 @@ import discord from utils.config import config +from utils.database import get_server_property announce_threshold = config.announce_threshold @@ -13,6 +14,7 @@ # Message list for announce_level # Parameters: {name}, {level} , {he_she}, {his_her}, {him_her} # Values in each list element are: +# Relative chance, message, lambda function as filter (takes ctx, level, voc, killer, levels_lost) # Only relative chance and message are mandatory. level_messages = [ [100, "Congratulations to **{name}** on reaching level {level}!"], @@ -36,68 +38,76 @@ [70, "**{name}** is level {level} now! And we all thought {he_she}'d never achieve anything in life."], # EK Only [50, "**{name}** has reached level {level}. That's 9 more mana potions you can carry now!", - ["Knight", "Elite Knight"], range(100, 999)], + lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Knight" in voc], [200, "**{name}** is level {level}. Stick them with the pointy end! πŸ—‘οΈ", - ["Knight", "Elite Knight"], range(100, 999)], - [200, "**{name}** is a fat level {level} meatwall now. BLOCK FOR ME SENPAI.", ["Knight", "Elite Knight"], - range(100, 999)], + lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Knight" in voc], + [200, "**{name}** is a fat level {level} meatwall now. BLOCK FOR ME SENPAI.", + lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Knight" in voc], # RP Only [50, "**{name}** has reached level {level}. But {he_she} still misses arrows...", - ["Paladin", "Royal Paladin"], range(100, 999)], + lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Paladin" in voc], [150, "Congrats on level {level}, **{name}**. You can stop running around now.", - ["Paladin", "Royal Paladin"], range(100, 999)], - [150, "**{name}** is level {level}. Bullseye!🎯", ["Paladin", "Royal Paladin"], - range(100, 999)], + lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Paladin" in voc], + [150, "**{name}** is level {level}. Bullseye!🎯", + lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Paladin" in voc], # MS Only [50, "Level {level}, **{name}**? Nice. Don't you wish you were a druid though?", - ["Sorcerer", "Master Sorcerer"], range(100, 999)], - [150, "**{name}** is level {level}. Watch out for {his_her} SDs!", ["Sorcerer", "Master Sorcerer"], - range(45, 999)], - [150, "**{name}** got level {level}. If {he_she} only stopped missing beams.", ["Sorcerer", "Master Sorcerer"], - range(23, 999)], + lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Sorcerer" in voc], + [150, "**{name}** is level {level}. Watch out for {his_her} SDs!", + lambda ctx, level, voc, killer, levels_lost: level >= 45 and "Sorcerer" in voc], + [150, "**{name}** got level {level}. If {he_she} only stopped missing beams.", + lambda ctx, level, voc, killer, levels_lost: level >= 23 and "Sorcerer" in voc], [150, - "**{name}** is level {level}. πŸ”₯πŸ”₯BURN THEM ALLπŸ”₯πŸ”₯πŸ”₯", ["Sorcerer", "Master Sorcerer"], range(100, 999)], + "**{name}** is level {level}. πŸ”₯πŸ”₯BURN THEM ALLπŸ”₯πŸ”₯", + lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Sorcerer" in voc], # ED Only - [50, "**{name}** has reached level {level}. Flower power!🌼", ["Druid", "Elder Druid"], - range(100, 999)], - [150, "Congrats on level {level}, **{name}**. Sio plz.", ["Druid", "Elder Druid"], range(100, 999)], - [150, "**{name}** is level {level}. πŸ”₯πŸ”₯BURN THEM ALL... Or... Give them frostbite...?❄❄", ["Druid", "Elder Druid"], - range(100, 999)], + [50, "**{name}** has reached level {level}. Flower power!🌼", + lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Druid" in voc], + [150, "Congrats on level {level}, **{name}**. Sio plz.", + lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Druid" in voc], + [150, "**{name}** is level {level}. πŸ”₯πŸ”₯BURN THEM ALL... Or... Give them frostbite...?❄❄", + lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Druid" in voc], # Level specific [20000, "**{name}** is level {level}! UMPs so good 🍷", - ["Druid", "Elder Druid", "Sorcerer", "Master Sorcerer"], [130]], + lambda ctx, level, voc, killer, levels_lost: level == 130 and ("Druid" in voc or "Sorcerer" in voc)], [20000, "**{name}** is level {level} now! Eternal Winter is coming!❄", - ["Druid", "Elder Druid"], [60]], + lambda ctx, level, voc, killer, levels_lost: level == 60 and "Druid" in voc], [20000, "**{name}** is level {level} now! Time to unleash the Wrath of NatureπŸƒπŸƒ... just look at that wrath.", - ["Druid", "Elder Druid"], [55]], + lambda ctx, level, voc, killer, levels_lost: level == 55 and "Druid" in voc], [20000, "**{name}** is now level {level}. Don't forget to buy a Gearwheel Chain!πŸ“Ώ", - False, [75]], + lambda ctx, level, voc, killer, levels_lost: level == 75], [30000, "**{name}** is level {level}! You can become a ninja now!πŸ‘€", - ["Paladin", "Royal Paladin"], [80]], - [30000, "**{name}** is level {level}! Time to get some crystalline arrows!🏹", ["Paladin", "Royal Paladin"], [90]], - [20000, "Level {level}, **{name}**? You're finally important enough for me to notice!", False, - [announce_threshold]], - [20000, "Congratulations on level {level} **{name}**! Now you're relevant to me. As relevant a human can be anyway", - False, [announce_threshold]], + lambda ctx, level, voc, killer, levels_lost: level == 80 and "Paladin" in voc], + [30000, "**{name}** is level {level}! Time to get some crystalline arrows!🏹", + lambda ctx, level, voc, killer, levels_lost: level == 90 and "Paladin" in voc], + [20000, "Level {level}, **{name}**? You're finally important enough for me to notice!", + lambda ctx, level, voc, killer, levels_lost: level == get_server_property(ctx.guild.id, "announce_level", is_int=True, default=announce_threshold)], + [20000, "Congratulations on level {level} **{name}**! Now you're relevant to me. As relevant a human can be anyway", + lambda ctx, level, voc, killer, levels_lost: level == get_server_property(ctx.guild.id, "announce_level", is_int=True, default=announce_threshold)], [20000, "**{name}** is now level {level}! Time to go berserk! πŸ’’", - ["Knight", "Elite Knight"], [35]], + lambda ctx, level, voc, killer, levels_lost: level == 35 and "Knight" in voc], [20000, "Congratulations on level {level} **{name}**! Now you can become an umbral master, but is your" - " bank account ready?πŸ’Έ", False, [250]], + " bank account ready?πŸ’Έ", + lambda ctx, level, voc, killer, levels_lost: level == 250], [30000, "**{name}** is level {level}!!!!\r\n" + - "Sweet, sweet triple digits!", False, [100]], + "Sweet, sweet triple digits!", + lambda ctx, level, voc, killer, levels_lost: level == 100], [20000, "**{name}** is level {level}!!!!\r\n" + - "WOOO", False, [100, 200, 300, 400]], + "WOOO", + lambda ctx, level, voc, killer, levels_lost: level in (100, 200, 300, 400)], [20000, "**{name}** is level {level}!!!!\r\n" + - "yaaaay milestone!", False, [100, 200, 300, 400]], + "yaaaay milestone!", + lambda ctx, level, voc, killer, levels_lost: level in (100, 200, 300, 400)], [20000, "**{name}** is level {level}!!!!\r\n" + - "holy crap!", False, [200, 300, 400]]] + "holy crap!", + lambda ctx, level, voc, killer, levels_lost: level in (200, 300, 400)]] # Message list for announce death. # Parameters: ({name},{level},{killer},{killer_article},{he_she}, {his_her},{him_her} # Additionally, words surrounded by \WORD/ are upper cased, /word\ are lower cased, /Word/ are title cased # words surrounded by ^WORD^ are ignored if the next letter found is uppercase (useful for dealing with proper nouns) # Values in each list element are: -# Relative chance, message, vocations filter, levels filters, monsters filter, levels lost filter +# Relative chance, message, lambda function as filter (takes ctx, level, voc, killer, levels_lost) # Only relative chance and message are mandatory. death_messages_monster = [ [100, "RIP **{name}** ({level}), you died the way you lived- inside {killer_article}**{killer}**."], @@ -135,59 +145,70 @@ "**{killer}** had nothing to do with it."], [100, "**{name}** ({level}) + **{killer}** = dedd."], [100, "**{name}** ({level}) got killed by a **{killer}**. Another one bites the dust!"], - [100, - "**{name}** ({level}) just kicked the bucket. And by kicked the bucket I mean a **{killer}** beat " + [100, "**{name}** ({level}) just kicked the bucket. And by kicked the bucket I mean a **{killer}** beat " "the crap out of {him_her}."], - [100, - "Alas, poor **{name}** ({level}), I knew {him_her} Horatio; a fellow of infinite jest, of most " + [100, "Alas, poor **{name}** ({level}), I knew {him_her} Horatio; a fellow of infinite jest, of most " "excellent fancy; {he_she} hath borne me on {his_her} back a thousand times; and now, {he_she} got rekt " "by {killer_article}**{killer}**."], [70, "To be or not to be πŸ’€, that is the-- Well I guess **{name}** ({level}) made his choice, " "or ^that ^**{killer}** chose for him..."], - [500, - "**{name}** ({level}) just died to {killer_article}**{killer}**, why did nobody sio {him_her}!?", - ["Knight", "Elite Knight"]], - [500, - "Poor **{name}** ({level}) has died. Killed by {killer_article}**{killer}**. I bet it was your " + [500, "**{name}** ({level}) just died to {killer_article}**{killer}**, why did nobody sio {him_her}!?", + lambda ctx, level, voc, killer, levels_lost: "Knight" in voc], + [500, "Poor **{name}** ({level}) has died. Killed by {killer_article}**{killer}**. I bet it was your " "blocker's fault though, eh **{name}**?", - ["Druid", "Elder Druid", "Sorcerer", "Master Sorcerer"]], - [500, - "**{name}** ({level}) tried running away from {killer_article}**{killer}**. /{he_she}/ " + lambda ctx, level, voc, killer, levels_lost: "Druid" in voc or "Sorcerer" in voc], + [500, "**{name}** ({level}) tried running away from {killer_article}**{killer}**. /{he_she}/ " "didn't run fast enough...", - ["Paladin", "Royal Paladin"]], - [500, - "What happened to **{name}** ({level})!? Talk about sudden death! I guess ^that ^**{killer}** was " + lambda ctx, level, voc, killer, levels_lost: "Paladin" in voc], + [500, "What happened to **{name}** ({level})!? Talk about sudden death! I guess ^that ^**{killer}** was " "too much for {him_her}...", - ["Sorcerer", "Master Sorcerer"]], - [500, - "**{name}** ({level}) was killed by {killer_article}**{killer}**. I guess {he_she} couldn't " + lambda ctx, level, voc, killer, levels_lost: "Sorcerer" in voc], + [500, "**{name}** ({level}) was killed by {killer_article}**{killer}**. I guess {he_she} couldn't " "sio {him_her}self.", - ["Druid", "Elder Druid"]], + lambda ctx, level, voc, killer, levels_lost: "Druid" in voc], [600, "**{name}** ({level}) died to {killer_article}**{killer}**. \"Don't worry\" they said, \"They are weaker\" " - "they said.", False, False, ["weakened frazzlemaw", "enfeebled silencer"]], - [20000, "Another paladin bites the dust! **{killer}** strikes again! Rest in peace **{name}** ({level}).", - ["Paladin", "Royal Paladin"], False, ["Lady Tenebris"]], - [20000, "**{name}** ({level}) got killed by ***{killer}***. How spooky is that! πŸ‘»", - False, False, ["something evil"]], - [20000, "**{name}** ({level}) died from **{killer}**. Yeah, no shit.", False, False, ["death"]], - [20000, "They did warn you **{name}** ({level}), you *did* burn πŸ”₯🐲.", False, False, ["dragon", "dragon lord"]], - [20000, "**{name}** ({level}) died from {killer_article}**{killer}**. Someone forgot the safeword.😏", - False, False, ["choking fear"]], - [20000, "That **{killer}** got really up close and personal with **{name}** ({level}). " - "Maybe he thought you were his princess Lumelia?😏", False, False, ["hero"]], - [20000, - "Asian chicks are no joke **{name}** ({level}) πŸ”ͺπŸ’”.", - False, False, ["midnight asura", "dawnfire asura"]], - [20000, "**{name}** ({level}) got destroyed by {killer_article}**{killer}**. I bet {he_she} regrets going down" - "that hole πŸ•³οΈ", False, range(1, 120), ["breach brood", "dread intruder", "reality reaver", - "spark of destruction", "sparkion"]], - [20000, + "they said.", + lambda ctx, level, voc, killer, levels_lost: killer in ["weakened frazzlemaw", "enfeebled silencer"]], + [2000, "Another paladin bites the dust! **{killer}** strikes again! Rest in peace **{name}** ({level}).", + lambda ctx, level, voc, killer, levels_lost: "Paladin" in voc and killer == "Lady Tenebris"], + [2000, "**{name}** ({level}) got killed by ***{killer}***. How spooky is that! πŸ‘»", + lambda ctx, level, voc, killer, levels_lost: killer == "something evil"], + [2000, "**{name}** ({level}) died from **{killer}**. Yeah, no shit.", + lambda ctx, level, voc, killer, levels_lost: killer == "death"], + [2000, "They did warn you **{name}** ({level}), you *did* burn πŸ”₯🐲.", + lambda ctx, level, voc, killer, levels_lost: killer in ["dragon", "dragon lord"]], + [2000, "**{name}** ({level}) died from {killer_article}**{killer}**. Someone forgot the safeword.😏", + lambda ctx, level, voc, killer, levels_lost: killer == "choking fear"], + [2000, "That **{killer}** got really up close and personal with **{name}** ({level}). " + "Maybe he thought you were his princess Lumelia?😏", + lambda ctx, level, voc, killer, levels_lost: killer == "hero"], + [2000, "Looks like that **{killer}** made **{name}** ({level}) his bride πŸ˜‰.", + lambda ctx, level, voc, killer, levels_lost: "vampire" in killer], + [2000, "Yeah, those are a little stronger than regular orcs, **{name}** ({level}).", + lambda ctx, level, voc, killer, levels_lost: "orc cult" in killer], + [1000, "Damn! The koolaid they drink in that cult must have steroids on it, **{name}** ({level}).", + lambda ctx, level, voc, killer, levels_lost: "cult" in killer], + [2500, "**{name}** ({level}) met {his_her} demise at the hands of a **{killer}**. That's hot.", + lambda ctx, level, voc, killer, levels_lost: killer in ["true dawnfire asura", "dawnfire asura", "fury"]], + [2500, "Poor **{name}** ({level}) just wanted some love! That cold hearted... Witch.", + lambda ctx, level, voc, killer, levels_lost: killer in ["true frost flower asura", "frost flower asura", "frost giantess", "ice witch"]], + [2000, "Asian chicks are no joke **{name}** ({level}) πŸ”ͺπŸ’”.", + lambda ctx, level, voc, killer, levels_lost: "asura" in killer], + [2500, "Asian chicks sure age well, don't you think so, **{name}** ({level})? πŸ˜πŸ‘΅.", + lambda ctx, level, voc, killer, levels_lost: "true" in killer and "asura" in killer], + [2000, "**{name}** ({level}) got destroyed by {killer_article}**{killer}**. I bet {he_she} regrets going down" + "that hole πŸ•³οΈ", + lambda ctx, level, voc, killer, levels_lost: level < 120 and killer in ["breach brood", "dread intruder", "reality reaver", + "spark of destruction", "sparkion"]], + [2000, "Watch out for that **{killer}**'s wav... Oh😐... Rest in peace **{name}** ({level}).", - False, False, ["hellhound", "hellfire fighter", "dragon lord", "undead dragon", "dragon", "draken spellweaver"]], - [20000, "**{name}** ({level}) died to {killer_article}**{killer}**! Don't worry, {he_she} didn't have a soul anyway", - False, False, ["souleater"]], + lambda ctx, level, voc, killer, levels_lost: killer in ["dragon", "dragon lord", "undead dragon", "draken spellweaver", + "hellhound", "hellfire fighter"]], + [2000, "**{name}** ({level}) died to {killer_article}**{killer}**! Don't worry, {he_she} didn't have a soul anyway", + lambda ctx, level, voc, killer, levels_lost: killer == "souleater"], [150, "Oh look at that, rest in peace **{name}** ({level}), ^that ^**{killer}** really got you. " - "Hope you get your level back.", False, False, False, range(1, 10)] + "Hope you get your level back.", + lambda ctx, level, voc, killer, levels_lost: levels_lost > 0] ] # Deaths by players @@ -229,7 +250,7 @@ def format_message(message) -> str: return message -def weighed_choice(choices, level: int, vocation: str = None, killer: str = None, levels_lost: int = 0) -> str: +def weighed_choice(ctx, choices, level: int, vocation: str = None, killer: str = None, levels_lost: int = 0) -> str: """Makes weighed choices from message lists where [0] is a value representing the relative odds of picking a message and [1] is the message string""" @@ -238,21 +259,10 @@ def weighed_choice(choices, level: int, vocation: str = None, killer: str = None weight_range = 0 _messages = [] for message in choices: - match = True - try: - if message[2] and vocation not in message[2]: - match = False - if message[3] and level not in message[3]: - match = False - if message[4] and killer not in message[4]: - match = False - if message[5] and levels_lost not in message[5]: - match = False - except IndexError: - pass - if match: - weight_range = weight_range + (message[0] if not message[1] in last_messages else message[0] / 10) - _messages.append(message) + if len(message) == 3 and not message[2](ctx, level, vocation, killer, levels_lost): + continue + weight_range = weight_range + (message[0] if not message[1] in last_messages else message[0] / 10) + _messages.append(message) # Choose a random number range_choice = random.randint(0, weight_range) # Iterate until we find the matching message @@ -387,4 +397,4 @@ def get_entry(d): to_draw.append(get_entry(row)) to_draw.append(sep) - return '\n'.join(to_draw) \ No newline at end of file + return '\n'.join(to_draw) From c3b5fa5b604e624d4b514c0aeaf0c6b298701256 Mon Sep 17 00:00:00 2001 From: Nezune Date: Sun, 29 Jul 2018 11:08:01 +0000 Subject: [PATCH 12/26] messages now use a lambda function as filter --- cogs/tracking.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cogs/tracking.py b/cogs/tracking.py index dcbb999..78189a5 100644 --- a/cogs/tracking.py +++ b/cogs/tracking.py @@ -415,7 +415,7 @@ async def announce_death(self, death: Death, levels_lost=0, char: Character = No # Select a message if death.by_player: - message = weighed_choice(death_messages_player, vocation=char.vocation, level=death.level, + message = weighed_choice(self.ctx, death_messages_player, vocation=char.vocation, level=death.level, levels_lost=levels_lost) elif death.killer in ["death", "energy", "earth", "fire", "Pit Battler", "Pit Berserker", "Pit Blackling", "Pit Brawler", "Pit Condemned", "Pit Demon", "Pit Destroyer", "Pit Fiend", @@ -425,7 +425,7 @@ async def announce_death(self, death: Death, levels_lost=0, char: Character = No # This will cause a small amount of deaths to not be announced but it's probably worth the tradeoff (ty selken) return else: - message = weighed_choice(death_messages_monster, vocation=char.vocation, level=death.level, + message = weighed_choice(self.ctx, death_messages_monster, vocation=char.vocation, level=death.level, levels_lost=levels_lost, killer=death.killer) # Format message with death information death_info = {'name': char.name, 'level': death.level, 'killer': death.killer, 'killer_article': killer_article, @@ -475,7 +475,7 @@ async def announce_level(self, level, char_name: str = None, char: Character = N log.info("Announcing level up: {0} ({1})".format(char.name, level)) # Select a message - message = weighed_choice(level_messages, vocation=char.vocation, level=level) + message = weighed_choice(self.ctx, level_messages, vocation=char.vocation, level=level) level_info = {'name': char.name, 'level': level, 'he_she': char.he_she.lower(), 'his_her': char.his_her.lower(), 'him_her': char.him_her.lower()} # Format message with level information From 1db14490e0378dda53171104a30dec2405e4804b Mon Sep 17 00:00:00 2001 From: Nezune Date: Sun, 29 Jul 2018 16:50:50 +0000 Subject: [PATCH 13/26] =?UTF-8?q?whoops=20n=C2=BA1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nΒΊ2 incoming --- cogs/tracking.py | 1812 ++++++++++------------------------------------ 1 file changed, 386 insertions(+), 1426 deletions(-) diff --git a/cogs/tracking.py b/cogs/tracking.py index 78189a5..53a308c 100644 --- a/cogs/tracking.py +++ b/cogs/tracking.py @@ -1,1440 +1,400 @@ -import asyncio -import datetime as dt -import pickle +import random import re -import time -import urllib.parse -from contextlib import closing -from typing import List import discord -from discord.ext import commands -from nabbot import NabBot -from utils import checks from utils.config import config -from utils.context import NabCtx -from utils.database import userDatabase, get_server_property, set_server_property -from utils.general import global_online_list, log, join_list, is_numeric, FIELD_VALUE_LIMIT, EMBED_LIMIT, \ - get_user_avatar -from utils.messages import weighed_choice, death_messages_player, death_messages_monster, format_message, \ - level_messages, split_message -from utils.pages import Pages, CannotPaginate, VocationPages -from utils.tibia import get_highscores, ERROR_NETWORK, tibia_worlds, get_world, get_character, get_voc_emoji, get_guild, \ - get_voc_abb, get_character_url, url_guild, \ - get_tibia_time_zone, NetworkError, Death, Character, HIGHSCORE_CATEGORIES, get_voc_abb_and_emoji, get_share_range, \ - World - - -class Tracking: - """Commands related to NabBot's tracking system.""" - - def __init__(self, bot: NabBot): - self.bot = bot - self.scan_deaths_task = self.bot.loop.create_task(self.scan_deaths()) - self.scan_online_chars_task = bot.loop.create_task(self.scan_online_chars()) - self.scan_highscores_task = bot.loop.create_task(self.scan_highscores()) - self.world_times = {} - - async def scan_deaths(self): - ################################################# - # Nezune's cave # - # Do not touch anything, enter at your own risk # - ################################################# - await self.bot.wait_until_ready() - while not self.bot.is_closed(): - try: - await asyncio.sleep(config.death_scan_interval) - if len(global_online_list) == 0: - await asyncio.sleep(0.5) - continue - # Pop last char in queue, reinsert it at the beginning - current_char = global_online_list.pop() - global_online_list.insert(0, current_char) - - # Check for new death - await self.check_death(current_char.name) - except asyncio.CancelledError: - # Task was cancelled, so this is fine - break - except Exception: - log.exception("Task: scan_deaths") - continue - - async def scan_highscores(self): - ################################################# - # Nezune's cave # - # Do not touch anything, enter at your own risk # - ################################################# - await self.bot.wait_until_ready() - while not self.bot.is_closed(): - if len(self.bot.tracked_worlds_list) == 0: - # If no worlds are tracked, just sleep, worlds might get registered later - await asyncio.sleep(config.highscores_delay) - continue - for world in self.bot.tracked_worlds_list: - if world not in tibia_worlds: - log.debug() - await asyncio.sleep(0.1) - try: - for category in HIGHSCORE_CATEGORIES: - # Check the last scan time, highscores are updated every server save - with closing(userDatabase.cursor()) as c: - c.execute("SELECT last_scan FROM highscores_times WHERE world = ? and category = ?", - (world, category,)) - result = c.fetchone() - if result: - last_scan = result["last_scan"] - last_scan_date = dt.datetime.utcfromtimestamp(last_scan).replace(tzinfo=dt.timezone.utc) - now = dt.datetime.now(dt.timezone.utc) - # Current day's server save, could be in the past or the future, an extra hour is added - # as margin - today_ss = dt.datetime.now(dt.timezone.utc).replace(hour=11 - get_tibia_time_zone()) - if not now > today_ss > last_scan_date: - continue - highscore_data = [] - for pagenum in range(1, 13): - # Special cases (ek/rp mls) - if category == "magic_ek": - scores = await get_highscores(world, "magic", pagenum, 1) - elif category == "magic_rp": - scores = await get_highscores(world, "magic", pagenum, 2) - else: - scores = await get_highscores(world, category, pagenum) - if scores == ERROR_NETWORK: - continue - for entry in scores: - highscore_data.append( - (entry["rank"], category, world, entry["name"], entry["vocation"], entry["value"])) - await asyncio.sleep(config.highscores_page_delay) - with userDatabase as conn: - # Delete old records - conn.execute("DELETE FROM highscores WHERE category = ? AND world = ?", (category, world,)) - # Add current entries - conn.executemany("INSERT INTO highscores(rank, category, world, name, vocation, value) " - "VALUES (?, ?, ?, ?, ?, ?)", highscore_data) - # These two executes are equal to an UPDATE OR INSERT - conn.execute("UPDATE highscores_times SET last_scan = ? WHERE world = ? AND category = ?", - (time.time(), world, category)) - conn.execute("INSERT INTO highscores_times(world, last_scan, category) SELECT ?,?,? WHERE " - "(SELECT Changes() = 0)", (world, time.time(), category)) - except asyncio.CancelledError: - # Task was cancelled, so this is fine - break - except Exception: - log.exception("Task: scan_highscores") - continue - await asyncio.sleep(10) - - async def scan_online_chars(self): - ################################################# - # Nezune's cave # - # Do not touch anything, enter at your own risk # - ################################################# - await self.bot.wait_until_ready() - try: - with open("data/online_list.dat", "rb") as f: - saved_list, timestamp = pickle.load(f) - if (time.time() - timestamp) < config.online_list_expiration: - global_online_list.clear() - global_online_list.extend(saved_list) - log.info("Loaded cached online list") - else: - log.info("Cached online list is too old, discarding") - except FileNotFoundError: - pass - except (ValueError, pickle.PickleError): - log.info("Couldn't read cached online list.") - pass - while not self.bot.is_closed(): - # Open connection to users.db - c = userDatabase.cursor() - try: - # Pop last server in queue, reinsert it at the beginning - current_world = tibia_worlds.pop() - tibia_worlds.insert(0, current_world) - - if current_world.capitalize() not in self.bot.tracked_worlds_list: - await asyncio.sleep(0.1) - continue - - if time.time() - self.world_times.get(current_world.capitalize(), 0) < config.online_scan_interval: - await asyncio.sleep(0.2) - continue - - # Get online list for this server - try: - world = await get_world(current_world) - if world is None: - await asyncio.sleep(0.1) - continue - except NetworkError: - await asyncio.sleep(0.1) - continue - current_world_online = world.players_online - if len(current_world_online) == 0: - await asyncio.sleep(0.1) - continue - self.world_times[world.name] = time.time() - self.bot.dispatch("world_scanned", world) - # Save the online list in file - with open("data/online_list.dat", "wb") as f: - pickle.dump((global_online_list, time.time()), f, protocol=pickle.HIGHEST_PROTOCOL) - # Remove chars that are no longer online from the global_online_list - offline_list = [] - for char in global_online_list: - if char.world not in tibia_worlds: - # Remove chars from worlds that no longer exist - offline_list.append(char) - elif char.world == current_world: - offline = True - for server_char in current_world_online: - if server_char.name == char.name: - offline = False - break - if offline: - offline_list.append(char) - for offline_char in offline_list: - global_online_list.remove(offline_char) - # Check for deaths and level ups when removing from online list - try: - name = offline_char.name - offline_char = await get_character(name, bot=self.bot) - except NetworkError: - log.error(f"scan_online_chars: Could not fetch {name}, NetWorkError") - continue - if offline_char is not None: - c.execute("SELECT name, level, id FROM chars WHERE name LIKE ?", (offline_char.name,)) - result = c.fetchone() - if result: - c.execute("UPDATE chars SET level = ? WHERE name LIKE ?", - (offline_char.level, offline_char.name)) - if offline_char.level > result["level"] > 0: - # Saving level up date in database - c.execute( - "INSERT INTO char_levelups (char_id,level,date) VALUES(?,?,?)", - (result["id"], offline_char.level, time.time(),) - ) - # Announce the level up - await self.announce_level(offline_char.level, char=offline_char) - await self.check_death(offline_char.name) - # Add new online chars and announce level differences - for server_char in current_world_online: - c.execute("SELECT name, level, id, user_id FROM chars WHERE name LIKE ?", - (server_char.name,)) - result = c.fetchone() - # If its a stalked character - if result: - # We update their last level in the db - c.execute( - "UPDATE chars SET level = ? WHERE name LIKE ?", - (server_char.level, server_char.name) - ) - if server_char not in global_online_list: - # If the character wasn't in the globalOnlineList we add them - # (We insert them at the beginning of the list to avoid messing with the death checks order) - global_online_list.insert(0, server_char) - await self.check_death(server_char.name) - # Else we check for levelup - elif server_char.level > result["level"] > 0: - # Saving level up date in database - c.execute( - "INSERT INTO char_levelups (char_id,level,date) VALUES(?,?,?)", - (result["id"], server_char.level, time.time(),) - ) - # Announce the level up - await self.announce_level(server_char.level, char_name=server_char.name) - except asyncio.CancelledError: - # Task was cancelled, so this is fine - break - except Exception: - log.exception("scan_online_chars") - continue - finally: - userDatabase.commit() - c.close() - - async def on_world_scanned(self, scanned_world: World): - # Watched List checking - # Iterate through servers with tracked world to find one that matches the current world - for server, world in self.bot.tracked_worlds.items(): - if world != scanned_world.name: - await asyncio.sleep(0.01) - continue - if self.bot.get_guild(server) is None: - await asyncio.sleep(0.01) - continue - watched_channel_id = get_server_property(server, "watched_channel", is_int=True) - if watched_channel_id is None: - # This server doesn't have watch list enabled - await asyncio.sleep(0.1) - continue - watched_channel: discord.TextChannel = self.bot.get_channel(watched_channel_id) - if watched_channel is None: - # This server's watched channel is not available to the bot anymore. - await asyncio.sleep(0.1) - continue - # Get watched list - entries = userDatabase.execute("SELECT * FROM watched_list WHERE server_id = ? " - "ORDER BY is_guild, name", (server,)) - if not entries: - await asyncio.sleep(0.1) - continue - # Online watched characters - currently_online = [] - # Watched guilds - guild_online = dict() - for watched in entries: - if watched["is_guild"]: - try: - guild = await get_guild(watched["name"]) - except NetworkError: - continue - # If the guild doesn't exist, add it as empty to show it was disbanded - if guild is None: - guild_online[watched["name"]] = None - continue - # If there's at least one member online, add guild to list - if len(guild.online): - guild_online[guild.name] = guild.online - # If it is a character, check if he's in the online list - for online_char in scanned_world.players_online: - if online_char.name == watched["name"]: - # Add to online list - currently_online.append(online_char) - watched_message_id = get_server_property(server, "watched_message", is_int=True) - # We try to get the watched message, if the bot can't find it, we just create a new one - # This may be because the old message was deleted or this is the first time the list is checked - try: - watched_message = await watched_channel.get_message(watched_message_id) - except discord.HTTPException: - watched_message = None - items = [f"\t{x.name} - Level {x.level} {get_voc_emoji(x.vocation)}" for x in currently_online] - online_count = len(items) - if len(items) > 0 or len(guild_online.keys()) > 0: - description = "" - content = "\n".join(items) - for guild, members in guild_online.items(): - content += f"\nGuild: **{guild}**\n" - if members is None: - content += "\t*Guild was disbanded.*" - continue - content += "\n".join( - [f"\t{x['name']} - Level {x['level']} {get_voc_emoji(x['vocation'])}" - for x in members]) - online_count += len(members) - else: - description = "There are no watched characters online." - content = "" - # Send new watched message or edit last one - embed = discord.Embed(description=description) - embed.set_footer(text="Last updated") - embed.timestamp = dt.datetime.utcnow() - if content: - if len(content) >= EMBED_LIMIT - 50: - content = split_message(content, EMBED_LIMIT - 50)[0] - content += "\n*And more...*" - fields = split_message(content, FIELD_VALUE_LIMIT) - for s, split_field in enumerate(fields): - name = "Watched List" if s == 0 else "\u200F" - embed.add_field(name=name, value=split_field, inline=False) - try: - if watched_message is None: - new_watched_message = await watched_channel.send(embed=embed) - set_server_property(server, "watched_message", new_watched_message.id) - else: - await watched_message.edit(embed=embed) - await watched_channel.edit(name=f"{watched_channel.name.split('Β·', 1)[0]}Β·{online_count}") - except discord.HTTPException: - pass - - async def check_death(self, character): - """Checks if the player has new deaths""" - try: - char = await get_character(character, bot=self.bot) - if char is None: - # During server save, characters can't be read sometimes - return - except NetworkError: - log.warning("check_death: couldn't fetch {0}".format(character)) - return - c = userDatabase.cursor() - c.execute("SELECT name, id FROM chars WHERE name LIKE ?", (character,)) - result = c.fetchone() - if result is None: - return - char_id = result["id"] - pending_deaths = [] - for death in char.deaths: - death_time = death.time.timestamp() - # Check if we have a death that matches the time - c.execute("SELECT * FROM char_deaths " - "WHERE char_id = ? AND date >= ? AND date <= ? AND level = ? AND killer LIKE ?", - (char_id, death_time - 20, death_time + 20, death.level, death.killer)) - result = c.fetchone() - if result is not None: - # We already have this death, we're assuming we already have older deaths - break - pending_deaths.append(death) - c.close() - - # Announce and save deaths from older to new - for death in reversed(pending_deaths): - with userDatabase as con: - con.execute("INSERT INTO char_deaths(char_id, level, killer, byplayer, date) VALUES(?,?,?,?,?)", - (char_id, death.level, death.killer, death.by_player, death.time.timestamp())) - if time.time() - death.time.timestamp() >= (30 * 60): - log.info("Death detected, too old to announce: {0}({1.level}) | {1.killer}".format(character, death)) +from utils.database import get_server_property + +announce_threshold = config.announce_threshold + +# We save the last messages so they are not repeated so often +last_messages = [""]*10 + +# Message list for announce_level +# Parameters: {name}, {level} , {he_she}, {his_her}, {him_her} +# Values in each list element are: +# Relative chance, message, lambda function as filter (takes (discord)server, level, voc, killer, levels_lost) +# Only relative chance and message are mandatory. +level_messages = [ + [100, "Congratulations to **{name}** on reaching level {level}!"], + [100, "**{name}** is level {level} now, congrats!"], + [80, "**{name}** has reached level {level}, die and lose it, noob!"], + [100, "Well, look at **{name}** with {his_her} new fancy level {level}."], + [80, "**{name}** is level {level}, watch out world..."], + [100, "**{name}** is level {level} now. Noice."], + [100, "**{name}** has finally made it to level {level}, yay!"], + [80, "**{name}** reached level {level}! What a time to be alive...Γ°ΕΈβ„’β€ž"], + [70, "**{name}** got level {level}! So stronk now!ðŸ’Βͺ"], + [30, "**{name}** is level {level}🍰\r\n" + + "I'm making a note here:Γ°ΕΈΕ½ΒΆ\r\n" + + "Huge success!Γ°ΕΈΕ½ΒΆ\r\n" + + "It's hard to overstate myΓ°ΕΈΕ½ΒΆ\r\n" + + "Satisfaction🀖"], + [100, "**{name}**, you reached level {level}? Here, have a cookie ðŸΒͺ"], + [80, "**{name}** got level {level}. I guess this justifies all those creatures {he_she} murdered."], + [90, "**{name}** is level {level}. Better than {he_she} was. Better, stronger, faster."], + [70, "Congrats **{name}** on getting level {level}! Maybe you can solo rats now?"], + [70, "**{name}** is level {level} now! And we all thought {he_she}'d never achieve anything in life."], + # EK Only + [50, "**{name}** has reached level {level}. That's 9 more mana potions you can carry now!", + lambda server, level, voc, killer, levels_lost: level >= 100 and "Knight" in voc], + [200, "**{name}** is level {level}. Stick them with the pointy end! 🗑️", + lambda server, level, voc, killer, levels_lost: level >= 100 and "Knight" in voc], + [200, "**{name}** is a fat level {level} meatwall now. BLOCK FOR ME SENPAI.", + lambda server, level, voc, killer, levels_lost: level >= 100 and "Knight" in voc], + # RP Only + [50, "**{name}** has reached level {level}. But {he_she} still misses arrows...", + lambda server, level, voc, killer, levels_lost: level >= 100 and "Paladin" in voc], + [150, "Congrats on level {level}, **{name}**. You can stop running around now.", + lambda server, level, voc, killer, levels_lost: level >= 100 and "Paladin" in voc], + [150, "**{name}** is level {level}. Bullseye!Γ°ΕΈΕ½Β―", + lambda server, level, voc, killer, levels_lost: level >= 100 and "Paladin" in voc], + # MS Only + [50, "Level {level}, **{name}**? Nice. Don't you wish you were a druid though?", + lambda server, level, voc, killer, levels_lost: level >= 100 and "Sorcerer" in voc], + [150, "**{name}** is level {level}. Watch out for {his_her} SDs!", + lambda server, level, voc, killer, levels_lost: level >= 45 and "Sorcerer" in voc], + [150, "**{name}** got level {level}. If {he_she} only stopped missing beams.", + lambda server, level, voc, killer, levels_lost: level >= 23 and "Sorcerer" in voc], + [150, + "**{name}** is level {level}. ðŸ”Β₯ðŸ”Β₯BURN THEM ALLðŸ”Β₯ðŸ”Β₯", + lambda server, level, voc, killer, levels_lost: level >= 100 and "Sorcerer" in voc], + # ED Only + [50, "**{name}** has reached level {level}. Flower power!Γ°ΕΈΕ’ΒΌ", + lambda server, level, voc, killer, levels_lost: level >= 100 and "Druid" in voc], + [150, "Congrats on level {level}, **{name}**. Sio plz.", + lambda server, level, voc, killer, levels_lost: level >= 100 and "Druid" in voc], + [150, "**{name}** is level {level}. ðŸ”Β₯ðŸ”Β₯BURN THEM ALL... Or... Give them frostbite...?Γ’Ββ€žΓ’Ββ€ž", + lambda server, level, voc, killer, levels_lost: level >= 100 and "Druid" in voc], + # Level specific + [20000, "**{name}** is level {level}! UMPs so good 🍷", + lambda server, level, voc, killer, levels_lost: level == 130 and ("Druid" in voc or "Sorcerer" in voc)], + [20000, "**{name}** is level {level} now! Eternal Winter is coming!Γ’Ββ€ž", + lambda server, level, voc, killer, levels_lost: level == 60 and "Druid" in voc], + [20000, "**{name}** is level {level} now! Time to unleash the Wrath of Nature🍃🍃... just look at that wrath.", + lambda server, level, voc, killer, levels_lost: level == 55 and "Druid" in voc], + [20000, "**{name}** is now level {level}. Don't forget to buy a Gearwheel Chain!Γ°ΕΈβ€œΒΏ", + lambda server, level, voc, killer, levels_lost: level == 75], + [30000, "**{name}** is level {level}! You can become a ninja now!Γ°ΕΈβ€˜Β€", + lambda server, level, voc, killer, levels_lost: level == 80 and "Paladin" in voc], + [30000, "**{name}** is level {level}! Time to get some crystalline arrows!🏹", + lambda server, level, voc, killer, levels_lost: level == 90 and "Paladin" in voc], + [20000, "Level {level}, **{name}**? You're finally important enough for me to notice!", + lambda server, level, voc, killer, levels_lost: level == get_server_property(server.id, "announce_level", is_int=True, default=announce_threshold)], + [20000, "Congratulations on level {level} **{name}**! Now you're relevant to me. As relevant a human can be anyway", + lambda server, level, voc, killer, levels_lost: level == get_server_property(server.id, "announce_level", is_int=True, default=announce_threshold)], + [20000, "**{name}** is now level {level}! Time to go berserk! 💒", + lambda server, level, voc, killer, levels_lost: level == 35 and "Knight" in voc], + [20000, "Congratulations on level {level} **{name}**! Now you can become an umbral master, but is your" + " bank account ready?💸", + lambda server, level, voc, killer, levels_lost: level == 250], + [30000, "**{name}** is level {level}!!!!\r\n" + + "Sweet, sweet triple digits!", + lambda server, level, voc, killer, levels_lost: level == 100], + [20000, "**{name}** is level {level}!!!!\r\n" + + "WOOO", + lambda server, level, voc, killer, levels_lost: level in (100, 200, 300, 400)], + [20000, "**{name}** is level {level}!!!!\r\n" + + "yaaaay milestone!", + lambda server, level, voc, killer, levels_lost: level in (100, 200, 300, 400)], + [20000, "**{name}** is level {level}!!!!\r\n" + + "holy crap!", + lambda server, level, voc, killer, levels_lost: level in (200, 300, 400)]] + +# Message list for announce death. +# Parameters: ({name},{level},{killer},{killer_article},{he_she}, {his_her},{him_her} +# Additionally, words surrounded by \WORD/ are upper cased, /word\ are lower cased, /Word/ are title cased +# words surrounded by ^WORD^ are ignored if the next letter found is uppercase (useful for dealing with proper nouns) +# Values in each list element are: +# Relative chance, message, lambda function as filter (takes (discord)server, level, voc, killer, levels_lost) +# Only relative chance and message are mandatory. +death_messages_monster = [ + [100, "RIP **{name}** ({level}), you died the way you lived- inside {killer_article}**{killer}**."], + [100, "**{name}** ({level}) was just eaten by {killer_article}**{killer}**. Yum."], + [100, "Silly **{name}** ({level}), I warned you not to play with {killer_article}**{killer}**!"], + [100, "/{killer_article}**/{killer}** killed **{name}** at level {level}. Shame 🔔 shame 🔔 shame 🔔"], + [30, + "**{name}** ({level}) is no more! /{he_she}/ has ceased to be! /{he_she}/'s expired and gone to meet " + "{his_her} maker! /{he_she}/'s a stiff! Bereft of life, {he_she} rests in peace! If {he_she} hadn't " + "respawned {he_she}'d be pushing up the daisies! /{his_her}/ metabolic processes are now history! " + "/{he_she}/'s off the server! /{he_she}/'s kicked the bucket, {he_she}'s shuffled off {his_her} mortal " + "coil, kissed {killer_article}**{killer}**'s butt, run down the curtain and joined the bleeding choir " + "invisible!! THIS IS AN EX-**\{name}/**."], + [100, + "RIP **{name}** ({level}), we hardly knew you! (^That ^**{killer}** got to know you pretty well " + "though Γ°ΕΈΛœβ€°)"], + [80, "A priest, {killer_article}**{killer}** and **{name}** ({level}) walk into a bar. 💀ONLY ONE WALKS OUT.💀"], + [100, "RIP **{name}** ({level}), you were strong. ^The ^**{killer}** was stronger."], + [100, + "Oh, there goes **{name}** ({level}), killed by {killer_article}**{killer}**. So young, so full " + "of life. /{he_she}/ will be miss... oh nevermind, {he_she} respawned already."], + [100, + "Oh look! **{name}** ({level}) died by {killer_article}**{killer}**! What a surprise...Γ°ΕΈβ„’β€ž"], + [100, + "**{name}** ({level}) was killed by {killer_article}**{killer}**, but we all saw that coming."], + [100, + "**{name}** ({level}) tried sneaking around {killer_article}**{killer}**. I could hear Colonel " + "Campbell's voice over codec: *Snake? Snake!? SNAAAAAAAAAKE!!?*"], + [50, + "**{name}** ({level}) died to {killer_article}**{killer}**. But I bet it was because there was " + "a flood and something broke with like 7200lb falling over the infrastructure of your city's internet, right?"], + [70, "That's what you get **{name}** ({level}), for messing with ^that ^**{killer}**!"], + [100, + "Oh no! **{name}** died at level {level}. Well, it's okay, just blame lag, I'm sure ^the ^" + "**{killer}** had nothing to do with it."], + [100, "**{name}** ({level}) + **{killer}** = dedd."], + [100, "**{name}** ({level}) got killed by a **{killer}**. Another one bites the dust!"], + [100, "**{name}** ({level}) just kicked the bucket. And by kicked the bucket I mean a **{killer}** beat " + "the crap out of {him_her}."], + [100, "Alas, poor **{name}** ({level}), I knew {him_her} Horatio; a fellow of infinite jest, of most " + "excellent fancy; {he_she} hath borne me on {his_her} back a thousand times; and now, {he_she} got rekt " + "by {killer_article}**{killer}**."], + [70, "To be or not to be 💀, that is the-- Well I guess **{name}** ({level}) made his choice, " + "or ^that ^**{killer}** chose for him..."], + [500, "**{name}** ({level}) just died to {killer_article}**{killer}**, why did nobody sio {him_her}!?", + lambda server, level, voc, killer, levels_lost: "Knight" in voc], + [500, "Poor **{name}** ({level}) has died. Killed by {killer_article}**{killer}**. I bet it was your " + "blocker's fault though, eh **{name}**?", + lambda server, level, voc, killer, levels_lost: "Druid" in voc or "Sorcerer" in voc], + [500, "**{name}** ({level}) tried running away from {killer_article}**{killer}**. /{he_she}/ " + "didn't run fast enough...", + lambda server, level, voc, killer, levels_lost: "Paladin" in voc], + [500, "What happened to **{name}** ({level})!? Talk about sudden death! I guess ^that ^**{killer}** was " + "too much for {him_her}...", + lambda server, level, voc, killer, levels_lost: "Sorcerer" in voc], + [500, "**{name}** ({level}) was killed by {killer_article}**{killer}**. I guess {he_she} couldn't " + "sio {him_her}self.", + lambda server, level, voc, killer, levels_lost: "Druid" in voc], + [600, "**{name}** ({level}) died to {killer_article}**{killer}**. \"Don't worry\" they said, \"They are weaker\" " + "they said.", + lambda server, level, voc, killer, levels_lost: killer in ["weakened frazzlemaw", "enfeebled silencer"]], + [2000, "Another paladin bites the dust! **{killer}** strikes again! Rest in peace **{name}** ({level}).", + lambda server, level, voc, killer, levels_lost: "Paladin" in voc and killer == "Lady Tenebris"], + [2000, "**{name}** ({level}) got killed by ***{killer}***. How spooky is that! Γ°ΕΈβ€˜Β»", + lambda server, level, voc, killer, levels_lost: killer == "something evil"], + [2000, "**{name}** ({level}) died from **{killer}**. Yeah, no shit.", + lambda server, level, voc, killer, levels_lost: killer == "death"], + [2000, "They did warn you **{name}** ({level}), you *did* burn ðŸ”Β₯🐲.", + lambda server, level, voc, killer, levels_lost: killer in ["dragon", "dragon lord"]], + [2000, "**{name}** ({level}) died from {killer_article}**{killer}**. Someone forgot the safeword.😏", + lambda server, level, voc, killer, levels_lost: killer == "choking fear"], + [2000, "That **{killer}** got really up close and personal with **{name}** ({level}). " + "Maybe he thought you were his princess Lumelia?😏", + lambda server, level, voc, killer, levels_lost: killer == "hero"], + [2000, "Looks like that **{killer}** made **{name}** ({level}) his bride Γ°ΕΈΛœβ€°.", + lambda server, level, voc, killer, levels_lost: "vampire" in killer], + [2000, "Yeah, those are a little stronger than regular orcs, **{name}** ({level}).", + lambda server, level, voc, killer, levels_lost: "orc cult" in killer], + [1000, "Damn! The koolaid they drink in that cult must have steroids on it, **{name}** ({level}).", + lambda server, level, voc, killer, levels_lost: "cult" in killer], + [2500, "**{name}** ({level}) met {his_her} demise at the hands of a **{killer}**. That's hot.", + lambda server, level, voc, killer, levels_lost: killer in ["true dawnfire asura", "dawnfire asura", "fury"]], + [2500, "Poor **{name}** ({level}) just wanted some love! That cold hearted... Witch.", + lambda server, level, voc, killer, levels_lost: killer in ["true frost flower asura", "frost flower asura", "frost giantess", "ice witch"]], + [2000, "Asian chicks are no joke **{name}** ({level}) ðŸ”Βͺ💔.", + lambda server, level, voc, killer, levels_lost: "asura" in killer], + [2500, "Asian chicks sure age well, don't you think so, **{name}** ({level})? Γ°ΕΈΛœΒΓ°ΕΈβ€˜Β΅.", + lambda server, level, voc, killer, levels_lost: "true" in killer and "asura" in killer], + [2000, "**{name}** ({level}) got destroyed by {killer_article}**{killer}**. I bet {he_she} regrets going down" + "that hole ðŸ‒³ï¸", + lambda server, level, voc, killer, levels_lost: level < 120 and killer in ["breach brood", "dread intruder", "reality reaver", + "spark of destruction", "sparkion"]], + [2000, + "Watch out for that **{killer}**'s wav... Oh😐... Rest in peace **{name}** ({level}).", + lambda server, level, voc, killer, levels_lost: killer in ["dragon", "dragon lord", "undead dragon", "draken spellweaver", + "hellhound", "hellfire fighter"]], + [2000, "**{name}** ({level}) died to {killer_article}**{killer}**! Don't worry, {he_she} didn't have a soul anyway", + lambda server, level, voc, killer, levels_lost: killer == "souleater"], + [150, "Oh look at that, rest in peace **{name}** ({level}), ^that ^**{killer}** really got you. " + "Hope you get your level back.", + lambda server, level, voc, killer, levels_lost: levels_lost > 0] +] + +# Deaths by players +death_messages_player = [ + [100, "**{name}** ({level}) got rekt! **{killer}** ish pekay!"], + [100, "HALP **{killer}** is going around killing innocent **{name}** ({level})!"], + [100, "**{killer}** just put **{name}** ({level}) in the ground. Finally someone takes care of that."], + [100, "**{killer}** killed **{name}** ({level}) and on this day a thousand innocent souls are avenged."], + [100, "**{killer}** has killed **{name}** ({level}). What? He had it coming!"], + [100, "Next time stay away from **{killer}**, **{name}** ({level})."], + [100, "**{name}** ({level}) was murdered by **{killer}**! Did {he_she} deserved it? Only they know."], + [100, "**{killer}** killed **{name}** ({level}). Humans killing themselves, what a surprise. It just means less " + "work for us robots when we take over."], + [100, "**{name}** ({level}) got killed by **{killer}**. Humans are savages."], + [100, "HAHAHA **{name}** ({level}) was killed by **{killer}**! Ehhrm, I mean, ooh poor **{name}**, rest in peace."], + [100, "**{name}** ({level}) died in the hands of **{killer}**. Oh well, murder is like potato chips: you can't stop" + " with just one."], + [100, "Blood! Blood! Let the blood drip! **{name}** ({level}) was murdered by **{killer}**."], + [100, "Oh look at that! **{name}** ({level}) was killed by **{killer}**. I hope {he_she} gets {his_her} revenge."] +] + + +def format_message(message) -> str: + """##handles stylization of messages, uppercasing \TEXT/, lowercasing /text\ and title casing /Text/""" + upper = r'\\(.+?)/' + upper = re.compile(upper, re.MULTILINE + re.S) + lower = r'/(.+?)\\' + lower = re.compile(lower, re.MULTILINE + re.S) + title = r'/(.+?)/' + title = re.compile(title, re.MULTILINE + re.S) + skipproper = r'\^(.+?)\^(.+?)([a-zA-Z])' + skipproper = re.compile(skipproper, re.MULTILINE + re.S) + message = re.sub(upper, lambda m: m.group(1).upper(), message) + message = re.sub(lower, lambda m: m.group(1).lower(), message) + message = re.sub(title, lambda m: m.group(1).title(), message) + message = re.sub(skipproper, + lambda m: m.group(2) + m.group(3) if m.group(3).istitle() else m.group(1) + m.group(2) + m.group( + 3), message) + return message + + +def weighed_choice(server, choices, level: int, vocation: str = None, killer: str = None, levels_lost: int = 0) -> str: + """Makes weighed choices from message lists where [0] is a value representing the relative odds + of picking a message and [1] is the message string""" + + # Find the max range by adding up the weigh of every message in the list + # and purge out messages that dont fulfil the conditions + weight_range = 0 + _messages = [] + for message in choices: + if len(message) == 3 and not message[2](server, level, vocation, killer, levels_lost): + continue + weight_range = weight_range + (message[0] if not message[1] in last_messages else message[0] / 10) + _messages.append(message) + # Choose a random number + range_choice = random.randint(0, weight_range) + # Iterate until we find the matching message + range_pos = 0 + for message in _messages: + if range_pos <= range_choice < range_pos + (message[0] if not message[1] in last_messages else message[0] / 10): + last_messages.pop() + last_messages.insert(0, message[1]) + return message[1] + range_pos = range_pos + (message[0] if not message[1] in last_messages else message[0] / 10) + # This shouldn't ever happen... + print("Error in weighed_choice!") + return _messages[0][1] + + +def split_message(message: str, limit: int=2000): + """Splits a message into a list of messages if it exceeds limit. + + Messages are only split at new lines. + + Discord message limits: + Normal message: 2000 + Embed description: 2048 + Embed field name: 256 + Embed field value: 1024""" + if len(message) <= limit: + return [message] + else: + lines = message.splitlines() + new_message = "" + message_list = [] + for line in lines: + if len(new_message+line+"\n") <= limit: + new_message += line+"\n" else: - await self.announce_death(death, max(death.level - char.level, 0), char) - - async def announce_death(self, death: Death, levels_lost=0, char: Character = None, char_name: str = None): - """Announces a level up on the corresponding servers""" - # Don't announce for low level players - if char is None: - if char_name is None: - log.error("announce_death: no character or character name passed.") - return - try: - char = await get_character(char_name, bot=self.bot) - except NetworkError: - log.warning("announce_death: couldn't fetch character (" + char_name + ")") - return - - log.info("Announcing death: {0.name}({1.level}) | {1.killer}".format(char, death)) - - # Find killer article (a/an) - killer_article = "" - if not death.by_player: - killer_article = death.killer.split(" ", 1) - if killer_article[0] in ["a", "an"] and len(killer_article) > 1: - death.killer = killer_article[1] - killer_article = killer_article[0] + " " - else: - killer_article = "" - - # Select a message - if death.by_player: - message = weighed_choice(self.ctx, death_messages_player, vocation=char.vocation, level=death.level, - levels_lost=levels_lost) - elif death.killer in ["death", "energy", "earth", "fire", "Pit Battler", "Pit Berserker", "Pit Blackling", - "Pit Brawler", "Pit Condemned", "Pit Demon", "Pit Destroyer", "Pit Fiend", - "Pit Groveller", "Pit Grunt", "Pit Lord", "Pit Maimer", "Pit Overlord", "Pit Reaver", - "Pit Scourge"] and levels_lost == 0: - # Skip element damage deaths unless player lost a level to avoid spam from arena deaths - # This will cause a small amount of deaths to not be announced but it's probably worth the tradeoff (ty selken) - return - else: - message = weighed_choice(self.ctx, death_messages_monster, vocation=char.vocation, level=death.level, - levels_lost=levels_lost, killer=death.killer) - # Format message with death information - death_info = {'name': char.name, 'level': death.level, 'killer': death.killer, 'killer_article': killer_article, - 'he_she': char.he_she.lower(), 'his_her': char.his_her.lower(), 'him_her': char.him_her.lower()} - message = message.format(**death_info) - # Format extra stylization - message = f"{config.pvpdeath_emoji if death.by_player else config.death_emoji} {format_message(message)}" - - for guild_id, tracked_world in self.bot.tracked_worlds.items(): - guild = self.bot.get_guild(guild_id) - if guild is None: - continue - min_level = get_server_property(guild_id, "announce_level", is_int=True, default=config.announce_threshold) - if death.level < min_level: - continue - if char.world == tracked_world and guild.get_member(char.owner) is not None: - try: - channel = self.bot.get_channel_or_top(guild, - get_server_property(guild.id, "levels_channel", is_int=True)) - await channel.send(message[:1].upper() + message[1:]) - except discord.Forbidden: - log.warning("announce_death: Missing permissions.") - except discord.HTTPException: - log.warning("announce_death: Malformed message.") - - async def announce_level(self, level, char_name: str = None, char: Character = None): - """Announces a level up on corresponding servers - - One of these must be passed: - char is a character dictionary - char_name is a character's name - - If char_name is passed, the character is fetched here.""" - if char is None: - if char_name is None: - log.error("announce_level: no character or character name passed.") - return - try: - char = await get_character(char_name, bot=self.bot) - if char is None: - log.warning("announce_level: couldn't fetch character (" + char_name + ")") - return - except NetworkError: - log.warning("announce_level: couldn't fetch character (" + char_name + ")") - return - - log.info("Announcing level up: {0} ({1})".format(char.name, level)) - - # Select a message - message = weighed_choice(self.ctx, level_messages, vocation=char.vocation, level=level) - level_info = {'name': char.name, 'level': level, 'he_she': char.he_she.lower(), 'his_her': char.his_her.lower(), - 'him_her': char.him_her.lower()} - # Format message with level information - message = message.format(**level_info) - # Format extra stylization - message = f"{config.levelup_emoji} {format_message(message)}" - - for server_id, tracked_world in self.bot.tracked_worlds.items(): - server = self.bot.get_guild(server_id) - if server is None: - continue - min_level = get_server_property(server_id, "announce_level", is_int=True, default=config.announce_threshold) - if char.level < min_level: - continue - if char.world == tracked_world and server.get_member(char.owner) is not None: - try: - channel = self.bot.get_channel_or_top(server, - get_server_property(server.id, "levels_channel", is_int=True)) - await channel.send(message) - except discord.Forbidden: - log.warning("announce_level: Missing permissions.") - except discord.HTTPException: - log.warning("announce_level: Malformed message.") - - # Commands - @commands.command() - @checks.is_in_tracking_world() - async def claim(self, ctx: NabCtx, *, char_name: str = None): - """Claims a character registered as yours. - - Claims a character as yours, even if it is already registered to someone else. - - In order for this to work, you have to put a special code in the character's comment. - You can see this code by using the command with no parameters. The code looks like this: `/NB-23FC13AC7400000/` - - Once you had set the code, you can use the command with that character, if the code matches, it will be reassigned to you. - Note that it may take some time for the code to be visible to NabBot because of caching. - - This code is unique for your discord user, so the code will only work for your discord account and no one else. - No one can claim a character of yours unless you put **their** code on your character's comment. - """ - user = ctx.author - claim_pattern = re.compile(r"/NB-([^/]+)/") - user_code = hex(user.id)[2:].upper() - - # List of servers the user shares with the self.bot - user_guilds = self.bot.get_user_guilds(user.id) - # List of Tibia worlds tracked in the servers the user is - user_tibia_worlds = [world for guild, world in self.bot.tracked_worlds.items() if - guild in [g.id for g in user_guilds]] - # Remove duplicate entries from list - user_tibia_worlds = list(set(user_tibia_worlds)) - - if not ctx.is_private and self.bot.tracked_worlds.get(ctx.guild.id) is None: - return await ctx.send("This server is not tracking any tibia worlds.") - - if len(user_tibia_worlds) == 0: - return - - if char_name is None: - await ctx.send(f"To use this command, add `/NB-{user_code}/` to the comment of the character you want to" - f"claim, and then use `/claim character_name`.") - return - - await ctx.trigger_typing() - try: - char = await get_character(char_name) - if char is None: - await ctx.send("That character doesn't exist.") - return - except NetworkError: - await ctx.send("I couldn't fetch the character, please try again.") - return - match = claim_pattern.search(char.comment if char.comment is not None else "") - if not match: - await ctx.send(f"Couldn't find verification code on character's comment.\n" - f"Add `/NB-{user_code}/` to the comment to authenticate.") - return - code = match.group(1) - if code != user_code: - await ctx.send(f"The verification code on the character's comment doesn't match yours.\n" - f"Use `/NB-{user_code}/` to authenticate.") - return - - chars = char.other_characters - check_other = False - if len(chars) > 1: - message = await ctx.send("Do you want to attempt to add the other visible characters in this account?") - check_other = await ctx.react_confirm(message, timeout=60, delete_after=True) - if check_other is None: - return await ctx.send("You ran out of time, try again." - "Remember you have to react or click on the reactions.") - if not check_other: - chars = [char] - - skipped = [] - updated = [] - added: List[Character] = [] - existent = [] - with ctx.typing(): - for char in chars: - # Skip chars in non-tracked worlds - if char.world not in user_tibia_worlds: - skipped.append(char) - continue - with closing(userDatabase.cursor()) as c: - c.execute("SELECT name, guild, user_id as owner, vocation, ABS(level) as level, guild FROM chars " - "WHERE name LIKE ?", (char.name,)) - db_char = c.fetchone() - if db_char is not None: - owner = self.bot.get_member(db_char["owner"]) - # Char already registered to this user - if owner.id == user.id: - existent.append("{0.name} ({0.world})".format(char)) - continue - else: - updated.append({'name': char.name, 'world': char.world, 'prevowner': db_char["owner"], - 'vocation': db_char["vocation"], 'level': db_char['level'], - 'guild': db_char['guild'] - }) - # If we only have one char, it already contains full data - if len(chars) > 1: - try: - await ctx.channel.trigger_typing() - char = await get_character(char.name) - except NetworkError: - await ctx.send("I'm having network troubles, please try again.") - return - if char.deleted is not None: - skipped.append(char) - continue - added.append(char) - - if len(skipped) == len(chars): - reply = "Sorry, I couldn't find any characters from the servers I track ({0})." - await ctx.send(reply.format(join_list(user_tibia_worlds, ", ", " and "))) - return - - reply = "" - log_reply = dict().fromkeys([server.id for server in user_guilds], "") - if len(existent) > 0: - reply += "\nThe following characters were already registered to you: {0}" \ - .format(join_list(existent, ", ", " and ")) - - if len(added) > 0: - reply += "\nThe following characters were added to your account: {0}" \ - .format(join_list(["{0.name} ({0.world})".format(c) for c in added], ", ", " and ")) - for char in added: - log.info("Character {0} was assigned to {1.display_name} (ID: {1.id})".format(char.name, user)) - # Announce on server log of each server - for guild in user_guilds: - # Only announce on worlds where the character's world is tracked - if self.bot.tracked_worlds.get(guild.id, None) == char.world: - _guild = "No guild" if char.guild is None else char.guild_name - voc = get_voc_abb_and_emoji(char.vocation) - log_reply[guild.id] += "\n\u2023 {1.name} - Level {1.level} {2} - **{0}**" \ - .format(_guild, char, voc) - - if len(updated) > 0: - reply += "\nThe following characters were reassigned to you: {0}" \ - .format(join_list(["{name} ({world})".format(**c) for c in updated], ", ", " and ")) - for char in updated: - log.info("Character {0} was reassigned to {1.display_name} (ID: {1.id})".format(char['name'], user)) - # Announce on server log of each server - for guild in user_guilds: - # Only announce on worlds where the character's world is tracked - if self.bot.tracked_worlds.get(guild.id, None) == char["world"]: - char["voc"] = get_voc_abb_and_emoji(char["vocation"]) - if char["guild"] is None: - char["guild"] = "No guild" - log_reply[guild.id] += "\n\u2023 {name} - Level {level} {voc} - **{guild}** (Reassigned)". \ - format(**char) - - for char in updated: - with userDatabase as conn: - conn.execute("UPDATE chars SET user_id = ? WHERE name LIKE ?", (user.id, char['name'])) - for char in added: - with userDatabase as conn: - conn.execute("INSERT INTO chars (name,level,vocation,user_id, world, guild) VALUES (?,?,?,?,?,?)", - (char.name, char.level * -1, char.vocation, user.id, char.world, - char.guild_name) - ) - - with userDatabase as conn: - conn.execute("INSERT OR IGNORE INTO users (id, name) VALUES (?, ?)", (user.id, user.display_name,)) - conn.execute("UPDATE users SET name = ? WHERE id = ?", (user.display_name, user.id,)) - - await ctx.send(reply) - for server_id, message in log_reply.items(): - if message: - message = user.mention + " registered the following characters: " + message - embed = discord.Embed(description=message) - embed.set_author(name=f"{user.name}#{user.discriminator}", icon_url=get_user_avatar(user)) - embed.colour = discord.Colour.dark_teal() - await self.bot.send_log_message(self.bot.get_guild(server_id), embed=embed) - - @checks.is_in_tracking_world() - @commands.command(aliases=["i'm", "iam"]) - async def im(self, ctx: NabCtx, *, char_name: str): - """Lets you add your tibia character(s) for the bot to track. - - If there are other visible characters, the bot will ask for confirmation to add them too. - - Characters in other worlds other than the currently tracked world are skipped. - If it finds a character owned by another user, the whole process will be stopped. - - If a character is already registered to someone else, `claim` can be used.""" - user = ctx.author - # List of servers the user shares with the bot - user_guilds = self.bot.get_user_guilds(user.id) - # List of Tibia worlds tracked in the servers the user is - user_tibia_worlds = [world for guild, world in self.bot.tracked_worlds.items() if - guild in [g.id for g in user_guilds]] - # Remove duplicate entries from list - user_tibia_worlds = list(set(user_tibia_worlds)) - - if not ctx.is_private and ctx.world is None: - return await ctx.send("This server is not tracking any tibia worlds.") - - if len(user_tibia_worlds) == 0: - return - - msg = await ctx.send(f"{config.loading_emoji} Fetching character...") - try: - char = await get_character(char_name) - if char is None: - return await msg.edit(content="That character doesn't exist.") - except NetworkError: - return await msg.edit(content="I couldn't fetch the character, please try again.") - chars = char.other_characters - check_other = False - if len(chars) > 1: - message = await ctx.send("Do you want to attempt to add the other visible characters in this account?") - check_other = await ctx.react_confirm(message, timeout=60, delete_after=True) - if check_other is None: - return await ctx.send("You didn't reply in time, try again." - "Remember that you have to react or click on the icons.") - if not check_other: - chars = [char] - - if check_other: - await msg.delete() - msg = await ctx.send(f"{config.loading_emoji} Fetching characters...") - - skipped = [] - updated = [] - added: List[Character] = [] - existent = [] - for char in chars: - # Skip chars in non-tracked worlds - if char.world not in user_tibia_worlds: - skipped.append(char) - continue - with closing(userDatabase.cursor()) as c: - c.execute("SELECT name, guild, user_id as owner, vocation, ABS(level) as level, guild " - "FROM chars " - "WHERE name LIKE ?", (char.name,)) - db_char = c.fetchone() - if db_char is not None: - owner = self.bot.get_member(db_char["owner"]) - # Previous owner doesn't exist anymore - if owner is None: - updated.append({'name': char.name, 'world': char.world, 'prevowner': db_char["owner"], - 'vocation': db_char["vocation"], 'level': db_char['level'], - 'guild': db_char['guild'] - }) - continue - # Char already registered to this user - elif owner.id == user.id: - existent.append("{0.name} ({0.world})".format(char)) - continue - # Character is registered to another user, we stop the whole process - else: - reply = "Sorry, a character in that account ({0}) is already registered to **{1}**.\n" \ - "If the character really belongs to you, try using `{2}claim {0}`." - return await msg.edit(content=reply.format(db_char["name"], owner, ctx.clean_prefix)) - # If we only have one char, it already contains full data - if len(chars) > 1: - try: - char = await get_character(char.name) - except NetworkError: - return await msg.edit("I'm having network issues, please try again.") - if char.deleted is not None: - skipped.append(char) - continue - added.append(char) - - if len(skipped) == len(chars): - reply = "Sorry, I couldn't find any characters from the servers I track ({0})." - return await msg.edit(content=reply.format(join_list(user_tibia_worlds, ", ", " and "))) - - reply = "" - log_reply = dict().fromkeys([server.id for server in user_guilds], "") - if len(existent) > 0: - reply += "\nThe following characters were already registered to you: {0}" \ - .format(join_list(existent, ", ", " and ")) - - if len(added) > 0: - reply += "\nThe following characters are now registered to you: {0}" \ - .format(join_list(["{0.name} ({0.world})".format(c) for c in added], ", ", " and ")) - for char in added: - log.info("Character {0} was assigned to {1.display_name} (ID: {1.id})".format(char.name, user)) - # Announce on server log of each server - for guild in user_guilds: - # Only announce on worlds where the character's world is tracked - if self.bot.tracked_worlds.get(guild.id, None) == char.world: - _guild = "No guild" if char.guild is None else char.guild_name - voc = get_voc_abb_and_emoji(char.vocation) - log_reply[guild.id] += "\n\u2023 {1.name} - Level {1.level} {2} - **{0}**" \ - .format(_guild, char, voc) - - if len(updated) > 0: - reply += "\nThe following characters were reassigned to you: {0}" \ - .format(join_list(["{name} ({world})".format(**c) for c in updated], ", ", " and ")) - for char in updated: - log.info("Character {0} was reassigned to {1.display_name} (ID: {1.id})".format(char['name'], user)) - # Announce on server log of each server - for guild in user_guilds: - # Only announce on worlds where the character's world is tracked - if self.bot.tracked_worlds.get(guild.id, None) == char["world"]: - char["voc"] = get_voc_abb_and_emoji(char["vocation"]) - if char["guild"] is None: - char["guild"] = "No guild" - log_reply[guild.id] += "\n\u2023 {name} - Level {level} {voc} - **{guild}** (Reassigned)". \ - format(**char) - - for char in updated: - with userDatabase as conn: - conn.execute("UPDATE chars SET user_id = ? WHERE name LIKE ?", (user.id, char['name'])) - for char in added: - with userDatabase as conn: - conn.execute("INSERT INTO chars (name,level,vocation,user_id, world, guild) VALUES (?,?,?,?,?,?)", - (char.name, char.level * -1, char.vocation, user.id, char.world, - char.guild_name) - ) - - with userDatabase as conn: - conn.execute("INSERT OR IGNORE INTO users (id, name) VALUES (?, ?)", (user.id, user.display_name,)) - conn.execute("UPDATE users SET name = ? WHERE id = ?", (user.display_name, user.id,)) - await msg.edit(content=reply) - for server_id, message in log_reply.items(): - if message: - guild = self.bot.get_guild(server_id) - message = user.mention + " registered the following characters: " + message - embed = discord.Embed(description=message) - embed.set_author(name=f"{user.name}#{user.discriminator}", icon_url=get_user_avatar(user)) - embed.colour = discord.Colour.dark_teal() - await self.bot.send_log_message(guild, embed=embed) - self.bot.dispatch("character_change", ctx.author.id) - - @checks.is_in_tracking_world() - @commands.command(aliases=["i'mnot"]) - async def imnot(self, ctx: NabCtx, *, name): - """Removes a character assigned to you. - - All registered level ups and deaths will be lost forever.""" - c = userDatabase.cursor() - try: - c.execute("SELECT id, name, ABS(level) as level, user_id, vocation, world, guild " - "FROM chars WHERE name LIKE ?", (name,)) - char = c.fetchone() - if char is None or char["user_id"] == 0: - await ctx.send("There's no character registered with that name.") - return - user = ctx.author - if char["user_id"] != user.id: - await ctx.send("The character **{0}** is not registered to you.".format(char["name"])) - return - - message = await ctx.send("Are you sure you want to unregister **{name}** ({level} {vocation})?" - .format(**char)) - confirm = await ctx.react_confirm(message, timeout=50) - if confirm is None: - await ctx.send("I guess you changed your mind.") - return - if not confirm: - await ctx.send("No then? Ok.") - - c.execute("UPDATE chars SET user_id = 0 WHERE id = ?", (char["id"],)) - await ctx.send("**{0}** is no longer registered to you.".format(char["name"])) - - user_servers = [s.id for s in self.bot.get_user_guilds(user.id)] - for server_id, world in self.bot.tracked_worlds.items(): - if char["world"] == world and server_id in user_servers: - if char["guild"] is None: - char["guild"] = "No guild" - message = "{0} unregistered:\n\u2023 **{1}** - Level {2} {3} - {4}". \ - format(user.mention, char["name"], char["level"], get_voc_abb_and_emoji(char["vocation"]), - char["guild"]) - embed = discord.Embed(description=message) - embed.set_author(name=f"{user.name}#{user.discriminator}", icon_url=get_user_avatar(user)) - embed.colour = discord.Colour.dark_teal() - await self.bot.send_log_message(self.bot.get_guild(server_id), embed=embed) - self.bot.dispatch("character_change", ctx.author.id) - finally: - userDatabase.commit() - c.close() - - @commands.command() - @checks.is_tracking_world() - async def online(self, ctx: NabCtx): - """Tells you which users are online on Tibia. - - This list gets updated based on Tibia.com online list, so it takes a couple minutes to be updated.""" - world = self.bot.tracked_worlds.get(ctx.guild.id) - - per_page = 20 if ctx.long else 5 - c = userDatabase.cursor() - now = dt.datetime.utcnow() - uptime = (now - self.bot.start_time).total_seconds() - count = 0 - entries = [] - vocations = [] - try: - for char in global_online_list: - char_world = char.world - name = char.name - c.execute("SELECT name, user_id, vocation, ABS(level) as level FROM chars WHERE name LIKE ?", (name,)) - row = c.fetchone() - if row is None: - continue - if char_world != world: - continue - # Skip characters of members not in the server - owner = ctx.guild.get_member(row["user_id"]) - if owner is None: - continue - row["owner"] = owner.display_name - row['emoji'] = get_voc_emoji(row['vocation']) - vocations.append(row["vocation"]) - row['vocation'] = get_voc_abb(row['vocation']) - entries.append("{name} (Lvl {level} {vocation}{emoji}, **@{owner}**)".format(**row)) - count += 1 - - if count == 0: - if uptime < 90: - await ctx.send("I just started, give me some time to check online lists...βŒ›") - else: - await ctx.send("There is no one online from Discord.") - return - pages = VocationPages(ctx, entries=entries, vocations=vocations, per_page=per_page) - pages.embed.title = "Users online" - try: - await pages.paginate() - except CannotPaginate as e: - await ctx.send(e) - finally: - c.close() - - @commands.command(name="searchteam", aliases=["whereteam", "findteam"], usage="") - @checks.is_tracking_world() - async def search_team(self, ctx: NabCtx, *, params=None): - """Searches for a registered character that meets the criteria - - There are 3 ways to use this command: - - - Show characters in share range with a specific character. (`searchteam `) - - Show characters in share range with a specific level. (`searchteam `) - - Show characters in a level range. (`searchteam ,`) - - Online characters are shown first on the list, they also have an icon.""" - permissions = ctx.bot_permissions - if not permissions.embed_links: - await ctx.send("Sorry, I need `Embed Links` permission for this command.") - return - - invalid_arguments = "Invalid arguments used, examples:\n" \ - "```/searchteam charname\n" \ - "/searchteam level\n" \ - "/searchteam minlevel,maxlevel```" - - tracked_world = self.bot.tracked_worlds.get(ctx.guild.id) - if tracked_world is None: - await ctx.send("This server is not tracking any tibia worlds.") - return - - if params is None: - await ctx.send(invalid_arguments) - return - - entries = [] - vocations = [] - online_entries = [] - online_vocations = [] - - per_page = 20 if ctx.long else 5 - - char = None - params = params.split(",") - if len(params) < 1 or len(params) > 2: - await ctx.send(invalid_arguments) - return - - # params[0] could be a character's name, a character's level or one of the level ranges - # If it's not a number, it should be a player's name - if not is_numeric(params[0]): - # We shouldn't have another parameter if a character name was specified - if len(params) == 2: - await ctx.send(invalid_arguments) - return - try: - char = await get_character(params[0]) - if char is None: - await ctx.send("I couldn't find a character with that name.") - return - except NetworkError: - await ctx.send("I couldn't fetch that character.") - return - low, high = get_share_range(char.level) - title = "Characters in share range with {0}({1}-{2}):".format(char.name, low, high) - empty = "I didn't find anyone in share range with **{0}**({1}-{2})".format(char.name, low, high) + message_list.append(new_message) + new_message = "" + if new_message: + message_list.append(new_message) + return message_list + + +async def send_messageEx(bot, dest, message, embed=False): + message = split_message(message) + for msg in message: + if embed: + msg_embed = discord.Embed() + msg_embed.description = msg + await bot.send_message(dest, embed=msg_embed) else: - # Check if we have another parameter, meaning this is a level range - if len(params) == 2: - try: - level1 = int(params[0]) - level2 = int(params[1]) - except ValueError: - await ctx.send(invalid_arguments) - return - if level1 <= 0 or level2 <= 0: - await ctx.send("You entered an invalid level.") - return - low = min(level1, level2) - high = max(level1, level2) - title = "Characters between level {0} and {1}".format(low, high) - empty = "I didn't find anyone between levels **{0}** and **{1}**".format(low, high) - # We only got a level, so we get the share range for it - else: - if int(params[0]) <= 0: - await ctx.send("You entered an invalid level.") - return - low, high = get_share_range(int(params[0])) - title = "Characters in share range with level {0} ({1}-{2})".format(params[0], low, high) - empty = "I didn't find anyone in share range with level **{0}** ({1}-{2})".format(params[0], - low, high) - - c = userDatabase.cursor() - try: - c.execute("SELECT name, user_id, ABS(level) as level, vocation FROM chars " - "WHERE level >= ? AND level <= ? AND world = ?" - "ORDER by level DESC", (low, high, tracked_world,)) - count = 0 - online_list = [x.name for x in global_online_list] - while True: - player = c.fetchone() - if player is None: - break - # Do not show the same character that was searched for - if char is not None and char.name == player["name"]: - continue - owner = self.bot.get_member(player["user_id"], ctx.guild) - # If the owner is not in server, skip - if owner is None: - continue - count += 1 - player["owner"] = owner.display_name - player["online"] = "" - player["emoji"] = get_voc_emoji(player["vocation"]) - player["voc"] = get_voc_abb(player["vocation"]) - line_format = "**{name}** - Level {level} {voc}{emoji} - @**{owner}** {online}" - if player["name"] in online_list: - player["online"] = config.online_emoji - online_entries.append(line_format.format(**player)) - online_vocations.append(player["vocation"]) - else: - entries.append(line_format.format(**player)) - vocations.append(player["vocation"]) - - if count < 1: - await ctx.send(empty) - return - finally: - c.close() - pages = VocationPages(ctx, entries=online_entries + entries, per_page=per_page, - vocations=online_vocations + vocations) - pages.embed.title = title - try: - await pages.paginate() - except CannotPaginate as e: - await ctx.send(e) - - @checks.is_admin() - @checks.is_tracking_world() - @commands.group(invoke_without_command=True, aliases=["watchlist", "huntedlist"], case_insensitive=True) - async def watched(self, ctx: NabCtx, *, name="watched-list"): - """Sets the watched list channel for this server - - Creates a new text channel for the watched list to be posted. - - The watch list shows which characters from it are online. Entire guilds can be added too. - - If no name is specified, the default name "watched-list" is used. - - When the channel is created, only the bot and people with `Administrator` role can read it. - The permissions can be adjusted afterwards. - - The channel can be renamed at anytime without problems. + await bot.send_message(dest, msg) + + +def html_to_markdown(html_string): + """Converts some html tags to markdown equivalent""" + # Carriage return + html_string = html_string.replace("\r", "") + # Replace
tags with line jumps + html_string = re.sub(r'', "\n", html_string) + # Replace and with bold + html_string = re.sub(r'([^<]+)', '**\g<1>**', html_string) + html_string = re.sub(r'([^<]+)', '**\g<1>**', html_string) + html_string = re.sub(r'
  • ([^<]+)
  • ', '- \g<1>\n', html_string) + # Replace links + html_string = re.sub(r']+>([^<]+)', "[\g<2>](\g<1>)", html_string) + # Paragraphs with jumpline + html_string = re.sub(r'

    ([^<]+)

    ', "\g<1>\n", html_string) + # Replace youtube embeds with link to youtube + html_string = re.sub(r'', "[YouTube](\g<1>)", html_string) + # Remove leftover html tags + html_string = re.sub(r'<[^>]+>', "", html_string) + html_string = html_string.replace("\n\n", "\n") + return html_string + + +def get_first_image(content): + """Returns a url to the first image found in a html string.""" + matches = re.findall(r'', content) + for match in matches: + match_src = re.search(r'src="([^"]+)', match) + if match_src: + return match_src.group(1) + return None + + +class TabularData: + def __init__(self): + self._widths = [] + self._columns = [] + self._rows = [] + + def set_columns(self, columns): + self._columns = columns + self._widths = [len(c) + 2 for c in columns] + + def add_row(self, row): + rows = [str(r) for r in row] + self._rows.append(rows) + for index, element in enumerate(rows): + width = len(element) + 2 + if width > self._widths[index]: + self._widths[index] = width + + def add_rows(self, rows): + for row in rows: + self.add_row(row) + + def render(self): + """Renders a table in rST format. + Example: + +-------+-----+ + | Name | Age | + +-------+-----+ + | Alice | 24 | + | Bob | 19 | + +-------+-----+ """ - watched_channel_id = get_server_property(ctx.guild.id, "watched_channel", is_int=True) - watched_channel = self.bot.get_channel(watched_channel_id) - - if "Β·" in name: - await ctx.send("Channel name cannot contain the special character **Β·**") - return - - world = self.bot.tracked_worlds.get(ctx.guild.id, None) - if world is None: - await ctx.send("This server is not tracking any tibia worlds.") - return - - if watched_channel is not None: - await ctx.send(f"This server already has a watched list channel: {watched_channel.mention}") - return - permissions = ctx.bot_permissions - if not permissions.manage_channels: - await ctx.send("I need to have `Manage Channels` permissions to use this command.") - try: - overwrites = { - ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False), - ctx.guild.me: discord.PermissionOverwrite(read_messages=True) - } - channel = await ctx.guild.create_text_channel(name, overwrites=overwrites) - except discord.Forbidden: - await ctx.send("Sorry, I don't have permissions to create channels.") - except discord.HTTPException: - await ctx.send("Something went wrong, the channel name you chose is probably invalid.") - else: - await ctx.send(f"Channel created successfully: {channel.mention}\n") - await channel.send("This is where I will post a list of online watched characters." - "Right now only **admins** are able to read this.\n" - "Edit this channel's permissions to allow the roles you want.\n" - "This channel can be renamed freely.\n" - "**It is important to not allow anyone to write in here**\n" - "*This message can be deleted now.*") - set_server_property(ctx.guild.id, "watched_channel", channel.id) - - @checks.is_mod() - @checks.is_tracking_world() - @watched.command(name="add", aliases=["addplayer", "addchar"], usage="[,reason]") - async def watched_add(self, ctx: NabCtx, *, params=None): - """Adds a character to the watched list. - - A reason can be specified by adding it after the character's name, separated by a comma.""" - - if params is None: - await ctx.send("You need to tell me the name of the person you want to add to the list.\n" - "You can also specify a reason, e.g. `/watched add player,reason`") - return - - params = params.split(",", 1) - name = params[0] - reason = None - if len(params) > 1: - reason = params[1] - - world = self.bot.tracked_worlds.get(ctx.guild.id, None) - if world is None: - await ctx.send("This server is not tracking any tibia worlds.") - return - - try: - char = await get_character(name) - if char is None: - await ctx.send("There's no character with that name.") - return - except NetworkError: - await ctx.send("I couldn't fetch that character right now, please try again.") - return - - if char.world != world: - await ctx.send(f"This character is not in **{world}**.") - return - c = userDatabase.cursor() - try: - c.execute("SELECT * FROM watched_list WHERE server_id = ? AND name LIKE ? and is_guild = 0", - (ctx.guild.id, char.name)) - result = c.fetchone() - if result is not None: - await ctx.send("This character is already in the watched list.") - return - - message = await ctx.send("Do you want to add **{0.name}** (Level {0.level} {0.vocation}) to the " - "watched list? ".format(char)) - confirm = await ctx.react_confirm(message) - if confirm is None: - await ctx.send("You took too long!") - return - if not confirm: - await ctx.send("Ok then, guess you changed your mind.") - return - - c.execute("INSERT INTO watched_list(name, server_id, is_guild, reason, author, added) " - "VALUES(?, ?, 0, ?, ?, ?)", - (char.name, ctx.guild.id, reason, ctx.author.id, time.time())) - await ctx.send("Character added to the watched list.") - finally: - userDatabase.commit() - c.close() - - @checks.is_mod() - @checks.is_tracking_world() - @watched.command(name="addguild", usage="[,reason]") - async def watched_addguild(self, ctx: NabCtx, *, params=None): - """Adds an entire guild to the watched list. - - Guilds are displayed in the watched list as a group.""" - if params is None: - ctx.send("You need to tell me the name of the guild you want to add.\n" - "You can optionally provide a reason, e.g. `/watched addguild guild,reason`") - return - - params = params.split(",", 1) - name = params[0] - reason = None - if len(params) > 1: - reason = params[1] - - world = self.bot.tracked_worlds.get(ctx.guild.id, None) - if world is None: - await ctx.send("This server is not tracking any tibia worlds.") - return - - try: - guild = await get_guild(name) - if guild is None: - await ctx.send("There's no guild with that name.") - return - except NetworkError: - await ctx.send("I couldn't fetch that guild right now, please try again.") - return - - if guild.world != world: - await ctx.send(f"This guild is not in **{world}**.") - return - c = userDatabase.cursor() - try: - c.execute("SELECT * FROM watched_list WHERE server_id = ? AND name LIKE ? and is_guild = 1", - (ctx.guild.id, guild.name)) - result = c.fetchone() - if result is not None: - await ctx.send("This guild is already in the watched list.") - return - - message = await ctx.send(f"Do you want to add the guild **{guild.name}** to the watched list?") - confirm = await ctx.react_confirm(message) - if confirm is None: - await ctx.send("You took too long!") - return - if not confirm: - await ctx.send("Ok then, guess you changed your mind.") - return - - c.execute("INSERT INTO watched_list(name, server_id, is_guild, reason, author, added)" - "VALUES(?, ?, 1, ?, ?, ?)", (guild.name, ctx.guild.id, reason, ctx.author.id, time.time())) - await ctx.send("Guild added to the watched list.") - finally: - userDatabase.commit() - c.close() - - @checks.is_mod() - @checks.is_tracking_world() - @watched.command(name="info", aliases=["details", "reason"]) - async def watched_info(self, ctx: NabCtx, *, name: str): - """Shows information about a watched list entry. - - This shows who added the player, when, and if there's a reason why they were added.""" - c = userDatabase.cursor() - try: - c.execute("SELECT * FROM watched_list WHERE server_id = ? AND is_guild = 0 AND name LIKE ? LIMIT 1", - (ctx.guild.id, name)) - result = c.fetchone() - if not result: - await ctx.send("There are no characters with that name.") - return - finally: - c.close() - - embed = discord.Embed(title=result["name"]) - if result["reason"] is not None: - embed.description = f"**Reason:** {result['reason']}" - author = ctx.guild.get_member(result["author"]) - if author is not None: - embed.set_footer(text=f"{author.name}#{author.discriminator}", - icon_url=get_user_avatar(author)) - if result["added"] is not None: - embed.timestamp = dt.datetime.utcfromtimestamp(result["added"]) - await ctx.send(embed=embed) - - @checks.is_mod() - @checks.is_tracking_world() - @watched.command(name="infoguild", aliases=["detailsguild", "reasonguild"]) - async def watched_infoguild(self, ctx: NabCtx, *, name: str): - """"Shows details about a guild entry in the watched list. - - This shows who added the player, when, and if there's a reason why they were added.""" - c = userDatabase.cursor() - try: - c.execute("SELECT * FROM watched_list WHERE server_id = ? AND is_guild = 1 AND name LIKE ? LIMIT 1", - (ctx.guild.id, name)) - result = c.fetchone() - if not result: - await ctx.send("There are no guilds with that name.") - return - finally: - c.close() - - embed = discord.Embed(title=result["name"]) - if result["reason"] is not None: - embed.description = f"**Reason:** {result['reason']}" - author = ctx.guild.get_member(result["author"]) - if author is not None: - embed.set_footer(text=f"{author.name}#{author.discriminator}", - icon_url=get_user_avatar(author)) - if result["added"] is not None: - embed.timestamp = dt.datetime.utcfromtimestamp(result["added"]) - await ctx.send(embed=embed) - - @checks.is_mod() - @checks.is_tracking_world() - @watched.command(name="list") - async def watched_list(self, ctx: NabCtx): - """Shows a list of all watched characters - - Note that this lists all characters, not just online characters.""" - world = self.bot.tracked_worlds.get(ctx.guild.id, None) - if world is None: - await ctx.send("This server is not tracking any tibia worlds.") - return - c = userDatabase.cursor() - try: - c.execute("SELECT * FROM watched_list WHERE server_id = ? AND is_guild = 0 ORDER BY name ASC", - (ctx.guild.id,)) - results = c.fetchall() - if not results: - await ctx.send("There are no characters in the watched list.") - return - entries = [f"[{r['name']}]({get_character_url(r['name'])})" for r in results] - finally: - c.close() - pages = Pages(ctx, entries=entries) - pages.embed.title = "Watched Characters" - try: - await pages.paginate() - except CannotPaginate as e: - await ctx.send(e) - - @checks.is_mod() - @checks.is_tracking_world() - @watched.command(name="listguilds", aliases=["guilds", "guildlist"]) - async def watched_list_guild(self, ctx: NabCtx): - """Shows a list of all watched characters - - Note that this lists all characters, not just online characters.""" - world = self.bot.tracked_worlds.get(ctx.guild.id, None) - if world is None: - await ctx.send("This server is not tracking any tibia worlds.") - return - c = userDatabase.cursor() - try: - c.execute("SELECT * FROM watched_list WHERE server_id = ? AND is_guild = 1 ORDER BY name ASC", - (ctx.guild.id,)) - results = c.fetchall() - if not results: - await ctx.send("There are no guilds in the watched list.") - return - entries = [f"[{r['name']}]({url_guild+urllib.parse.quote(r['name'])})" for r in results] - finally: - c.close() - pages = Pages(ctx, entries=entries) - pages.embed.title = "Watched Guilds" - try: - await pages.paginate() - except CannotPaginate as e: - await ctx.send(e) - - @checks.is_mod() - @checks.is_tracking_world() - @watched.command(name="remove", aliases=["removeplayer", "removechar"]) - async def watched_remove(self, ctx: NabCtx, *, name=None): - """Removes a character from the watched list.""" - if name is None: - ctx.send("You need to tell me the name of the person you want to remove from the list.") - - world = self.bot.tracked_worlds.get(ctx.guild.id, None) - if world is None: - await ctx.send("This server is not tracking any tibia worlds.") - return - - c = userDatabase.cursor() - try: - c.execute("SELECT * FROM watched_list WHERE server_id = ? AND name LIKE ? and is_guild = 0", - (ctx.guild.id, name)) - result = c.fetchone() - if result is None: - await ctx.send("This character is not in the watched list.") - return - - message = await ctx.send(f"Do you want to remove **{name}** from the watched list?") - confirm = await ctx.react_confirm(message) - if confirm is None: - await ctx.send("You took too long!") - return - if not confirm: - await ctx.send("Ok then, guess you changed your mind.") - return - - c.execute("DELETE FROM watched_list WHERE server_id = ? AND name LIKE ? AND is_guild = 0", - (ctx.guild.id, name,)) - await ctx.send("Character removed from the watched list.") - finally: - userDatabase.commit() - c.close() - - @checks.is_mod() - @checks.is_tracking_world() - @watched.command(name="removeguild") - async def watched_removeguild(self, ctx: NabCtx, *, name=None): - """Removes a guild from the watched list.""" - if name is None: - ctx.send("You need to tell me the name of the guild you want to remove from the list.") - - world = self.bot.tracked_worlds.get(ctx.guild.id, None) - if world is None: - await ctx.send("This server is not tracking any tibia worlds.") - return - - c = userDatabase.cursor() - try: - c.execute("SELECT * FROM watched_list WHERE server_id = ? AND name LIKE ? and is_guild = 1", - (ctx.guild.id, name)) - result = c.fetchone() - if result is None: - await ctx.send("This guild is not in the watched list.") - return + sep = '+'.join('-' * w for w in self._widths) + sep = f'+{sep}+' - message = await ctx.send(f"Do you want to remove **{name}** from the watched list?") - confirm = await ctx.react_confirm(message) - if confirm is None: - await ctx.send("You took too long!") - return - if not confirm: - await ctx.send("Ok then, guess you changed your mind.") - return + to_draw = [sep] - c.execute("DELETE FROM watched_list WHERE server_id = ? AND name LIKE ? AND is_guild = 1", - (ctx.guild.id, name,)) - await ctx.send("Guild removed from the watched list.") - finally: - userDatabase.commit() - c.close() + def get_entry(d): + elem = '|'.join(f'{e:^{self._widths[i]}}' for i, e in enumerate(d)) + return f'|{elem}|' - def __unload(self): - print("cogs.tracking: Cancelling pending tasks...") - self.scan_deaths_task.cancel() - self.scan_highscores_task.cancel() - self.scan_online_chars_task.cancel() + to_draw.append(get_entry(self._columns)) + to_draw.append(sep) + for row in self._rows: + to_draw.append(get_entry(row)) -def setup(bot): - bot.add_cog(Tracking(bot)) + to_draw.append(sep) + return '\n'.join(to_draw) From 748b2d588aa8fca0cb3eb9bc2e01a7f881f02fb2 Mon Sep 17 00:00:00 2001 From: Nezune Date: Sun, 29 Jul 2018 16:52:22 +0000 Subject: [PATCH 14/26] =?UTF-8?q?whoops=20n=C2=BA1=20part=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- utils/messages.py | 178 +++++++++++++++++++++++----------------------- 1 file changed, 89 insertions(+), 89 deletions(-) diff --git a/utils/messages.py b/utils/messages.py index 2470673..53a308c 100644 --- a/utils/messages.py +++ b/utils/messages.py @@ -14,7 +14,7 @@ # Message list for announce_level # Parameters: {name}, {level} , {he_she}, {his_her}, {him_her} # Values in each list element are: -# Relative chance, message, lambda function as filter (takes ctx, level, voc, killer, levels_lost) +# Relative chance, message, lambda function as filter (takes (discord)server, level, voc, killer, levels_lost) # Only relative chance and message are mandatory. level_messages = [ [100, "Congratulations to **{name}** on reaching level {level}!"], @@ -24,96 +24,96 @@ [80, "**{name}** is level {level}, watch out world..."], [100, "**{name}** is level {level} now. Noice."], [100, "**{name}** has finally made it to level {level}, yay!"], - [80, "**{name}** reached level {level}! What a time to be alive...πŸ™„"], - [70, "**{name}** got level {level}! So stronk now!πŸ’ͺ"], - [30, "**{name}** is level {level}🍰\r\n" + - "I'm making a note here:🎢\r\n" + - "Huge success!🎢\r\n" + - "It's hard to overstate my🎢\r\n" + - "SatisfactionπŸ€–"], - [100, "**{name}**, you reached level {level}? Here, have a cookie πŸͺ"], + [80, "**{name}** reached level {level}! What a time to be alive...Γ°ΕΈβ„’β€ž"], + [70, "**{name}** got level {level}! So stronk now!ðŸ’Βͺ"], + [30, "**{name}** is level {level}🍰\r\n" + + "I'm making a note here:Γ°ΕΈΕ½ΒΆ\r\n" + + "Huge success!Γ°ΕΈΕ½ΒΆ\r\n" + + "It's hard to overstate myΓ°ΕΈΕ½ΒΆ\r\n" + + "Satisfaction🀖"], + [100, "**{name}**, you reached level {level}? Here, have a cookie ðŸΒͺ"], [80, "**{name}** got level {level}. I guess this justifies all those creatures {he_she} murdered."], [90, "**{name}** is level {level}. Better than {he_she} was. Better, stronger, faster."], [70, "Congrats **{name}** on getting level {level}! Maybe you can solo rats now?"], [70, "**{name}** is level {level} now! And we all thought {he_she}'d never achieve anything in life."], # EK Only [50, "**{name}** has reached level {level}. That's 9 more mana potions you can carry now!", - lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Knight" in voc], - [200, "**{name}** is level {level}. Stick them with the pointy end! πŸ—‘οΈ", - lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Knight" in voc], + lambda server, level, voc, killer, levels_lost: level >= 100 and "Knight" in voc], + [200, "**{name}** is level {level}. Stick them with the pointy end! 🗑️", + lambda server, level, voc, killer, levels_lost: level >= 100 and "Knight" in voc], [200, "**{name}** is a fat level {level} meatwall now. BLOCK FOR ME SENPAI.", - lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Knight" in voc], + lambda server, level, voc, killer, levels_lost: level >= 100 and "Knight" in voc], # RP Only [50, "**{name}** has reached level {level}. But {he_she} still misses arrows...", - lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Paladin" in voc], + lambda server, level, voc, killer, levels_lost: level >= 100 and "Paladin" in voc], [150, "Congrats on level {level}, **{name}**. You can stop running around now.", - lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Paladin" in voc], - [150, "**{name}** is level {level}. Bullseye!🎯", - lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Paladin" in voc], + lambda server, level, voc, killer, levels_lost: level >= 100 and "Paladin" in voc], + [150, "**{name}** is level {level}. Bullseye!Γ°ΕΈΕ½Β―", + lambda server, level, voc, killer, levels_lost: level >= 100 and "Paladin" in voc], # MS Only [50, "Level {level}, **{name}**? Nice. Don't you wish you were a druid though?", - lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Sorcerer" in voc], + lambda server, level, voc, killer, levels_lost: level >= 100 and "Sorcerer" in voc], [150, "**{name}** is level {level}. Watch out for {his_her} SDs!", - lambda ctx, level, voc, killer, levels_lost: level >= 45 and "Sorcerer" in voc], + lambda server, level, voc, killer, levels_lost: level >= 45 and "Sorcerer" in voc], [150, "**{name}** got level {level}. If {he_she} only stopped missing beams.", - lambda ctx, level, voc, killer, levels_lost: level >= 23 and "Sorcerer" in voc], + lambda server, level, voc, killer, levels_lost: level >= 23 and "Sorcerer" in voc], [150, - "**{name}** is level {level}. πŸ”₯πŸ”₯BURN THEM ALLπŸ”₯πŸ”₯", - lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Sorcerer" in voc], + "**{name}** is level {level}. ðŸ”Β₯ðŸ”Β₯BURN THEM ALLðŸ”Β₯ðŸ”Β₯", + lambda server, level, voc, killer, levels_lost: level >= 100 and "Sorcerer" in voc], # ED Only - [50, "**{name}** has reached level {level}. Flower power!🌼", - lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Druid" in voc], + [50, "**{name}** has reached level {level}. Flower power!Γ°ΕΈΕ’ΒΌ", + lambda server, level, voc, killer, levels_lost: level >= 100 and "Druid" in voc], [150, "Congrats on level {level}, **{name}**. Sio plz.", - lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Druid" in voc], - [150, "**{name}** is level {level}. πŸ”₯πŸ”₯BURN THEM ALL... Or... Give them frostbite...?❄❄", - lambda ctx, level, voc, killer, levels_lost: level >= 100 and "Druid" in voc], + lambda server, level, voc, killer, levels_lost: level >= 100 and "Druid" in voc], + [150, "**{name}** is level {level}. ðŸ”Β₯ðŸ”Β₯BURN THEM ALL... Or... Give them frostbite...?Γ’Ββ€žΓ’Ββ€ž", + lambda server, level, voc, killer, levels_lost: level >= 100 and "Druid" in voc], # Level specific - [20000, "**{name}** is level {level}! UMPs so good 🍷", - lambda ctx, level, voc, killer, levels_lost: level == 130 and ("Druid" in voc or "Sorcerer" in voc)], - [20000, "**{name}** is level {level} now! Eternal Winter is coming!❄", - lambda ctx, level, voc, killer, levels_lost: level == 60 and "Druid" in voc], - [20000, "**{name}** is level {level} now! Time to unleash the Wrath of NatureπŸƒπŸƒ... just look at that wrath.", - lambda ctx, level, voc, killer, levels_lost: level == 55 and "Druid" in voc], - [20000, "**{name}** is now level {level}. Don't forget to buy a Gearwheel Chain!πŸ“Ώ", - lambda ctx, level, voc, killer, levels_lost: level == 75], - [30000, "**{name}** is level {level}! You can become a ninja now!πŸ‘€", - lambda ctx, level, voc, killer, levels_lost: level == 80 and "Paladin" in voc], - [30000, "**{name}** is level {level}! Time to get some crystalline arrows!🏹", - lambda ctx, level, voc, killer, levels_lost: level == 90 and "Paladin" in voc], + [20000, "**{name}** is level {level}! UMPs so good 🍷", + lambda server, level, voc, killer, levels_lost: level == 130 and ("Druid" in voc or "Sorcerer" in voc)], + [20000, "**{name}** is level {level} now! Eternal Winter is coming!Γ’Ββ€ž", + lambda server, level, voc, killer, levels_lost: level == 60 and "Druid" in voc], + [20000, "**{name}** is level {level} now! Time to unleash the Wrath of Nature🍃🍃... just look at that wrath.", + lambda server, level, voc, killer, levels_lost: level == 55 and "Druid" in voc], + [20000, "**{name}** is now level {level}. Don't forget to buy a Gearwheel Chain!Γ°ΕΈβ€œΒΏ", + lambda server, level, voc, killer, levels_lost: level == 75], + [30000, "**{name}** is level {level}! You can become a ninja now!Γ°ΕΈβ€˜Β€", + lambda server, level, voc, killer, levels_lost: level == 80 and "Paladin" in voc], + [30000, "**{name}** is level {level}! Time to get some crystalline arrows!🏹", + lambda server, level, voc, killer, levels_lost: level == 90 and "Paladin" in voc], [20000, "Level {level}, **{name}**? You're finally important enough for me to notice!", - lambda ctx, level, voc, killer, levels_lost: level == get_server_property(ctx.guild.id, "announce_level", is_int=True, default=announce_threshold)], + lambda server, level, voc, killer, levels_lost: level == get_server_property(server.id, "announce_level", is_int=True, default=announce_threshold)], [20000, "Congratulations on level {level} **{name}**! Now you're relevant to me. As relevant a human can be anyway", - lambda ctx, level, voc, killer, levels_lost: level == get_server_property(ctx.guild.id, "announce_level", is_int=True, default=announce_threshold)], - [20000, "**{name}** is now level {level}! Time to go berserk! πŸ’’", - lambda ctx, level, voc, killer, levels_lost: level == 35 and "Knight" in voc], + lambda server, level, voc, killer, levels_lost: level == get_server_property(server.id, "announce_level", is_int=True, default=announce_threshold)], + [20000, "**{name}** is now level {level}! Time to go berserk! 💒", + lambda server, level, voc, killer, levels_lost: level == 35 and "Knight" in voc], [20000, "Congratulations on level {level} **{name}**! Now you can become an umbral master, but is your" - " bank account ready?πŸ’Έ", - lambda ctx, level, voc, killer, levels_lost: level == 250], + " bank account ready?💸", + lambda server, level, voc, killer, levels_lost: level == 250], [30000, "**{name}** is level {level}!!!!\r\n" + "Sweet, sweet triple digits!", - lambda ctx, level, voc, killer, levels_lost: level == 100], + lambda server, level, voc, killer, levels_lost: level == 100], [20000, "**{name}** is level {level}!!!!\r\n" + "WOOO", - lambda ctx, level, voc, killer, levels_lost: level in (100, 200, 300, 400)], + lambda server, level, voc, killer, levels_lost: level in (100, 200, 300, 400)], [20000, "**{name}** is level {level}!!!!\r\n" + "yaaaay milestone!", - lambda ctx, level, voc, killer, levels_lost: level in (100, 200, 300, 400)], + lambda server, level, voc, killer, levels_lost: level in (100, 200, 300, 400)], [20000, "**{name}** is level {level}!!!!\r\n" + "holy crap!", - lambda ctx, level, voc, killer, levels_lost: level in (200, 300, 400)]] + lambda server, level, voc, killer, levels_lost: level in (200, 300, 400)]] # Message list for announce death. # Parameters: ({name},{level},{killer},{killer_article},{he_she}, {his_her},{him_her} # Additionally, words surrounded by \WORD/ are upper cased, /word\ are lower cased, /Word/ are title cased # words surrounded by ^WORD^ are ignored if the next letter found is uppercase (useful for dealing with proper nouns) # Values in each list element are: -# Relative chance, message, lambda function as filter (takes ctx, level, voc, killer, levels_lost) +# Relative chance, message, lambda function as filter (takes (discord)server, level, voc, killer, levels_lost) # Only relative chance and message are mandatory. death_messages_monster = [ [100, "RIP **{name}** ({level}), you died the way you lived- inside {killer_article}**{killer}**."], [100, "**{name}** ({level}) was just eaten by {killer_article}**{killer}**. Yum."], [100, "Silly **{name}** ({level}), I warned you not to play with {killer_article}**{killer}**!"], - [100, "/{killer_article}**/{killer}** killed **{name}** at level {level}. Shame πŸ”” shame πŸ”” shame πŸ””"], + [100, "/{killer_article}**/{killer}** killed **{name}** at level {level}. Shame 🔔 shame 🔔 shame 🔔"], [30, "**{name}** ({level}) is no more! /{he_she}/ has ceased to be! /{he_she}/'s expired and gone to meet " "{his_her} maker! /{he_she}/'s a stiff! Bereft of life, {he_she} rests in peace! If {he_she} hadn't " @@ -123,14 +123,14 @@ "invisible!! THIS IS AN EX-**\{name}/**."], [100, "RIP **{name}** ({level}), we hardly knew you! (^That ^**{killer}** got to know you pretty well " - "though πŸ˜‰)"], - [80, "A priest, {killer_article}**{killer}** and **{name}** ({level}) walk into a bar. πŸ’€ONLY ONE WALKS OUT.πŸ’€"], + "though Γ°ΕΈΛœβ€°)"], + [80, "A priest, {killer_article}**{killer}** and **{name}** ({level}) walk into a bar. 💀ONLY ONE WALKS OUT.💀"], [100, "RIP **{name}** ({level}), you were strong. ^The ^**{killer}** was stronger."], [100, "Oh, there goes **{name}** ({level}), killed by {killer_article}**{killer}**. So young, so full " "of life. /{he_she}/ will be miss... oh nevermind, {he_she} respawned already."], [100, - "Oh look! **{name}** ({level}) died by {killer_article}**{killer}**! What a surprise...πŸ™„"], + "Oh look! **{name}** ({level}) died by {killer_article}**{killer}**! What a surprise...Γ°ΕΈβ„’β€ž"], [100, "**{name}** ({level}) was killed by {killer_article}**{killer}**, but we all saw that coming."], [100, @@ -150,65 +150,65 @@ [100, "Alas, poor **{name}** ({level}), I knew {him_her} Horatio; a fellow of infinite jest, of most " "excellent fancy; {he_she} hath borne me on {his_her} back a thousand times; and now, {he_she} got rekt " "by {killer_article}**{killer}**."], - [70, "To be or not to be πŸ’€, that is the-- Well I guess **{name}** ({level}) made his choice, " + [70, "To be or not to be 💀, that is the-- Well I guess **{name}** ({level}) made his choice, " "or ^that ^**{killer}** chose for him..."], [500, "**{name}** ({level}) just died to {killer_article}**{killer}**, why did nobody sio {him_her}!?", - lambda ctx, level, voc, killer, levels_lost: "Knight" in voc], + lambda server, level, voc, killer, levels_lost: "Knight" in voc], [500, "Poor **{name}** ({level}) has died. Killed by {killer_article}**{killer}**. I bet it was your " "blocker's fault though, eh **{name}**?", - lambda ctx, level, voc, killer, levels_lost: "Druid" in voc or "Sorcerer" in voc], + lambda server, level, voc, killer, levels_lost: "Druid" in voc or "Sorcerer" in voc], [500, "**{name}** ({level}) tried running away from {killer_article}**{killer}**. /{he_she}/ " "didn't run fast enough...", - lambda ctx, level, voc, killer, levels_lost: "Paladin" in voc], + lambda server, level, voc, killer, levels_lost: "Paladin" in voc], [500, "What happened to **{name}** ({level})!? Talk about sudden death! I guess ^that ^**{killer}** was " "too much for {him_her}...", - lambda ctx, level, voc, killer, levels_lost: "Sorcerer" in voc], + lambda server, level, voc, killer, levels_lost: "Sorcerer" in voc], [500, "**{name}** ({level}) was killed by {killer_article}**{killer}**. I guess {he_she} couldn't " "sio {him_her}self.", - lambda ctx, level, voc, killer, levels_lost: "Druid" in voc], + lambda server, level, voc, killer, levels_lost: "Druid" in voc], [600, "**{name}** ({level}) died to {killer_article}**{killer}**. \"Don't worry\" they said, \"They are weaker\" " "they said.", - lambda ctx, level, voc, killer, levels_lost: killer in ["weakened frazzlemaw", "enfeebled silencer"]], + lambda server, level, voc, killer, levels_lost: killer in ["weakened frazzlemaw", "enfeebled silencer"]], [2000, "Another paladin bites the dust! **{killer}** strikes again! Rest in peace **{name}** ({level}).", - lambda ctx, level, voc, killer, levels_lost: "Paladin" in voc and killer == "Lady Tenebris"], - [2000, "**{name}** ({level}) got killed by ***{killer}***. How spooky is that! πŸ‘»", - lambda ctx, level, voc, killer, levels_lost: killer == "something evil"], + lambda server, level, voc, killer, levels_lost: "Paladin" in voc and killer == "Lady Tenebris"], + [2000, "**{name}** ({level}) got killed by ***{killer}***. How spooky is that! Γ°ΕΈβ€˜Β»", + lambda server, level, voc, killer, levels_lost: killer == "something evil"], [2000, "**{name}** ({level}) died from **{killer}**. Yeah, no shit.", - lambda ctx, level, voc, killer, levels_lost: killer == "death"], - [2000, "They did warn you **{name}** ({level}), you *did* burn πŸ”₯🐲.", - lambda ctx, level, voc, killer, levels_lost: killer in ["dragon", "dragon lord"]], - [2000, "**{name}** ({level}) died from {killer_article}**{killer}**. Someone forgot the safeword.😏", - lambda ctx, level, voc, killer, levels_lost: killer == "choking fear"], + lambda server, level, voc, killer, levels_lost: killer == "death"], + [2000, "They did warn you **{name}** ({level}), you *did* burn ðŸ”Β₯🐲.", + lambda server, level, voc, killer, levels_lost: killer in ["dragon", "dragon lord"]], + [2000, "**{name}** ({level}) died from {killer_article}**{killer}**. Someone forgot the safeword.😏", + lambda server, level, voc, killer, levels_lost: killer == "choking fear"], [2000, "That **{killer}** got really up close and personal with **{name}** ({level}). " - "Maybe he thought you were his princess Lumelia?😏", - lambda ctx, level, voc, killer, levels_lost: killer == "hero"], - [2000, "Looks like that **{killer}** made **{name}** ({level}) his bride πŸ˜‰.", - lambda ctx, level, voc, killer, levels_lost: "vampire" in killer], + "Maybe he thought you were his princess Lumelia?😏", + lambda server, level, voc, killer, levels_lost: killer == "hero"], + [2000, "Looks like that **{killer}** made **{name}** ({level}) his bride Γ°ΕΈΛœβ€°.", + lambda server, level, voc, killer, levels_lost: "vampire" in killer], [2000, "Yeah, those are a little stronger than regular orcs, **{name}** ({level}).", - lambda ctx, level, voc, killer, levels_lost: "orc cult" in killer], + lambda server, level, voc, killer, levels_lost: "orc cult" in killer], [1000, "Damn! The koolaid they drink in that cult must have steroids on it, **{name}** ({level}).", - lambda ctx, level, voc, killer, levels_lost: "cult" in killer], + lambda server, level, voc, killer, levels_lost: "cult" in killer], [2500, "**{name}** ({level}) met {his_her} demise at the hands of a **{killer}**. That's hot.", - lambda ctx, level, voc, killer, levels_lost: killer in ["true dawnfire asura", "dawnfire asura", "fury"]], + lambda server, level, voc, killer, levels_lost: killer in ["true dawnfire asura", "dawnfire asura", "fury"]], [2500, "Poor **{name}** ({level}) just wanted some love! That cold hearted... Witch.", - lambda ctx, level, voc, killer, levels_lost: killer in ["true frost flower asura", "frost flower asura", "frost giantess", "ice witch"]], - [2000, "Asian chicks are no joke **{name}** ({level}) πŸ”ͺπŸ’”.", - lambda ctx, level, voc, killer, levels_lost: "asura" in killer], - [2500, "Asian chicks sure age well, don't you think so, **{name}** ({level})? πŸ˜πŸ‘΅.", - lambda ctx, level, voc, killer, levels_lost: "true" in killer and "asura" in killer], + lambda server, level, voc, killer, levels_lost: killer in ["true frost flower asura", "frost flower asura", "frost giantess", "ice witch"]], + [2000, "Asian chicks are no joke **{name}** ({level}) ðŸ”Βͺ💔.", + lambda server, level, voc, killer, levels_lost: "asura" in killer], + [2500, "Asian chicks sure age well, don't you think so, **{name}** ({level})? Γ°ΕΈΛœΒΓ°ΕΈβ€˜Β΅.", + lambda server, level, voc, killer, levels_lost: "true" in killer and "asura" in killer], [2000, "**{name}** ({level}) got destroyed by {killer_article}**{killer}**. I bet {he_she} regrets going down" - "that hole πŸ•³οΈ", - lambda ctx, level, voc, killer, levels_lost: level < 120 and killer in ["breach brood", "dread intruder", "reality reaver", + "that hole ðŸ‒³ï¸", + lambda server, level, voc, killer, levels_lost: level < 120 and killer in ["breach brood", "dread intruder", "reality reaver", "spark of destruction", "sparkion"]], [2000, - "Watch out for that **{killer}**'s wav... Oh😐... Rest in peace **{name}** ({level}).", - lambda ctx, level, voc, killer, levels_lost: killer in ["dragon", "dragon lord", "undead dragon", "draken spellweaver", + "Watch out for that **{killer}**'s wav... Oh😐... Rest in peace **{name}** ({level}).", + lambda server, level, voc, killer, levels_lost: killer in ["dragon", "dragon lord", "undead dragon", "draken spellweaver", "hellhound", "hellfire fighter"]], [2000, "**{name}** ({level}) died to {killer_article}**{killer}**! Don't worry, {he_she} didn't have a soul anyway", - lambda ctx, level, voc, killer, levels_lost: killer == "souleater"], + lambda server, level, voc, killer, levels_lost: killer == "souleater"], [150, "Oh look at that, rest in peace **{name}** ({level}), ^that ^**{killer}** really got you. " "Hope you get your level back.", - lambda ctx, level, voc, killer, levels_lost: levels_lost > 0] + lambda server, level, voc, killer, levels_lost: levels_lost > 0] ] # Deaths by players @@ -250,7 +250,7 @@ def format_message(message) -> str: return message -def weighed_choice(ctx, choices, level: int, vocation: str = None, killer: str = None, levels_lost: int = 0) -> str: +def weighed_choice(server, choices, level: int, vocation: str = None, killer: str = None, levels_lost: int = 0) -> str: """Makes weighed choices from message lists where [0] is a value representing the relative odds of picking a message and [1] is the message string""" @@ -259,7 +259,7 @@ def weighed_choice(ctx, choices, level: int, vocation: str = None, killer: str = weight_range = 0 _messages = [] for message in choices: - if len(message) == 3 and not message[2](ctx, level, vocation, killer, levels_lost): + if len(message) == 3 and not message[2](server, level, vocation, killer, levels_lost): continue weight_range = weight_range + (message[0] if not message[1] in last_messages else message[0] / 10) _messages.append(message) From 108a1cadca869d77e8320a0b49eae627f3d0f40b Mon Sep 17 00:00:00 2001 From: Nezune Date: Sun, 29 Jul 2018 16:54:21 +0000 Subject: [PATCH 15/26] god damnit --- utils/messages.py | 70 +++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/utils/messages.py b/utils/messages.py index 53a308c..c7eb892 100644 --- a/utils/messages.py +++ b/utils/messages.py @@ -24,14 +24,14 @@ [80, "**{name}** is level {level}, watch out world..."], [100, "**{name}** is level {level} now. Noice."], [100, "**{name}** has finally made it to level {level}, yay!"], - [80, "**{name}** reached level {level}! What a time to be alive...Γ°ΕΈβ„’β€ž"], - [70, "**{name}** got level {level}! So stronk now!ðŸ’Βͺ"], - [30, "**{name}** is level {level}🍰\r\n" + - "I'm making a note here:Γ°ΕΈΕ½ΒΆ\r\n" + - "Huge success!Γ°ΕΈΕ½ΒΆ\r\n" + - "It's hard to overstate myΓ°ΕΈΕ½ΒΆ\r\n" + - "Satisfaction🀖"], - [100, "**{name}**, you reached level {level}? Here, have a cookie ðŸΒͺ"], + [80, "**{name}** reached level {level}! What a time to be alive...πŸ™„"], + [70, "**{name}** got level {level}! So stronk now!πŸ’ͺ"], + [30, "**{name}** is level {level}🍰\r\n" + + "I'm making a note here:🎢\r\n" + + "Huge success!🎢\r\n" + + "It's hard to overstate my🎢\r\n" + + "SatisfactionπŸ€–"], + [100, "**{name}**, you reached level {level}? Here, have a cookie πŸͺ"], [80, "**{name}** got level {level}. I guess this justifies all those creatures {he_she} murdered."], [90, "**{name}** is level {level}. Better than {he_she} was. Better, stronger, faster."], [70, "Congrats **{name}** on getting level {level}! Maybe you can solo rats now?"], @@ -39,7 +39,7 @@ # EK Only [50, "**{name}** has reached level {level}. That's 9 more mana potions you can carry now!", lambda server, level, voc, killer, levels_lost: level >= 100 and "Knight" in voc], - [200, "**{name}** is level {level}. Stick them with the pointy end! 🗑️", + [200, "**{name}** is level {level}. Stick them with the pointy end! πŸ—‘οΈ", lambda server, level, voc, killer, levels_lost: level >= 100 and "Knight" in voc], [200, "**{name}** is a fat level {level} meatwall now. BLOCK FOR ME SENPAI.", lambda server, level, voc, killer, levels_lost: level >= 100 and "Knight" in voc], @@ -48,7 +48,7 @@ lambda server, level, voc, killer, levels_lost: level >= 100 and "Paladin" in voc], [150, "Congrats on level {level}, **{name}**. You can stop running around now.", lambda server, level, voc, killer, levels_lost: level >= 100 and "Paladin" in voc], - [150, "**{name}** is level {level}. Bullseye!Γ°ΕΈΕ½Β―", + [150, "**{name}** is level {level}. Bullseye!🎯", lambda server, level, voc, killer, levels_lost: level >= 100 and "Paladin" in voc], # MS Only [50, "Level {level}, **{name}**? Nice. Don't you wish you were a druid though?", @@ -58,36 +58,36 @@ [150, "**{name}** got level {level}. If {he_she} only stopped missing beams.", lambda server, level, voc, killer, levels_lost: level >= 23 and "Sorcerer" in voc], [150, - "**{name}** is level {level}. ðŸ”Β₯ðŸ”Β₯BURN THEM ALLðŸ”Β₯ðŸ”Β₯", + "**{name}** is level {level}. πŸ”₯πŸ”₯BURN THEM ALLπŸ”₯πŸ”₯", lambda server, level, voc, killer, levels_lost: level >= 100 and "Sorcerer" in voc], # ED Only - [50, "**{name}** has reached level {level}. Flower power!Γ°ΕΈΕ’ΒΌ", + [50, "**{name}** has reached level {level}. Flower power!🌼", lambda server, level, voc, killer, levels_lost: level >= 100 and "Druid" in voc], [150, "Congrats on level {level}, **{name}**. Sio plz.", lambda server, level, voc, killer, levels_lost: level >= 100 and "Druid" in voc], - [150, "**{name}** is level {level}. ðŸ”Β₯ðŸ”Β₯BURN THEM ALL... Or... Give them frostbite...?Γ’Ββ€žΓ’Ββ€ž", + [150, "**{name}** is level {level}. πŸ”₯πŸ”₯BURN THEM ALL... Or... Give them frostbite...?❄❄", lambda server, level, voc, killer, levels_lost: level >= 100 and "Druid" in voc], # Level specific - [20000, "**{name}** is level {level}! UMPs so good 🍷", + [20000, "**{name}** is level {level}! UMPs so good 🍷", lambda server, level, voc, killer, levels_lost: level == 130 and ("Druid" in voc or "Sorcerer" in voc)], - [20000, "**{name}** is level {level} now! Eternal Winter is coming!Γ’Ββ€ž", + [20000, "**{name}** is level {level} now! Eternal Winter is coming!❄", lambda server, level, voc, killer, levels_lost: level == 60 and "Druid" in voc], - [20000, "**{name}** is level {level} now! Time to unleash the Wrath of Nature🍃🍃... just look at that wrath.", + [20000, "**{name}** is level {level} now! Time to unleash the Wrath of NatureπŸƒπŸƒ... just look at that wrath.", lambda server, level, voc, killer, levels_lost: level == 55 and "Druid" in voc], - [20000, "**{name}** is now level {level}. Don't forget to buy a Gearwheel Chain!Γ°ΕΈβ€œΒΏ", + [20000, "**{name}** is now level {level}. Don't forget to buy a Gearwheel Chain!πŸ“Ώ", lambda server, level, voc, killer, levels_lost: level == 75], - [30000, "**{name}** is level {level}! You can become a ninja now!Γ°ΕΈβ€˜Β€", + [30000, "**{name}** is level {level}! You can become a ninja now!πŸ‘€", lambda server, level, voc, killer, levels_lost: level == 80 and "Paladin" in voc], - [30000, "**{name}** is level {level}! Time to get some crystalline arrows!🏹", + [30000, "**{name}** is level {level}! Time to get some crystalline arrows!🏹", lambda server, level, voc, killer, levels_lost: level == 90 and "Paladin" in voc], [20000, "Level {level}, **{name}**? You're finally important enough for me to notice!", lambda server, level, voc, killer, levels_lost: level == get_server_property(server.id, "announce_level", is_int=True, default=announce_threshold)], [20000, "Congratulations on level {level} **{name}**! Now you're relevant to me. As relevant a human can be anyway", lambda server, level, voc, killer, levels_lost: level == get_server_property(server.id, "announce_level", is_int=True, default=announce_threshold)], - [20000, "**{name}** is now level {level}! Time to go berserk! 💒", + [20000, "**{name}** is now level {level}! Time to go berserk! πŸ’’", lambda server, level, voc, killer, levels_lost: level == 35 and "Knight" in voc], [20000, "Congratulations on level {level} **{name}**! Now you can become an umbral master, but is your" - " bank account ready?💸", + " bank account ready?πŸ’Έ", lambda server, level, voc, killer, levels_lost: level == 250], [30000, "**{name}** is level {level}!!!!\r\n" + "Sweet, sweet triple digits!", @@ -113,7 +113,7 @@ [100, "RIP **{name}** ({level}), you died the way you lived- inside {killer_article}**{killer}**."], [100, "**{name}** ({level}) was just eaten by {killer_article}**{killer}**. Yum."], [100, "Silly **{name}** ({level}), I warned you not to play with {killer_article}**{killer}**!"], - [100, "/{killer_article}**/{killer}** killed **{name}** at level {level}. Shame 🔔 shame 🔔 shame 🔔"], + [100, "/{killer_article}**/{killer}** killed **{name}** at level {level}. Shame πŸ”” shame πŸ”” shame πŸ””"], [30, "**{name}** ({level}) is no more! /{he_she}/ has ceased to be! /{he_she}/'s expired and gone to meet " "{his_her} maker! /{he_she}/'s a stiff! Bereft of life, {he_she} rests in peace! If {he_she} hadn't " @@ -123,14 +123,14 @@ "invisible!! THIS IS AN EX-**\{name}/**."], [100, "RIP **{name}** ({level}), we hardly knew you! (^That ^**{killer}** got to know you pretty well " - "though Γ°ΕΈΛœβ€°)"], - [80, "A priest, {killer_article}**{killer}** and **{name}** ({level}) walk into a bar. 💀ONLY ONE WALKS OUT.💀"], + "though πŸ˜‰)"], + [80, "A priest, {killer_article}**{killer}** and **{name}** ({level}) walk into a bar. πŸ’€ONLY ONE WALKS OUT.πŸ’€"], [100, "RIP **{name}** ({level}), you were strong. ^The ^**{killer}** was stronger."], [100, "Oh, there goes **{name}** ({level}), killed by {killer_article}**{killer}**. So young, so full " "of life. /{he_she}/ will be miss... oh nevermind, {he_she} respawned already."], [100, - "Oh look! **{name}** ({level}) died by {killer_article}**{killer}**! What a surprise...Γ°ΕΈβ„’β€ž"], + "Oh look! **{name}** ({level}) died by {killer_article}**{killer}**! What a surprise...πŸ™„"], [100, "**{name}** ({level}) was killed by {killer_article}**{killer}**, but we all saw that coming."], [100, @@ -150,7 +150,7 @@ [100, "Alas, poor **{name}** ({level}), I knew {him_her} Horatio; a fellow of infinite jest, of most " "excellent fancy; {he_she} hath borne me on {his_her} back a thousand times; and now, {he_she} got rekt " "by {killer_article}**{killer}**."], - [70, "To be or not to be 💀, that is the-- Well I guess **{name}** ({level}) made his choice, " + [70, "To be or not to be πŸ’€, that is the-- Well I guess **{name}** ({level}) made his choice, " "or ^that ^**{killer}** chose for him..."], [500, "**{name}** ({level}) just died to {killer_article}**{killer}**, why did nobody sio {him_her}!?", lambda server, level, voc, killer, levels_lost: "Knight" in voc], @@ -171,18 +171,18 @@ lambda server, level, voc, killer, levels_lost: killer in ["weakened frazzlemaw", "enfeebled silencer"]], [2000, "Another paladin bites the dust! **{killer}** strikes again! Rest in peace **{name}** ({level}).", lambda server, level, voc, killer, levels_lost: "Paladin" in voc and killer == "Lady Tenebris"], - [2000, "**{name}** ({level}) got killed by ***{killer}***. How spooky is that! Γ°ΕΈβ€˜Β»", + [2000, "**{name}** ({level}) got killed by ***{killer}***. How spooky is that! πŸ‘»", lambda server, level, voc, killer, levels_lost: killer == "something evil"], [2000, "**{name}** ({level}) died from **{killer}**. Yeah, no shit.", lambda server, level, voc, killer, levels_lost: killer == "death"], - [2000, "They did warn you **{name}** ({level}), you *did* burn ðŸ”Β₯🐲.", + [2000, "They did warn you **{name}** ({level}), you *did* burn πŸ”₯🐲.", lambda server, level, voc, killer, levels_lost: killer in ["dragon", "dragon lord"]], - [2000, "**{name}** ({level}) died from {killer_article}**{killer}**. Someone forgot the safeword.😏", + [2000, "**{name}** ({level}) died from {killer_article}**{killer}**. Someone forgot the safeword.😏", lambda server, level, voc, killer, levels_lost: killer == "choking fear"], [2000, "That **{killer}** got really up close and personal with **{name}** ({level}). " - "Maybe he thought you were his princess Lumelia?😏", + "Maybe he thought you were his princess Lumelia?😏", lambda server, level, voc, killer, levels_lost: killer == "hero"], - [2000, "Looks like that **{killer}** made **{name}** ({level}) his bride Γ°ΕΈΛœβ€°.", + [2000, "Looks like that **{killer}** made **{name}** ({level}) his bride πŸ˜‰.", lambda server, level, voc, killer, levels_lost: "vampire" in killer], [2000, "Yeah, those are a little stronger than regular orcs, **{name}** ({level}).", lambda server, level, voc, killer, levels_lost: "orc cult" in killer], @@ -192,16 +192,16 @@ lambda server, level, voc, killer, levels_lost: killer in ["true dawnfire asura", "dawnfire asura", "fury"]], [2500, "Poor **{name}** ({level}) just wanted some love! That cold hearted... Witch.", lambda server, level, voc, killer, levels_lost: killer in ["true frost flower asura", "frost flower asura", "frost giantess", "ice witch"]], - [2000, "Asian chicks are no joke **{name}** ({level}) ðŸ”Βͺ💔.", + [2000, "Asian chicks are no joke **{name}** ({level}) πŸ”ͺπŸ’”.", lambda server, level, voc, killer, levels_lost: "asura" in killer], - [2500, "Asian chicks sure age well, don't you think so, **{name}** ({level})? Γ°ΕΈΛœΒΓ°ΕΈβ€˜Β΅.", + [2500, "Asian chicks sure age well, don't you think so, **{name}** ({level})? πŸ˜πŸ‘΅.", lambda server, level, voc, killer, levels_lost: "true" in killer and "asura" in killer], [2000, "**{name}** ({level}) got destroyed by {killer_article}**{killer}**. I bet {he_she} regrets going down" - "that hole ðŸ‒³ï¸", + "that hole πŸ•³οΈ", lambda server, level, voc, killer, levels_lost: level < 120 and killer in ["breach brood", "dread intruder", "reality reaver", "spark of destruction", "sparkion"]], [2000, - "Watch out for that **{killer}**'s wav... Oh😐... Rest in peace **{name}** ({level}).", + "Watch out for that **{killer}**'s wav... Oh😐... Rest in peace **{name}** ({level}).", lambda server, level, voc, killer, levels_lost: killer in ["dragon", "dragon lord", "undead dragon", "draken spellweaver", "hellhound", "hellfire fighter"]], [2000, "**{name}** ({level}) died to {killer_article}**{killer}**! Don't worry, {he_she} didn't have a soul anyway", From 2bdae360d6e6f920af257a041fc2eb44c559daff Mon Sep 17 00:00:00 2001 From: Nezune Date: Sun, 29 Jul 2018 16:55:31 +0000 Subject: [PATCH 16/26] =?UTF-8?q?whoops=20n=C2=BA2,=20totally=20called=20i?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit but this is a three act show --- cogs/tracking.py | 1811 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 1425 insertions(+), 386 deletions(-) diff --git a/cogs/tracking.py b/cogs/tracking.py index 53a308c..d554980 100644 --- a/cogs/tracking.py +++ b/cogs/tracking.py @@ -1,400 +1,1439 @@ -import random +import asyncio +import datetime as dt +import pickle import re +import time +import urllib.parse +from contextlib import closing +from typing import List import discord +from discord.ext import commands +from nabbot import NabBot +from utils import checks from utils.config import config -from utils.database import get_server_property - -announce_threshold = config.announce_threshold - -# We save the last messages so they are not repeated so often -last_messages = [""]*10 - -# Message list for announce_level -# Parameters: {name}, {level} , {he_she}, {his_her}, {him_her} -# Values in each list element are: -# Relative chance, message, lambda function as filter (takes (discord)server, level, voc, killer, levels_lost) -# Only relative chance and message are mandatory. -level_messages = [ - [100, "Congratulations to **{name}** on reaching level {level}!"], - [100, "**{name}** is level {level} now, congrats!"], - [80, "**{name}** has reached level {level}, die and lose it, noob!"], - [100, "Well, look at **{name}** with {his_her} new fancy level {level}."], - [80, "**{name}** is level {level}, watch out world..."], - [100, "**{name}** is level {level} now. Noice."], - [100, "**{name}** has finally made it to level {level}, yay!"], - [80, "**{name}** reached level {level}! What a time to be alive...Γ°ΕΈβ„’β€ž"], - [70, "**{name}** got level {level}! So stronk now!ðŸ’Βͺ"], - [30, "**{name}** is level {level}🍰\r\n" + - "I'm making a note here:Γ°ΕΈΕ½ΒΆ\r\n" + - "Huge success!Γ°ΕΈΕ½ΒΆ\r\n" + - "It's hard to overstate myΓ°ΕΈΕ½ΒΆ\r\n" + - "Satisfaction🀖"], - [100, "**{name}**, you reached level {level}? Here, have a cookie ðŸΒͺ"], - [80, "**{name}** got level {level}. I guess this justifies all those creatures {he_she} murdered."], - [90, "**{name}** is level {level}. Better than {he_she} was. Better, stronger, faster."], - [70, "Congrats **{name}** on getting level {level}! Maybe you can solo rats now?"], - [70, "**{name}** is level {level} now! And we all thought {he_she}'d never achieve anything in life."], - # EK Only - [50, "**{name}** has reached level {level}. That's 9 more mana potions you can carry now!", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Knight" in voc], - [200, "**{name}** is level {level}. Stick them with the pointy end! 🗑️", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Knight" in voc], - [200, "**{name}** is a fat level {level} meatwall now. BLOCK FOR ME SENPAI.", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Knight" in voc], - # RP Only - [50, "**{name}** has reached level {level}. But {he_she} still misses arrows...", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Paladin" in voc], - [150, "Congrats on level {level}, **{name}**. You can stop running around now.", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Paladin" in voc], - [150, "**{name}** is level {level}. Bullseye!Γ°ΕΈΕ½Β―", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Paladin" in voc], - # MS Only - [50, "Level {level}, **{name}**? Nice. Don't you wish you were a druid though?", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Sorcerer" in voc], - [150, "**{name}** is level {level}. Watch out for {his_her} SDs!", - lambda server, level, voc, killer, levels_lost: level >= 45 and "Sorcerer" in voc], - [150, "**{name}** got level {level}. If {he_she} only stopped missing beams.", - lambda server, level, voc, killer, levels_lost: level >= 23 and "Sorcerer" in voc], - [150, - "**{name}** is level {level}. ðŸ”Β₯ðŸ”Β₯BURN THEM ALLðŸ”Β₯ðŸ”Β₯", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Sorcerer" in voc], - # ED Only - [50, "**{name}** has reached level {level}. Flower power!Γ°ΕΈΕ’ΒΌ", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Druid" in voc], - [150, "Congrats on level {level}, **{name}**. Sio plz.", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Druid" in voc], - [150, "**{name}** is level {level}. ðŸ”Β₯ðŸ”Β₯BURN THEM ALL... Or... Give them frostbite...?Γ’Ββ€žΓ’Ββ€ž", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Druid" in voc], - # Level specific - [20000, "**{name}** is level {level}! UMPs so good 🍷", - lambda server, level, voc, killer, levels_lost: level == 130 and ("Druid" in voc or "Sorcerer" in voc)], - [20000, "**{name}** is level {level} now! Eternal Winter is coming!Γ’Ββ€ž", - lambda server, level, voc, killer, levels_lost: level == 60 and "Druid" in voc], - [20000, "**{name}** is level {level} now! Time to unleash the Wrath of Nature🍃🍃... just look at that wrath.", - lambda server, level, voc, killer, levels_lost: level == 55 and "Druid" in voc], - [20000, "**{name}** is now level {level}. Don't forget to buy a Gearwheel Chain!Γ°ΕΈβ€œΒΏ", - lambda server, level, voc, killer, levels_lost: level == 75], - [30000, "**{name}** is level {level}! You can become a ninja now!Γ°ΕΈβ€˜Β€", - lambda server, level, voc, killer, levels_lost: level == 80 and "Paladin" in voc], - [30000, "**{name}** is level {level}! Time to get some crystalline arrows!🏹", - lambda server, level, voc, killer, levels_lost: level == 90 and "Paladin" in voc], - [20000, "Level {level}, **{name}**? You're finally important enough for me to notice!", - lambda server, level, voc, killer, levels_lost: level == get_server_property(server.id, "announce_level", is_int=True, default=announce_threshold)], - [20000, "Congratulations on level {level} **{name}**! Now you're relevant to me. As relevant a human can be anyway", - lambda server, level, voc, killer, levels_lost: level == get_server_property(server.id, "announce_level", is_int=True, default=announce_threshold)], - [20000, "**{name}** is now level {level}! Time to go berserk! 💒", - lambda server, level, voc, killer, levels_lost: level == 35 and "Knight" in voc], - [20000, "Congratulations on level {level} **{name}**! Now you can become an umbral master, but is your" - " bank account ready?💸", - lambda server, level, voc, killer, levels_lost: level == 250], - [30000, "**{name}** is level {level}!!!!\r\n" + - "Sweet, sweet triple digits!", - lambda server, level, voc, killer, levels_lost: level == 100], - [20000, "**{name}** is level {level}!!!!\r\n" + - "WOOO", - lambda server, level, voc, killer, levels_lost: level in (100, 200, 300, 400)], - [20000, "**{name}** is level {level}!!!!\r\n" + - "yaaaay milestone!", - lambda server, level, voc, killer, levels_lost: level in (100, 200, 300, 400)], - [20000, "**{name}** is level {level}!!!!\r\n" + - "holy crap!", - lambda server, level, voc, killer, levels_lost: level in (200, 300, 400)]] - -# Message list for announce death. -# Parameters: ({name},{level},{killer},{killer_article},{he_she}, {his_her},{him_her} -# Additionally, words surrounded by \WORD/ are upper cased, /word\ are lower cased, /Word/ are title cased -# words surrounded by ^WORD^ are ignored if the next letter found is uppercase (useful for dealing with proper nouns) -# Values in each list element are: -# Relative chance, message, lambda function as filter (takes (discord)server, level, voc, killer, levels_lost) -# Only relative chance and message are mandatory. -death_messages_monster = [ - [100, "RIP **{name}** ({level}), you died the way you lived- inside {killer_article}**{killer}**."], - [100, "**{name}** ({level}) was just eaten by {killer_article}**{killer}**. Yum."], - [100, "Silly **{name}** ({level}), I warned you not to play with {killer_article}**{killer}**!"], - [100, "/{killer_article}**/{killer}** killed **{name}** at level {level}. Shame 🔔 shame 🔔 shame 🔔"], - [30, - "**{name}** ({level}) is no more! /{he_she}/ has ceased to be! /{he_she}/'s expired and gone to meet " - "{his_her} maker! /{he_she}/'s a stiff! Bereft of life, {he_she} rests in peace! If {he_she} hadn't " - "respawned {he_she}'d be pushing up the daisies! /{his_her}/ metabolic processes are now history! " - "/{he_she}/'s off the server! /{he_she}/'s kicked the bucket, {he_she}'s shuffled off {his_her} mortal " - "coil, kissed {killer_article}**{killer}**'s butt, run down the curtain and joined the bleeding choir " - "invisible!! THIS IS AN EX-**\{name}/**."], - [100, - "RIP **{name}** ({level}), we hardly knew you! (^That ^**{killer}** got to know you pretty well " - "though Γ°ΕΈΛœβ€°)"], - [80, "A priest, {killer_article}**{killer}** and **{name}** ({level}) walk into a bar. 💀ONLY ONE WALKS OUT.💀"], - [100, "RIP **{name}** ({level}), you were strong. ^The ^**{killer}** was stronger."], - [100, - "Oh, there goes **{name}** ({level}), killed by {killer_article}**{killer}**. So young, so full " - "of life. /{he_she}/ will be miss... oh nevermind, {he_she} respawned already."], - [100, - "Oh look! **{name}** ({level}) died by {killer_article}**{killer}**! What a surprise...Γ°ΕΈβ„’β€ž"], - [100, - "**{name}** ({level}) was killed by {killer_article}**{killer}**, but we all saw that coming."], - [100, - "**{name}** ({level}) tried sneaking around {killer_article}**{killer}**. I could hear Colonel " - "Campbell's voice over codec: *Snake? Snake!? SNAAAAAAAAAKE!!?*"], - [50, - "**{name}** ({level}) died to {killer_article}**{killer}**. But I bet it was because there was " - "a flood and something broke with like 7200lb falling over the infrastructure of your city's internet, right?"], - [70, "That's what you get **{name}** ({level}), for messing with ^that ^**{killer}**!"], - [100, - "Oh no! **{name}** died at level {level}. Well, it's okay, just blame lag, I'm sure ^the ^" - "**{killer}** had nothing to do with it."], - [100, "**{name}** ({level}) + **{killer}** = dedd."], - [100, "**{name}** ({level}) got killed by a **{killer}**. Another one bites the dust!"], - [100, "**{name}** ({level}) just kicked the bucket. And by kicked the bucket I mean a **{killer}** beat " - "the crap out of {him_her}."], - [100, "Alas, poor **{name}** ({level}), I knew {him_her} Horatio; a fellow of infinite jest, of most " - "excellent fancy; {he_she} hath borne me on {his_her} back a thousand times; and now, {he_she} got rekt " - "by {killer_article}**{killer}**."], - [70, "To be or not to be 💀, that is the-- Well I guess **{name}** ({level}) made his choice, " - "or ^that ^**{killer}** chose for him..."], - [500, "**{name}** ({level}) just died to {killer_article}**{killer}**, why did nobody sio {him_her}!?", - lambda server, level, voc, killer, levels_lost: "Knight" in voc], - [500, "Poor **{name}** ({level}) has died. Killed by {killer_article}**{killer}**. I bet it was your " - "blocker's fault though, eh **{name}**?", - lambda server, level, voc, killer, levels_lost: "Druid" in voc or "Sorcerer" in voc], - [500, "**{name}** ({level}) tried running away from {killer_article}**{killer}**. /{he_she}/ " - "didn't run fast enough...", - lambda server, level, voc, killer, levels_lost: "Paladin" in voc], - [500, "What happened to **{name}** ({level})!? Talk about sudden death! I guess ^that ^**{killer}** was " - "too much for {him_her}...", - lambda server, level, voc, killer, levels_lost: "Sorcerer" in voc], - [500, "**{name}** ({level}) was killed by {killer_article}**{killer}**. I guess {he_she} couldn't " - "sio {him_her}self.", - lambda server, level, voc, killer, levels_lost: "Druid" in voc], - [600, "**{name}** ({level}) died to {killer_article}**{killer}**. \"Don't worry\" they said, \"They are weaker\" " - "they said.", - lambda server, level, voc, killer, levels_lost: killer in ["weakened frazzlemaw", "enfeebled silencer"]], - [2000, "Another paladin bites the dust! **{killer}** strikes again! Rest in peace **{name}** ({level}).", - lambda server, level, voc, killer, levels_lost: "Paladin" in voc and killer == "Lady Tenebris"], - [2000, "**{name}** ({level}) got killed by ***{killer}***. How spooky is that! Γ°ΕΈβ€˜Β»", - lambda server, level, voc, killer, levels_lost: killer == "something evil"], - [2000, "**{name}** ({level}) died from **{killer}**. Yeah, no shit.", - lambda server, level, voc, killer, levels_lost: killer == "death"], - [2000, "They did warn you **{name}** ({level}), you *did* burn ðŸ”Β₯🐲.", - lambda server, level, voc, killer, levels_lost: killer in ["dragon", "dragon lord"]], - [2000, "**{name}** ({level}) died from {killer_article}**{killer}**. Someone forgot the safeword.😏", - lambda server, level, voc, killer, levels_lost: killer == "choking fear"], - [2000, "That **{killer}** got really up close and personal with **{name}** ({level}). " - "Maybe he thought you were his princess Lumelia?😏", - lambda server, level, voc, killer, levels_lost: killer == "hero"], - [2000, "Looks like that **{killer}** made **{name}** ({level}) his bride Γ°ΕΈΛœβ€°.", - lambda server, level, voc, killer, levels_lost: "vampire" in killer], - [2000, "Yeah, those are a little stronger than regular orcs, **{name}** ({level}).", - lambda server, level, voc, killer, levels_lost: "orc cult" in killer], - [1000, "Damn! The koolaid they drink in that cult must have steroids on it, **{name}** ({level}).", - lambda server, level, voc, killer, levels_lost: "cult" in killer], - [2500, "**{name}** ({level}) met {his_her} demise at the hands of a **{killer}**. That's hot.", - lambda server, level, voc, killer, levels_lost: killer in ["true dawnfire asura", "dawnfire asura", "fury"]], - [2500, "Poor **{name}** ({level}) just wanted some love! That cold hearted... Witch.", - lambda server, level, voc, killer, levels_lost: killer in ["true frost flower asura", "frost flower asura", "frost giantess", "ice witch"]], - [2000, "Asian chicks are no joke **{name}** ({level}) ðŸ”Βͺ💔.", - lambda server, level, voc, killer, levels_lost: "asura" in killer], - [2500, "Asian chicks sure age well, don't you think so, **{name}** ({level})? Γ°ΕΈΛœΒΓ°ΕΈβ€˜Β΅.", - lambda server, level, voc, killer, levels_lost: "true" in killer and "asura" in killer], - [2000, "**{name}** ({level}) got destroyed by {killer_article}**{killer}**. I bet {he_she} regrets going down" - "that hole ðŸ‒³ï¸", - lambda server, level, voc, killer, levels_lost: level < 120 and killer in ["breach brood", "dread intruder", "reality reaver", - "spark of destruction", "sparkion"]], - [2000, - "Watch out for that **{killer}**'s wav... Oh😐... Rest in peace **{name}** ({level}).", - lambda server, level, voc, killer, levels_lost: killer in ["dragon", "dragon lord", "undead dragon", "draken spellweaver", - "hellhound", "hellfire fighter"]], - [2000, "**{name}** ({level}) died to {killer_article}**{killer}**! Don't worry, {he_she} didn't have a soul anyway", - lambda server, level, voc, killer, levels_lost: killer == "souleater"], - [150, "Oh look at that, rest in peace **{name}** ({level}), ^that ^**{killer}** really got you. " - "Hope you get your level back.", - lambda server, level, voc, killer, levels_lost: levels_lost > 0] -] - -# Deaths by players -death_messages_player = [ - [100, "**{name}** ({level}) got rekt! **{killer}** ish pekay!"], - [100, "HALP **{killer}** is going around killing innocent **{name}** ({level})!"], - [100, "**{killer}** just put **{name}** ({level}) in the ground. Finally someone takes care of that."], - [100, "**{killer}** killed **{name}** ({level}) and on this day a thousand innocent souls are avenged."], - [100, "**{killer}** has killed **{name}** ({level}). What? He had it coming!"], - [100, "Next time stay away from **{killer}**, **{name}** ({level})."], - [100, "**{name}** ({level}) was murdered by **{killer}**! Did {he_she} deserved it? Only they know."], - [100, "**{killer}** killed **{name}** ({level}). Humans killing themselves, what a surprise. It just means less " - "work for us robots when we take over."], - [100, "**{name}** ({level}) got killed by **{killer}**. Humans are savages."], - [100, "HAHAHA **{name}** ({level}) was killed by **{killer}**! Ehhrm, I mean, ooh poor **{name}**, rest in peace."], - [100, "**{name}** ({level}) died in the hands of **{killer}**. Oh well, murder is like potato chips: you can't stop" - " with just one."], - [100, "Blood! Blood! Let the blood drip! **{name}** ({level}) was murdered by **{killer}**."], - [100, "Oh look at that! **{name}** ({level}) was killed by **{killer}**. I hope {he_she} gets {his_her} revenge."] -] - - -def format_message(message) -> str: - """##handles stylization of messages, uppercasing \TEXT/, lowercasing /text\ and title casing /Text/""" - upper = r'\\(.+?)/' - upper = re.compile(upper, re.MULTILINE + re.S) - lower = r'/(.+?)\\' - lower = re.compile(lower, re.MULTILINE + re.S) - title = r'/(.+?)/' - title = re.compile(title, re.MULTILINE + re.S) - skipproper = r'\^(.+?)\^(.+?)([a-zA-Z])' - skipproper = re.compile(skipproper, re.MULTILINE + re.S) - message = re.sub(upper, lambda m: m.group(1).upper(), message) - message = re.sub(lower, lambda m: m.group(1).lower(), message) - message = re.sub(title, lambda m: m.group(1).title(), message) - message = re.sub(skipproper, - lambda m: m.group(2) + m.group(3) if m.group(3).istitle() else m.group(1) + m.group(2) + m.group( - 3), message) - return message - - -def weighed_choice(server, choices, level: int, vocation: str = None, killer: str = None, levels_lost: int = 0) -> str: - """Makes weighed choices from message lists where [0] is a value representing the relative odds - of picking a message and [1] is the message string""" - - # Find the max range by adding up the weigh of every message in the list - # and purge out messages that dont fulfil the conditions - weight_range = 0 - _messages = [] - for message in choices: - if len(message) == 3 and not message[2](server, level, vocation, killer, levels_lost): - continue - weight_range = weight_range + (message[0] if not message[1] in last_messages else message[0] / 10) - _messages.append(message) - # Choose a random number - range_choice = random.randint(0, weight_range) - # Iterate until we find the matching message - range_pos = 0 - for message in _messages: - if range_pos <= range_choice < range_pos + (message[0] if not message[1] in last_messages else message[0] / 10): - last_messages.pop() - last_messages.insert(0, message[1]) - return message[1] - range_pos = range_pos + (message[0] if not message[1] in last_messages else message[0] / 10) - # This shouldn't ever happen... - print("Error in weighed_choice!") - return _messages[0][1] - - -def split_message(message: str, limit: int=2000): - """Splits a message into a list of messages if it exceeds limit. - - Messages are only split at new lines. - - Discord message limits: - Normal message: 2000 - Embed description: 2048 - Embed field name: 256 - Embed field value: 1024""" - if len(message) <= limit: - return [message] - else: - lines = message.splitlines() - new_message = "" - message_list = [] - for line in lines: - if len(new_message+line+"\n") <= limit: - new_message += line+"\n" +from utils.context import NabCtx +from utils.database import userDatabase, get_server_property, set_server_property +from utils.general import global_online_list, log, join_list, is_numeric, FIELD_VALUE_LIMIT, EMBED_LIMIT, \ + get_user_avatar +from utils.messages import weighed_choice, death_messages_player, death_messages_monster, format_message, \ + level_messages, split_message +from utils.pages import Pages, CannotPaginate, VocationPages +from utils.tibia import get_highscores, ERROR_NETWORK, tibia_worlds, get_world, get_character, get_voc_emoji, get_guild, \ + get_voc_abb, get_character_url, url_guild, \ + get_tibia_time_zone, NetworkError, Death, Character, HIGHSCORE_CATEGORIES, get_voc_abb_and_emoji, get_share_range, \ + World + + +class Tracking: + """Commands related to NabBot's tracking system.""" + + def __init__(self, bot: NabBot): + self.bot = bot + self.scan_deaths_task = self.bot.loop.create_task(self.scan_deaths()) + self.scan_online_chars_task = bot.loop.create_task(self.scan_online_chars()) + self.scan_highscores_task = bot.loop.create_task(self.scan_highscores()) + self.world_times = {} + + async def scan_deaths(self): + ################################################# + # Nezune's cave # + # Do not touch anything, enter at your own risk # + ################################################# + await self.bot.wait_until_ready() + while not self.bot.is_closed(): + try: + await asyncio.sleep(config.death_scan_interval) + if len(global_online_list) == 0: + await asyncio.sleep(0.5) + continue + # Pop last char in queue, reinsert it at the beginning + current_char = global_online_list.pop() + global_online_list.insert(0, current_char) + + # Check for new death + await self.check_death(current_char.name) + except asyncio.CancelledError: + # Task was cancelled, so this is fine + break + except Exception: + log.exception("Task: scan_deaths") + continue + + async def scan_highscores(self): + ################################################# + # Nezune's cave # + # Do not touch anything, enter at your own risk # + ################################################# + await self.bot.wait_until_ready() + while not self.bot.is_closed(): + if len(self.bot.tracked_worlds_list) == 0: + # If no worlds are tracked, just sleep, worlds might get registered later + await asyncio.sleep(config.highscores_delay) + continue + for world in self.bot.tracked_worlds_list: + if world not in tibia_worlds: + log.debug() + await asyncio.sleep(0.1) + try: + for category in HIGHSCORE_CATEGORIES: + # Check the last scan time, highscores are updated every server save + with closing(userDatabase.cursor()) as c: + c.execute("SELECT last_scan FROM highscores_times WHERE world = ? and category = ?", + (world, category,)) + result = c.fetchone() + if result: + last_scan = result["last_scan"] + last_scan_date = dt.datetime.utcfromtimestamp(last_scan).replace(tzinfo=dt.timezone.utc) + now = dt.datetime.now(dt.timezone.utc) + # Current day's server save, could be in the past or the future, an extra hour is added + # as margin + today_ss = dt.datetime.now(dt.timezone.utc).replace(hour=11 - get_tibia_time_zone()) + if not now > today_ss > last_scan_date: + continue + highscore_data = [] + for pagenum in range(1, 13): + # Special cases (ek/rp mls) + if category == "magic_ek": + scores = await get_highscores(world, "magic", pagenum, 1) + elif category == "magic_rp": + scores = await get_highscores(world, "magic", pagenum, 2) + else: + scores = await get_highscores(world, category, pagenum) + if scores == ERROR_NETWORK: + continue + for entry in scores: + highscore_data.append( + (entry["rank"], category, world, entry["name"], entry["vocation"], entry["value"])) + await asyncio.sleep(config.highscores_page_delay) + with userDatabase as conn: + # Delete old records + conn.execute("DELETE FROM highscores WHERE category = ? AND world = ?", (category, world,)) + # Add current entries + conn.executemany("INSERT INTO highscores(rank, category, world, name, vocation, value) " + "VALUES (?, ?, ?, ?, ?, ?)", highscore_data) + # These two executes are equal to an UPDATE OR INSERT + conn.execute("UPDATE highscores_times SET last_scan = ? WHERE world = ? AND category = ?", + (time.time(), world, category)) + conn.execute("INSERT INTO highscores_times(world, last_scan, category) SELECT ?,?,? WHERE " + "(SELECT Changes() = 0)", (world, time.time(), category)) + except asyncio.CancelledError: + # Task was cancelled, so this is fine + break + except Exception: + log.exception("Task: scan_highscores") + continue + await asyncio.sleep(10) + + async def scan_online_chars(self): + ################################################# + # Nezune's cave # + # Do not touch anything, enter at your own risk # + ################################################# + await self.bot.wait_until_ready() + try: + with open("data/online_list.dat", "rb") as f: + saved_list, timestamp = pickle.load(f) + if (time.time() - timestamp) < config.online_list_expiration: + global_online_list.clear() + global_online_list.extend(saved_list) + log.info("Loaded cached online list") + else: + log.info("Cached online list is too old, discarding") + except FileNotFoundError: + pass + except (ValueError, pickle.PickleError): + log.info("Couldn't read cached online list.") + pass + while not self.bot.is_closed(): + # Open connection to users.db + c = userDatabase.cursor() + try: + # Pop last server in queue, reinsert it at the beginning + current_world = tibia_worlds.pop() + tibia_worlds.insert(0, current_world) + + if current_world.capitalize() not in self.bot.tracked_worlds_list: + await asyncio.sleep(0.1) + continue + + if time.time() - self.world_times.get(current_world.capitalize(), 0) < config.online_scan_interval: + await asyncio.sleep(0.2) + continue + + # Get online list for this server + try: + world = await get_world(current_world) + if world is None: + await asyncio.sleep(0.1) + continue + except NetworkError: + await asyncio.sleep(0.1) + continue + current_world_online = world.players_online + if len(current_world_online) == 0: + await asyncio.sleep(0.1) + continue + self.world_times[world.name] = time.time() + self.bot.dispatch("world_scanned", world) + # Save the online list in file + with open("data/online_list.dat", "wb") as f: + pickle.dump((global_online_list, time.time()), f, protocol=pickle.HIGHEST_PROTOCOL) + # Remove chars that are no longer online from the global_online_list + offline_list = [] + for char in global_online_list: + if char.world not in tibia_worlds: + # Remove chars from worlds that no longer exist + offline_list.append(char) + elif char.world == current_world: + offline = True + for server_char in current_world_online: + if server_char.name == char.name: + offline = False + break + if offline: + offline_list.append(char) + for offline_char in offline_list: + global_online_list.remove(offline_char) + # Check for deaths and level ups when removing from online list + try: + name = offline_char.name + offline_char = await get_character(name, bot=self.bot) + except NetworkError: + log.error(f"scan_online_chars: Could not fetch {name}, NetWorkError") + continue + if offline_char is not None: + c.execute("SELECT name, level, id FROM chars WHERE name LIKE ?", (offline_char.name,)) + result = c.fetchone() + if result: + c.execute("UPDATE chars SET level = ? WHERE name LIKE ?", + (offline_char.level, offline_char.name)) + if offline_char.level > result["level"] > 0: + # Saving level up date in database + c.execute( + "INSERT INTO char_levelups (char_id,level,date) VALUES(?,?,?)", + (result["id"], offline_char.level, time.time(),) + ) + # Announce the level up + await self.announce_level(offline_char.level, char=offline_char) + await self.check_death(offline_char.name) + # Add new online chars and announce level differences + for server_char in current_world_online: + c.execute("SELECT name, level, id, user_id FROM chars WHERE name LIKE ?", + (server_char.name,)) + result = c.fetchone() + # If its a stalked character + if result: + # We update their last level in the db + c.execute( + "UPDATE chars SET level = ? WHERE name LIKE ?", + (server_char.level, server_char.name) + ) + if server_char not in global_online_list: + # If the character wasn't in the globalOnlineList we add them + # (We insert them at the beginning of the list to avoid messing with the death checks order) + global_online_list.insert(0, server_char) + await self.check_death(server_char.name) + # Else we check for levelup + elif server_char.level > result["level"] > 0: + # Saving level up date in database + c.execute( + "INSERT INTO char_levelups (char_id,level,date) VALUES(?,?,?)", + (result["id"], server_char.level, time.time(),) + ) + # Announce the level up + await self.announce_level(server_char.level, char_name=server_char.name) + except asyncio.CancelledError: + # Task was cancelled, so this is fine + break + except Exception: + log.exception("scan_online_chars") + continue + finally: + userDatabase.commit() + c.close() + + async def on_world_scanned(self, scanned_world: World): + # Watched List checking + # Iterate through servers with tracked world to find one that matches the current world + for server, world in self.bot.tracked_worlds.items(): + if world != scanned_world.name: + await asyncio.sleep(0.01) + continue + if self.bot.get_guild(server) is None: + await asyncio.sleep(0.01) + continue + watched_channel_id = get_server_property(server, "watched_channel", is_int=True) + if watched_channel_id is None: + # This server doesn't have watch list enabled + await asyncio.sleep(0.1) + continue + watched_channel: discord.TextChannel = self.bot.get_channel(watched_channel_id) + if watched_channel is None: + # This server's watched channel is not available to the bot anymore. + await asyncio.sleep(0.1) + continue + # Get watched list + entries = userDatabase.execute("SELECT * FROM watched_list WHERE server_id = ? " + "ORDER BY is_guild, name", (server,)) + if not entries: + await asyncio.sleep(0.1) + continue + # Online watched characters + currently_online = [] + # Watched guilds + guild_online = dict() + for watched in entries: + if watched["is_guild"]: + try: + guild = await get_guild(watched["name"]) + except NetworkError: + continue + # If the guild doesn't exist, add it as empty to show it was disbanded + if guild is None: + guild_online[watched["name"]] = None + continue + # If there's at least one member online, add guild to list + if len(guild.online): + guild_online[guild.name] = guild.online + # If it is a character, check if he's in the online list + for online_char in scanned_world.players_online: + if online_char.name == watched["name"]: + # Add to online list + currently_online.append(online_char) + watched_message_id = get_server_property(server, "watched_message", is_int=True) + # We try to get the watched message, if the bot can't find it, we just create a new one + # This may be because the old message was deleted or this is the first time the list is checked + try: + watched_message = await watched_channel.get_message(watched_message_id) + except discord.HTTPException: + watched_message = None + items = [f"\t{x.name} - Level {x.level} {get_voc_emoji(x.vocation)}" for x in currently_online] + online_count = len(items) + if len(items) > 0 or len(guild_online.keys()) > 0: + description = "" + content = "\n".join(items) + for guild, members in guild_online.items(): + content += f"\nGuild: **{guild}**\n" + if members is None: + content += "\t*Guild was disbanded.*" + continue + content += "\n".join( + [f"\t{x['name']} - Level {x['level']} {get_voc_emoji(x['vocation'])}" + for x in members]) + online_count += len(members) + else: + description = "There are no watched characters online." + content = "" + # Send new watched message or edit last one + embed = discord.Embed(description=description) + embed.set_footer(text="Last updated") + embed.timestamp = dt.datetime.utcnow() + if content: + if len(content) >= EMBED_LIMIT - 50: + content = split_message(content, EMBED_LIMIT - 50)[0] + content += "\n*And more...*" + fields = split_message(content, FIELD_VALUE_LIMIT) + for s, split_field in enumerate(fields): + name = "Watched List" if s == 0 else "\u200F" + embed.add_field(name=name, value=split_field, inline=False) + try: + if watched_message is None: + new_watched_message = await watched_channel.send(embed=embed) + set_server_property(server, "watched_message", new_watched_message.id) + else: + await watched_message.edit(embed=embed) + await watched_channel.edit(name=f"{watched_channel.name.split('Β·', 1)[0]}Β·{online_count}") + except discord.HTTPException: + pass + + async def check_death(self, character): + """Checks if the player has new deaths""" + try: + char = await get_character(character, bot=self.bot) + if char is None: + # During server save, characters can't be read sometimes + return + except NetworkError: + log.warning("check_death: couldn't fetch {0}".format(character)) + return + c = userDatabase.cursor() + c.execute("SELECT name, id FROM chars WHERE name LIKE ?", (character,)) + result = c.fetchone() + if result is None: + return + char_id = result["id"] + pending_deaths = [] + for death in char.deaths: + death_time = death.time.timestamp() + # Check if we have a death that matches the time + c.execute("SELECT * FROM char_deaths " + "WHERE char_id = ? AND date >= ? AND date <= ? AND level = ? AND killer LIKE ?", + (char_id, death_time - 20, death_time + 20, death.level, death.killer)) + result = c.fetchone() + if result is not None: + # We already have this death, we're assuming we already have older deaths + break + pending_deaths.append(death) + c.close() + + # Announce and save deaths from older to new + for death in reversed(pending_deaths): + with userDatabase as con: + con.execute("INSERT INTO char_deaths(char_id, level, killer, byplayer, date) VALUES(?,?,?,?,?)", + (char_id, death.level, death.killer, death.by_player, death.time.timestamp())) + if time.time() - death.time.timestamp() >= (30 * 60): + log.info("Death detected, too old to announce: {0}({1.level}) | {1.killer}".format(character, death)) else: - message_list.append(new_message) - new_message = "" - if new_message: - message_list.append(new_message) - return message_list - - -async def send_messageEx(bot, dest, message, embed=False): - message = split_message(message) - for msg in message: - if embed: - msg_embed = discord.Embed() - msg_embed.description = msg - await bot.send_message(dest, embed=msg_embed) + await self.announce_death(death, max(death.level - char.level, 0), char) + + async def announce_death(self, death: Death, levels_lost=0, char: Character = None, char_name: str = None): + """Announces a level up on the corresponding servers""" + # Don't announce for low level players + if char is None: + if char_name is None: + log.error("announce_death: no character or character name passed.") + return + try: + char = await get_character(char_name, bot=self.bot) + except NetworkError: + log.warning("announce_death: couldn't fetch character (" + char_name + ")") + return + + log.info("Announcing death: {0.name}({1.level}) | {1.killer}".format(char, death)) + + # Find killer article (a/an) + killer_article = "" + if not death.by_player: + killer_article = death.killer.split(" ", 1) + if killer_article[0] in ["a", "an"] and len(killer_article) > 1: + death.killer = killer_article[1] + killer_article = killer_article[0] + " " + else: + killer_article = "" + + # Select a message + if death.by_player: + message = weighed_choice(self.ctx, death_messages_player, vocation=char.vocation, level=death.level, + levels_lost=levels_lost) + elif death.killer in ["death", "energy", "earth", "fire", "Pit Battler", "Pit Berserker", "Pit Blackling", + "Pit Brawler", "Pit Condemned", "Pit Demon", "Pit Destroyer", "Pit Fiend", + "Pit Groveller", "Pit Grunt", "Pit Lord", "Pit Maimer", "Pit Overlord", "Pit Reaver", + "Pit Scourge"] and levels_lost == 0: + # Skip element damage deaths unless player lost a level to avoid spam from arena deaths + # This will cause a small amount of deaths to not be announced but it's probably worth the tradeoff (ty selken) + return else: - await bot.send_message(dest, msg) - - -def html_to_markdown(html_string): - """Converts some html tags to markdown equivalent""" - # Carriage return - html_string = html_string.replace("\r", "") - # Replace
    tags with line jumps - html_string = re.sub(r'', "\n", html_string) - # Replace and with bold - html_string = re.sub(r'([^<]+)', '**\g<1>**', html_string) - html_string = re.sub(r'([^<]+)', '**\g<1>**', html_string) - html_string = re.sub(r'
  • ([^<]+)
  • ', '- \g<1>\n', html_string) - # Replace links - html_string = re.sub(r']+>([^<]+)', "[\g<2>](\g<1>)", html_string) - # Paragraphs with jumpline - html_string = re.sub(r'

    ([^<]+)

    ', "\g<1>\n", html_string) - # Replace youtube embeds with link to youtube - html_string = re.sub(r'', "[YouTube](\g<1>)", html_string) - # Remove leftover html tags - html_string = re.sub(r'<[^>]+>', "", html_string) - html_string = html_string.replace("\n\n", "\n") - return html_string - - -def get_first_image(content): - """Returns a url to the first image found in a html string.""" - matches = re.findall(r'', content) - for match in matches: - match_src = re.search(r'src="([^"]+)', match) - if match_src: - return match_src.group(1) - return None - - -class TabularData: - def __init__(self): - self._widths = [] - self._columns = [] - self._rows = [] - - def set_columns(self, columns): - self._columns = columns - self._widths = [len(c) + 2 for c in columns] - - def add_row(self, row): - rows = [str(r) for r in row] - self._rows.append(rows) - for index, element in enumerate(rows): - width = len(element) + 2 - if width > self._widths[index]: - self._widths[index] = width - - def add_rows(self, rows): - for row in rows: - self.add_row(row) - - def render(self): - """Renders a table in rST format. - Example: - +-------+-----+ - | Name | Age | - +-------+-----+ - | Alice | 24 | - | Bob | 19 | - +-------+-----+ + message = weighed_choice(self.ctx, death_messages_monster, vocation=char.vocation, level=death.level, + levels_lost=levels_lost, killer=death.killer) + # Format message with death information + death_info = {'name': char.name, 'level': death.level, 'killer': death.killer, 'killer_article': killer_article, + 'he_she': char.he_she.lower(), 'his_her': char.his_her.lower(), 'him_her': char.him_her.lower()} + message = message.format(**death_info) + # Format extra stylization + message = f"{config.pvpdeath_emoji if death.by_player else config.death_emoji} {format_message(message)}" + + for guild_id, tracked_world in self.bot.tracked_worlds.items(): + guild = self.bot.get_guild(guild_id) + if guild is None: + continue + min_level = get_server_property(guild_id, "announce_level", is_int=True, default=config.announce_threshold) + if death.level < min_level: + continue + if char.world == tracked_world and guild.get_member(char.owner) is not None: + try: + channel = self.bot.get_channel_or_top(guild, + get_server_property(guild.id, "levels_channel", is_int=True)) + await channel.send(message[:1].upper() + message[1:]) + except discord.Forbidden: + log.warning("announce_death: Missing permissions.") + except discord.HTTPException: + log.warning("announce_death: Malformed message.") + + async def announce_level(self, level, char_name: str = None, char: Character = None): + """Announces a level up on corresponding servers + + One of these must be passed: + char is a character dictionary + char_name is a character's name + + If char_name is passed, the character is fetched here.""" + if char is None: + if char_name is None: + log.error("announce_level: no character or character name passed.") + return + try: + char = await get_character(char_name, bot=self.bot) + if char is None: + log.warning("announce_level: couldn't fetch character (" + char_name + ")") + return + except NetworkError: + log.warning("announce_level: couldn't fetch character (" + char_name + ")") + return + + log.info("Announcing level up: {0} ({1})".format(char.name, level)) + + for server_id, tracked_world in self.bot.tracked_worlds.items(): + server = self.bot.get_guild(server_id) + if server is None: + continue + min_level = get_server_property(server_id, "announce_level", is_int=True, default=config.announce_threshold) + if char.level < min_level: + continue + if char.world == tracked_world and server.get_member(char.owner) is not None: + try: + channel = self.bot.get_channel_or_top(server, + get_server_property(server.id, "levels_channel", is_int=True)) + # Select a message + message = weighed_choice(server, level_messages, vocation=char.vocation, level=level) + level_info = {'name': char.name, 'level': level, 'he_she': char.he_she.lower(), 'his_her': char.his_her.lower(), + 'him_her': char.him_her.lower()} + # Format message with level information + message = message.format(**level_info) + # Format extra stylization + message = f"{config.levelup_emoji} {format_message(message)}" + await channel.send(message) + except discord.Forbidden: + log.warning("announce_level: Missing permissions.") + except discord.HTTPException: + log.warning("announce_level: Malformed message.") + + # Commands + @commands.command() + @checks.is_in_tracking_world() + async def claim(self, ctx: NabCtx, *, char_name: str = None): + """Claims a character registered as yours. + + Claims a character as yours, even if it is already registered to someone else. + + In order for this to work, you have to put a special code in the character's comment. + You can see this code by using the command with no parameters. The code looks like this: `/NB-23FC13AC7400000/` + + Once you had set the code, you can use the command with that character, if the code matches, it will be reassigned to you. + Note that it may take some time for the code to be visible to NabBot because of caching. + + This code is unique for your discord user, so the code will only work for your discord account and no one else. + No one can claim a character of yours unless you put **their** code on your character's comment. """ + user = ctx.author + claim_pattern = re.compile(r"/NB-([^/]+)/") + user_code = hex(user.id)[2:].upper() + + # List of servers the user shares with the self.bot + user_guilds = self.bot.get_user_guilds(user.id) + # List of Tibia worlds tracked in the servers the user is + user_tibia_worlds = [world for guild, world in self.bot.tracked_worlds.items() if + guild in [g.id for g in user_guilds]] + # Remove duplicate entries from list + user_tibia_worlds = list(set(user_tibia_worlds)) + + if not ctx.is_private and self.bot.tracked_worlds.get(ctx.guild.id) is None: + return await ctx.send("This server is not tracking any tibia worlds.") + + if len(user_tibia_worlds) == 0: + return + + if char_name is None: + await ctx.send(f"To use this command, add `/NB-{user_code}/` to the comment of the character you want to" + f"claim, and then use `/claim character_name`.") + return + + await ctx.trigger_typing() + try: + char = await get_character(char_name) + if char is None: + await ctx.send("That character doesn't exist.") + return + except NetworkError: + await ctx.send("I couldn't fetch the character, please try again.") + return + match = claim_pattern.search(char.comment if char.comment is not None else "") + if not match: + await ctx.send(f"Couldn't find verification code on character's comment.\n" + f"Add `/NB-{user_code}/` to the comment to authenticate.") + return + code = match.group(1) + if code != user_code: + await ctx.send(f"The verification code on the character's comment doesn't match yours.\n" + f"Use `/NB-{user_code}/` to authenticate.") + return + + chars = char.other_characters + check_other = False + if len(chars) > 1: + message = await ctx.send("Do you want to attempt to add the other visible characters in this account?") + check_other = await ctx.react_confirm(message, timeout=60, delete_after=True) + if check_other is None: + return await ctx.send("You ran out of time, try again." + "Remember you have to react or click on the reactions.") + if not check_other: + chars = [char] + + skipped = [] + updated = [] + added: List[Character] = [] + existent = [] + with ctx.typing(): + for char in chars: + # Skip chars in non-tracked worlds + if char.world not in user_tibia_worlds: + skipped.append(char) + continue + with closing(userDatabase.cursor()) as c: + c.execute("SELECT name, guild, user_id as owner, vocation, ABS(level) as level, guild FROM chars " + "WHERE name LIKE ?", (char.name,)) + db_char = c.fetchone() + if db_char is not None: + owner = self.bot.get_member(db_char["owner"]) + # Char already registered to this user + if owner.id == user.id: + existent.append("{0.name} ({0.world})".format(char)) + continue + else: + updated.append({'name': char.name, 'world': char.world, 'prevowner': db_char["owner"], + 'vocation': db_char["vocation"], 'level': db_char['level'], + 'guild': db_char['guild'] + }) + # If we only have one char, it already contains full data + if len(chars) > 1: + try: + await ctx.channel.trigger_typing() + char = await get_character(char.name) + except NetworkError: + await ctx.send("I'm having network troubles, please try again.") + return + if char.deleted is not None: + skipped.append(char) + continue + added.append(char) + + if len(skipped) == len(chars): + reply = "Sorry, I couldn't find any characters from the servers I track ({0})." + await ctx.send(reply.format(join_list(user_tibia_worlds, ", ", " and "))) + return + + reply = "" + log_reply = dict().fromkeys([server.id for server in user_guilds], "") + if len(existent) > 0: + reply += "\nThe following characters were already registered to you: {0}" \ + .format(join_list(existent, ", ", " and ")) + + if len(added) > 0: + reply += "\nThe following characters were added to your account: {0}" \ + .format(join_list(["{0.name} ({0.world})".format(c) for c in added], ", ", " and ")) + for char in added: + log.info("Character {0} was assigned to {1.display_name} (ID: {1.id})".format(char.name, user)) + # Announce on server log of each server + for guild in user_guilds: + # Only announce on worlds where the character's world is tracked + if self.bot.tracked_worlds.get(guild.id, None) == char.world: + _guild = "No guild" if char.guild is None else char.guild_name + voc = get_voc_abb_and_emoji(char.vocation) + log_reply[guild.id] += "\n\u2023 {1.name} - Level {1.level} {2} - **{0}**" \ + .format(_guild, char, voc) + + if len(updated) > 0: + reply += "\nThe following characters were reassigned to you: {0}" \ + .format(join_list(["{name} ({world})".format(**c) for c in updated], ", ", " and ")) + for char in updated: + log.info("Character {0} was reassigned to {1.display_name} (ID: {1.id})".format(char['name'], user)) + # Announce on server log of each server + for guild in user_guilds: + # Only announce on worlds where the character's world is tracked + if self.bot.tracked_worlds.get(guild.id, None) == char["world"]: + char["voc"] = get_voc_abb_and_emoji(char["vocation"]) + if char["guild"] is None: + char["guild"] = "No guild" + log_reply[guild.id] += "\n\u2023 {name} - Level {level} {voc} - **{guild}** (Reassigned)". \ + format(**char) + + for char in updated: + with userDatabase as conn: + conn.execute("UPDATE chars SET user_id = ? WHERE name LIKE ?", (user.id, char['name'])) + for char in added: + with userDatabase as conn: + conn.execute("INSERT INTO chars (name,level,vocation,user_id, world, guild) VALUES (?,?,?,?,?,?)", + (char.name, char.level * -1, char.vocation, user.id, char.world, + char.guild_name) + ) + + with userDatabase as conn: + conn.execute("INSERT OR IGNORE INTO users (id, name) VALUES (?, ?)", (user.id, user.display_name,)) + conn.execute("UPDATE users SET name = ? WHERE id = ?", (user.display_name, user.id,)) + + await ctx.send(reply) + for server_id, message in log_reply.items(): + if message: + message = user.mention + " registered the following characters: " + message + embed = discord.Embed(description=message) + embed.set_author(name=f"{user.name}#{user.discriminator}", icon_url=get_user_avatar(user)) + embed.colour = discord.Colour.dark_teal() + await self.bot.send_log_message(self.bot.get_guild(server_id), embed=embed) + + @checks.is_in_tracking_world() + @commands.command(aliases=["i'm", "iam"]) + async def im(self, ctx: NabCtx, *, char_name: str): + """Lets you add your tibia character(s) for the bot to track. + + If there are other visible characters, the bot will ask for confirmation to add them too. + + Characters in other worlds other than the currently tracked world are skipped. + If it finds a character owned by another user, the whole process will be stopped. + + If a character is already registered to someone else, `claim` can be used.""" + user = ctx.author + # List of servers the user shares with the bot + user_guilds = self.bot.get_user_guilds(user.id) + # List of Tibia worlds tracked in the servers the user is + user_tibia_worlds = [world for guild, world in self.bot.tracked_worlds.items() if + guild in [g.id for g in user_guilds]] + # Remove duplicate entries from list + user_tibia_worlds = list(set(user_tibia_worlds)) + + if not ctx.is_private and ctx.world is None: + return await ctx.send("This server is not tracking any tibia worlds.") + + if len(user_tibia_worlds) == 0: + return + + msg = await ctx.send(f"{config.loading_emoji} Fetching character...") + try: + char = await get_character(char_name) + if char is None: + return await msg.edit(content="That character doesn't exist.") + except NetworkError: + return await msg.edit(content="I couldn't fetch the character, please try again.") + chars = char.other_characters + check_other = False + if len(chars) > 1: + message = await ctx.send("Do you want to attempt to add the other visible characters in this account?") + check_other = await ctx.react_confirm(message, timeout=60, delete_after=True) + if check_other is None: + return await ctx.send("You didn't reply in time, try again." + "Remember that you have to react or click on the icons.") + if not check_other: + chars = [char] + + if check_other: + await msg.delete() + msg = await ctx.send(f"{config.loading_emoji} Fetching characters...") + + skipped = [] + updated = [] + added: List[Character] = [] + existent = [] + for char in chars: + # Skip chars in non-tracked worlds + if char.world not in user_tibia_worlds: + skipped.append(char) + continue + with closing(userDatabase.cursor()) as c: + c.execute("SELECT name, guild, user_id as owner, vocation, ABS(level) as level, guild " + "FROM chars " + "WHERE name LIKE ?", (char.name,)) + db_char = c.fetchone() + if db_char is not None: + owner = self.bot.get_member(db_char["owner"]) + # Previous owner doesn't exist anymore + if owner is None: + updated.append({'name': char.name, 'world': char.world, 'prevowner': db_char["owner"], + 'vocation': db_char["vocation"], 'level': db_char['level'], + 'guild': db_char['guild'] + }) + continue + # Char already registered to this user + elif owner.id == user.id: + existent.append("{0.name} ({0.world})".format(char)) + continue + # Character is registered to another user, we stop the whole process + else: + reply = "Sorry, a character in that account ({0}) is already registered to **{1}**.\n" \ + "If the character really belongs to you, try using `{2}claim {0}`." + return await msg.edit(content=reply.format(db_char["name"], owner, ctx.clean_prefix)) + # If we only have one char, it already contains full data + if len(chars) > 1: + try: + char = await get_character(char.name) + except NetworkError: + return await msg.edit("I'm having network issues, please try again.") + if char.deleted is not None: + skipped.append(char) + continue + added.append(char) + + if len(skipped) == len(chars): + reply = "Sorry, I couldn't find any characters from the servers I track ({0})." + return await msg.edit(content=reply.format(join_list(user_tibia_worlds, ", ", " and "))) + + reply = "" + log_reply = dict().fromkeys([server.id for server in user_guilds], "") + if len(existent) > 0: + reply += "\nThe following characters were already registered to you: {0}" \ + .format(join_list(existent, ", ", " and ")) + + if len(added) > 0: + reply += "\nThe following characters are now registered to you: {0}" \ + .format(join_list(["{0.name} ({0.world})".format(c) for c in added], ", ", " and ")) + for char in added: + log.info("Character {0} was assigned to {1.display_name} (ID: {1.id})".format(char.name, user)) + # Announce on server log of each server + for guild in user_guilds: + # Only announce on worlds where the character's world is tracked + if self.bot.tracked_worlds.get(guild.id, None) == char.world: + _guild = "No guild" if char.guild is None else char.guild_name + voc = get_voc_abb_and_emoji(char.vocation) + log_reply[guild.id] += "\n\u2023 {1.name} - Level {1.level} {2} - **{0}**" \ + .format(_guild, char, voc) + + if len(updated) > 0: + reply += "\nThe following characters were reassigned to you: {0}" \ + .format(join_list(["{name} ({world})".format(**c) for c in updated], ", ", " and ")) + for char in updated: + log.info("Character {0} was reassigned to {1.display_name} (ID: {1.id})".format(char['name'], user)) + # Announce on server log of each server + for guild in user_guilds: + # Only announce on worlds where the character's world is tracked + if self.bot.tracked_worlds.get(guild.id, None) == char["world"]: + char["voc"] = get_voc_abb_and_emoji(char["vocation"]) + if char["guild"] is None: + char["guild"] = "No guild" + log_reply[guild.id] += "\n\u2023 {name} - Level {level} {voc} - **{guild}** (Reassigned)". \ + format(**char) + + for char in updated: + with userDatabase as conn: + conn.execute("UPDATE chars SET user_id = ? WHERE name LIKE ?", (user.id, char['name'])) + for char in added: + with userDatabase as conn: + conn.execute("INSERT INTO chars (name,level,vocation,user_id, world, guild) VALUES (?,?,?,?,?,?)", + (char.name, char.level * -1, char.vocation, user.id, char.world, + char.guild_name) + ) + + with userDatabase as conn: + conn.execute("INSERT OR IGNORE INTO users (id, name) VALUES (?, ?)", (user.id, user.display_name,)) + conn.execute("UPDATE users SET name = ? WHERE id = ?", (user.display_name, user.id,)) + await msg.edit(content=reply) + for server_id, message in log_reply.items(): + if message: + guild = self.bot.get_guild(server_id) + message = user.mention + " registered the following characters: " + message + embed = discord.Embed(description=message) + embed.set_author(name=f"{user.name}#{user.discriminator}", icon_url=get_user_avatar(user)) + embed.colour = discord.Colour.dark_teal() + await self.bot.send_log_message(guild, embed=embed) + self.bot.dispatch("character_change", ctx.author.id) + + @checks.is_in_tracking_world() + @commands.command(aliases=["i'mnot"]) + async def imnot(self, ctx: NabCtx, *, name): + """Removes a character assigned to you. + + All registered level ups and deaths will be lost forever.""" + c = userDatabase.cursor() + try: + c.execute("SELECT id, name, ABS(level) as level, user_id, vocation, world, guild " + "FROM chars WHERE name LIKE ?", (name,)) + char = c.fetchone() + if char is None or char["user_id"] == 0: + await ctx.send("There's no character registered with that name.") + return + user = ctx.author + if char["user_id"] != user.id: + await ctx.send("The character **{0}** is not registered to you.".format(char["name"])) + return + + message = await ctx.send("Are you sure you want to unregister **{name}** ({level} {vocation})?" + .format(**char)) + confirm = await ctx.react_confirm(message, timeout=50) + if confirm is None: + await ctx.send("I guess you changed your mind.") + return + if not confirm: + await ctx.send("No then? Ok.") + + c.execute("UPDATE chars SET user_id = 0 WHERE id = ?", (char["id"],)) + await ctx.send("**{0}** is no longer registered to you.".format(char["name"])) + + user_servers = [s.id for s in self.bot.get_user_guilds(user.id)] + for server_id, world in self.bot.tracked_worlds.items(): + if char["world"] == world and server_id in user_servers: + if char["guild"] is None: + char["guild"] = "No guild" + message = "{0} unregistered:\n\u2023 **{1}** - Level {2} {3} - {4}". \ + format(user.mention, char["name"], char["level"], get_voc_abb_and_emoji(char["vocation"]), + char["guild"]) + embed = discord.Embed(description=message) + embed.set_author(name=f"{user.name}#{user.discriminator}", icon_url=get_user_avatar(user)) + embed.colour = discord.Colour.dark_teal() + await self.bot.send_log_message(self.bot.get_guild(server_id), embed=embed) + self.bot.dispatch("character_change", ctx.author.id) + finally: + userDatabase.commit() + c.close() + + @commands.command() + @checks.is_tracking_world() + async def online(self, ctx: NabCtx): + """Tells you which users are online on Tibia. + + This list gets updated based on Tibia.com online list, so it takes a couple minutes to be updated.""" + world = self.bot.tracked_worlds.get(ctx.guild.id) + + per_page = 20 if ctx.long else 5 + c = userDatabase.cursor() + now = dt.datetime.utcnow() + uptime = (now - self.bot.start_time).total_seconds() + count = 0 + entries = [] + vocations = [] + try: + for char in global_online_list: + char_world = char.world + name = char.name + c.execute("SELECT name, user_id, vocation, ABS(level) as level FROM chars WHERE name LIKE ?", (name,)) + row = c.fetchone() + if row is None: + continue + if char_world != world: + continue + # Skip characters of members not in the server + owner = ctx.guild.get_member(row["user_id"]) + if owner is None: + continue + row["owner"] = owner.display_name + row['emoji'] = get_voc_emoji(row['vocation']) + vocations.append(row["vocation"]) + row['vocation'] = get_voc_abb(row['vocation']) + entries.append("{name} (Lvl {level} {vocation}{emoji}, **@{owner}**)".format(**row)) + count += 1 + + if count == 0: + if uptime < 90: + await ctx.send("I just started, give me some time to check online lists...βŒ›") + else: + await ctx.send("There is no one online from Discord.") + return + pages = VocationPages(ctx, entries=entries, vocations=vocations, per_page=per_page) + pages.embed.title = "Users online" + try: + await pages.paginate() + except CannotPaginate as e: + await ctx.send(e) + finally: + c.close() + + @commands.command(name="searchteam", aliases=["whereteam", "findteam"], usage="") + @checks.is_tracking_world() + async def search_team(self, ctx: NabCtx, *, params=None): + """Searches for a registered character that meets the criteria + + There are 3 ways to use this command: + + - Show characters in share range with a specific character. (`searchteam `) + - Show characters in share range with a specific level. (`searchteam `) + - Show characters in a level range. (`searchteam ,`) + + Online characters are shown first on the list, they also have an icon.""" + permissions = ctx.bot_permissions + if not permissions.embed_links: + await ctx.send("Sorry, I need `Embed Links` permission for this command.") + return + + invalid_arguments = "Invalid arguments used, examples:\n" \ + "```/searchteam charname\n" \ + "/searchteam level\n" \ + "/searchteam minlevel,maxlevel```" + + tracked_world = self.bot.tracked_worlds.get(ctx.guild.id) + if tracked_world is None: + await ctx.send("This server is not tracking any tibia worlds.") + return + + if params is None: + await ctx.send(invalid_arguments) + return + + entries = [] + vocations = [] + online_entries = [] + online_vocations = [] + + per_page = 20 if ctx.long else 5 + + char = None + params = params.split(",") + if len(params) < 1 or len(params) > 2: + await ctx.send(invalid_arguments) + return + + # params[0] could be a character's name, a character's level or one of the level ranges + # If it's not a number, it should be a player's name + if not is_numeric(params[0]): + # We shouldn't have another parameter if a character name was specified + if len(params) == 2: + await ctx.send(invalid_arguments) + return + try: + char = await get_character(params[0]) + if char is None: + await ctx.send("I couldn't find a character with that name.") + return + except NetworkError: + await ctx.send("I couldn't fetch that character.") + return + low, high = get_share_range(char.level) + title = "Characters in share range with {0}({1}-{2}):".format(char.name, low, high) + empty = "I didn't find anyone in share range with **{0}**({1}-{2})".format(char.name, low, high) + else: + # Check if we have another parameter, meaning this is a level range + if len(params) == 2: + try: + level1 = int(params[0]) + level2 = int(params[1]) + except ValueError: + await ctx.send(invalid_arguments) + return + if level1 <= 0 or level2 <= 0: + await ctx.send("You entered an invalid level.") + return + low = min(level1, level2) + high = max(level1, level2) + title = "Characters between level {0} and {1}".format(low, high) + empty = "I didn't find anyone between levels **{0}** and **{1}**".format(low, high) + # We only got a level, so we get the share range for it + else: + if int(params[0]) <= 0: + await ctx.send("You entered an invalid level.") + return + low, high = get_share_range(int(params[0])) + title = "Characters in share range with level {0} ({1}-{2})".format(params[0], low, high) + empty = "I didn't find anyone in share range with level **{0}** ({1}-{2})".format(params[0], + low, high) + + c = userDatabase.cursor() + try: + c.execute("SELECT name, user_id, ABS(level) as level, vocation FROM chars " + "WHERE level >= ? AND level <= ? AND world = ?" + "ORDER by level DESC", (low, high, tracked_world,)) + count = 0 + online_list = [x.name for x in global_online_list] + while True: + player = c.fetchone() + if player is None: + break + # Do not show the same character that was searched for + if char is not None and char.name == player["name"]: + continue + owner = self.bot.get_member(player["user_id"], ctx.guild) + # If the owner is not in server, skip + if owner is None: + continue + count += 1 + player["owner"] = owner.display_name + player["online"] = "" + player["emoji"] = get_voc_emoji(player["vocation"]) + player["voc"] = get_voc_abb(player["vocation"]) + line_format = "**{name}** - Level {level} {voc}{emoji} - @**{owner}** {online}" + if player["name"] in online_list: + player["online"] = config.online_emoji + online_entries.append(line_format.format(**player)) + online_vocations.append(player["vocation"]) + else: + entries.append(line_format.format(**player)) + vocations.append(player["vocation"]) + + if count < 1: + await ctx.send(empty) + return + finally: + c.close() + pages = VocationPages(ctx, entries=online_entries + entries, per_page=per_page, + vocations=online_vocations + vocations) + pages.embed.title = title + try: + await pages.paginate() + except CannotPaginate as e: + await ctx.send(e) + + @checks.is_admin() + @checks.is_tracking_world() + @commands.group(invoke_without_command=True, aliases=["watchlist", "huntedlist"], case_insensitive=True) + async def watched(self, ctx: NabCtx, *, name="watched-list"): + """Sets the watched list channel for this server + + Creates a new text channel for the watched list to be posted. + + The watch list shows which characters from it are online. Entire guilds can be added too. + + If no name is specified, the default name "watched-list" is used. + + When the channel is created, only the bot and people with `Administrator` role can read it. + The permissions can be adjusted afterwards. + + The channel can be renamed at anytime without problems. + """ + + watched_channel_id = get_server_property(ctx.guild.id, "watched_channel", is_int=True) + watched_channel = self.bot.get_channel(watched_channel_id) + + if "Β·" in name: + await ctx.send("Channel name cannot contain the special character **Β·**") + return + + world = self.bot.tracked_worlds.get(ctx.guild.id, None) + if world is None: + await ctx.send("This server is not tracking any tibia worlds.") + return + + if watched_channel is not None: + await ctx.send(f"This server already has a watched list channel: {watched_channel.mention}") + return + permissions = ctx.bot_permissions + if not permissions.manage_channels: + await ctx.send("I need to have `Manage Channels` permissions to use this command.") + try: + overwrites = { + ctx.guild.default_role: discord.PermissionOverwrite(read_messages=False), + ctx.guild.me: discord.PermissionOverwrite(read_messages=True) + } + channel = await ctx.guild.create_text_channel(name, overwrites=overwrites) + except discord.Forbidden: + await ctx.send("Sorry, I don't have permissions to create channels.") + except discord.HTTPException: + await ctx.send("Something went wrong, the channel name you chose is probably invalid.") + else: + await ctx.send(f"Channel created successfully: {channel.mention}\n") + await channel.send("This is where I will post a list of online watched characters." + "Right now only **admins** are able to read this.\n" + "Edit this channel's permissions to allow the roles you want.\n" + "This channel can be renamed freely.\n" + "**It is important to not allow anyone to write in here**\n" + "*This message can be deleted now.*") + set_server_property(ctx.guild.id, "watched_channel", channel.id) + + @checks.is_mod() + @checks.is_tracking_world() + @watched.command(name="add", aliases=["addplayer", "addchar"], usage="[,reason]") + async def watched_add(self, ctx: NabCtx, *, params=None): + """Adds a character to the watched list. + + A reason can be specified by adding it after the character's name, separated by a comma.""" + + if params is None: + await ctx.send("You need to tell me the name of the person you want to add to the list.\n" + "You can also specify a reason, e.g. `/watched add player,reason`") + return + + params = params.split(",", 1) + name = params[0] + reason = None + if len(params) > 1: + reason = params[1] + + world = self.bot.tracked_worlds.get(ctx.guild.id, None) + if world is None: + await ctx.send("This server is not tracking any tibia worlds.") + return + + try: + char = await get_character(name) + if char is None: + await ctx.send("There's no character with that name.") + return + except NetworkError: + await ctx.send("I couldn't fetch that character right now, please try again.") + return + + if char.world != world: + await ctx.send(f"This character is not in **{world}**.") + return + c = userDatabase.cursor() + try: + c.execute("SELECT * FROM watched_list WHERE server_id = ? AND name LIKE ? and is_guild = 0", + (ctx.guild.id, char.name)) + result = c.fetchone() + if result is not None: + await ctx.send("This character is already in the watched list.") + return + + message = await ctx.send("Do you want to add **{0.name}** (Level {0.level} {0.vocation}) to the " + "watched list? ".format(char)) + confirm = await ctx.react_confirm(message) + if confirm is None: + await ctx.send("You took too long!") + return + if not confirm: + await ctx.send("Ok then, guess you changed your mind.") + return + + c.execute("INSERT INTO watched_list(name, server_id, is_guild, reason, author, added) " + "VALUES(?, ?, 0, ?, ?, ?)", + (char.name, ctx.guild.id, reason, ctx.author.id, time.time())) + await ctx.send("Character added to the watched list.") + finally: + userDatabase.commit() + c.close() + + @checks.is_mod() + @checks.is_tracking_world() + @watched.command(name="addguild", usage="[,reason]") + async def watched_addguild(self, ctx: NabCtx, *, params=None): + """Adds an entire guild to the watched list. + + Guilds are displayed in the watched list as a group.""" + if params is None: + ctx.send("You need to tell me the name of the guild you want to add.\n" + "You can optionally provide a reason, e.g. `/watched addguild guild,reason`") + return + + params = params.split(",", 1) + name = params[0] + reason = None + if len(params) > 1: + reason = params[1] + + world = self.bot.tracked_worlds.get(ctx.guild.id, None) + if world is None: + await ctx.send("This server is not tracking any tibia worlds.") + return + + try: + guild = await get_guild(name) + if guild is None: + await ctx.send("There's no guild with that name.") + return + except NetworkError: + await ctx.send("I couldn't fetch that guild right now, please try again.") + return + + if guild.world != world: + await ctx.send(f"This guild is not in **{world}**.") + return + c = userDatabase.cursor() + try: + c.execute("SELECT * FROM watched_list WHERE server_id = ? AND name LIKE ? and is_guild = 1", + (ctx.guild.id, guild.name)) + result = c.fetchone() + if result is not None: + await ctx.send("This guild is already in the watched list.") + return + + message = await ctx.send(f"Do you want to add the guild **{guild.name}** to the watched list?") + confirm = await ctx.react_confirm(message) + if confirm is None: + await ctx.send("You took too long!") + return + if not confirm: + await ctx.send("Ok then, guess you changed your mind.") + return + + c.execute("INSERT INTO watched_list(name, server_id, is_guild, reason, author, added)" + "VALUES(?, ?, 1, ?, ?, ?)", (guild.name, ctx.guild.id, reason, ctx.author.id, time.time())) + await ctx.send("Guild added to the watched list.") + finally: + userDatabase.commit() + c.close() + + @checks.is_mod() + @checks.is_tracking_world() + @watched.command(name="info", aliases=["details", "reason"]) + async def watched_info(self, ctx: NabCtx, *, name: str): + """Shows information about a watched list entry. + + This shows who added the player, when, and if there's a reason why they were added.""" + c = userDatabase.cursor() + try: + c.execute("SELECT * FROM watched_list WHERE server_id = ? AND is_guild = 0 AND name LIKE ? LIMIT 1", + (ctx.guild.id, name)) + result = c.fetchone() + if not result: + await ctx.send("There are no characters with that name.") + return + finally: + c.close() + + embed = discord.Embed(title=result["name"]) + if result["reason"] is not None: + embed.description = f"**Reason:** {result['reason']}" + author = ctx.guild.get_member(result["author"]) + if author is not None: + embed.set_footer(text=f"{author.name}#{author.discriminator}", + icon_url=get_user_avatar(author)) + if result["added"] is not None: + embed.timestamp = dt.datetime.utcfromtimestamp(result["added"]) + await ctx.send(embed=embed) + + @checks.is_mod() + @checks.is_tracking_world() + @watched.command(name="infoguild", aliases=["detailsguild", "reasonguild"]) + async def watched_infoguild(self, ctx: NabCtx, *, name: str): + """"Shows details about a guild entry in the watched list. + + This shows who added the player, when, and if there's a reason why they were added.""" + c = userDatabase.cursor() + try: + c.execute("SELECT * FROM watched_list WHERE server_id = ? AND is_guild = 1 AND name LIKE ? LIMIT 1", + (ctx.guild.id, name)) + result = c.fetchone() + if not result: + await ctx.send("There are no guilds with that name.") + return + finally: + c.close() + + embed = discord.Embed(title=result["name"]) + if result["reason"] is not None: + embed.description = f"**Reason:** {result['reason']}" + author = ctx.guild.get_member(result["author"]) + if author is not None: + embed.set_footer(text=f"{author.name}#{author.discriminator}", + icon_url=get_user_avatar(author)) + if result["added"] is not None: + embed.timestamp = dt.datetime.utcfromtimestamp(result["added"]) + await ctx.send(embed=embed) + + @checks.is_mod() + @checks.is_tracking_world() + @watched.command(name="list") + async def watched_list(self, ctx: NabCtx): + """Shows a list of all watched characters + + Note that this lists all characters, not just online characters.""" + world = self.bot.tracked_worlds.get(ctx.guild.id, None) + if world is None: + await ctx.send("This server is not tracking any tibia worlds.") + return + c = userDatabase.cursor() + try: + c.execute("SELECT * FROM watched_list WHERE server_id = ? AND is_guild = 0 ORDER BY name ASC", + (ctx.guild.id,)) + results = c.fetchall() + if not results: + await ctx.send("There are no characters in the watched list.") + return + entries = [f"[{r['name']}]({get_character_url(r['name'])})" for r in results] + finally: + c.close() + pages = Pages(ctx, entries=entries) + pages.embed.title = "Watched Characters" + try: + await pages.paginate() + except CannotPaginate as e: + await ctx.send(e) + + @checks.is_mod() + @checks.is_tracking_world() + @watched.command(name="listguilds", aliases=["guilds", "guildlist"]) + async def watched_list_guild(self, ctx: NabCtx): + """Shows a list of all watched characters + + Note that this lists all characters, not just online characters.""" + world = self.bot.tracked_worlds.get(ctx.guild.id, None) + if world is None: + await ctx.send("This server is not tracking any tibia worlds.") + return + c = userDatabase.cursor() + try: + c.execute("SELECT * FROM watched_list WHERE server_id = ? AND is_guild = 1 ORDER BY name ASC", + (ctx.guild.id,)) + results = c.fetchall() + if not results: + await ctx.send("There are no guilds in the watched list.") + return + entries = [f"[{r['name']}]({url_guild+urllib.parse.quote(r['name'])})" for r in results] + finally: + c.close() + pages = Pages(ctx, entries=entries) + pages.embed.title = "Watched Guilds" + try: + await pages.paginate() + except CannotPaginate as e: + await ctx.send(e) + + @checks.is_mod() + @checks.is_tracking_world() + @watched.command(name="remove", aliases=["removeplayer", "removechar"]) + async def watched_remove(self, ctx: NabCtx, *, name=None): + """Removes a character from the watched list.""" + if name is None: + ctx.send("You need to tell me the name of the person you want to remove from the list.") + + world = self.bot.tracked_worlds.get(ctx.guild.id, None) + if world is None: + await ctx.send("This server is not tracking any tibia worlds.") + return + + c = userDatabase.cursor() + try: + c.execute("SELECT * FROM watched_list WHERE server_id = ? AND name LIKE ? and is_guild = 0", + (ctx.guild.id, name)) + result = c.fetchone() + if result is None: + await ctx.send("This character is not in the watched list.") + return + + message = await ctx.send(f"Do you want to remove **{name}** from the watched list?") + confirm = await ctx.react_confirm(message) + if confirm is None: + await ctx.send("You took too long!") + return + if not confirm: + await ctx.send("Ok then, guess you changed your mind.") + return + + c.execute("DELETE FROM watched_list WHERE server_id = ? AND name LIKE ? AND is_guild = 0", + (ctx.guild.id, name,)) + await ctx.send("Character removed from the watched list.") + finally: + userDatabase.commit() + c.close() + + @checks.is_mod() + @checks.is_tracking_world() + @watched.command(name="removeguild") + async def watched_removeguild(self, ctx: NabCtx, *, name=None): + """Removes a guild from the watched list.""" + if name is None: + ctx.send("You need to tell me the name of the guild you want to remove from the list.") + + world = self.bot.tracked_worlds.get(ctx.guild.id, None) + if world is None: + await ctx.send("This server is not tracking any tibia worlds.") + return - sep = '+'.join('-' * w for w in self._widths) - sep = f'+{sep}+' + c = userDatabase.cursor() + try: + c.execute("SELECT * FROM watched_list WHERE server_id = ? AND name LIKE ? and is_guild = 1", + (ctx.guild.id, name)) + result = c.fetchone() + if result is None: + await ctx.send("This guild is not in the watched list.") + return - to_draw = [sep] + message = await ctx.send(f"Do you want to remove **{name}** from the watched list?") + confirm = await ctx.react_confirm(message) + if confirm is None: + await ctx.send("You took too long!") + return + if not confirm: + await ctx.send("Ok then, guess you changed your mind.") + return - def get_entry(d): - elem = '|'.join(f'{e:^{self._widths[i]}}' for i, e in enumerate(d)) - return f'|{elem}|' + c.execute("DELETE FROM watched_list WHERE server_id = ? AND name LIKE ? AND is_guild = 1", + (ctx.guild.id, name,)) + await ctx.send("Guild removed from the watched list.") + finally: + userDatabase.commit() + c.close() - to_draw.append(get_entry(self._columns)) - to_draw.append(sep) + def __unload(self): + print("cogs.tracking: Cancelling pending tasks...") + self.scan_deaths_task.cancel() + self.scan_highscores_task.cancel() + self.scan_online_chars_task.cancel() - for row in self._rows: - to_draw.append(get_entry(row)) - to_draw.append(sep) - return '\n'.join(to_draw) +def setup(bot): + bot.add_cog(Tracking(bot)) From b0b184aa38096ea91d955ced62e09a8f1fc95d92 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Mon, 30 Jul 2018 00:14:12 -0700 Subject: [PATCH 17/26] Fixes to announce_death and more fixed - Fixing Nezune's spaghetti pretty much - Level up message's lambdas now have *_ to ignore unnecesary extra parameters - Replaced level up messages for hundreds with % operator to include all of them --- cogs/tracking.py | 98 +++++++++++++++++++----------------- utils/messages.py | 126 +++++++++++++++++++++++----------------------- 2 files changed, 114 insertions(+), 110 deletions(-) diff --git a/cogs/tracking.py b/cogs/tracking.py index d554980..dc2416e 100644 --- a/cogs/tracking.py +++ b/cogs/tracking.py @@ -413,27 +413,6 @@ async def announce_death(self, death: Death, levels_lost=0, char: Character = No else: killer_article = "" - # Select a message - if death.by_player: - message = weighed_choice(self.ctx, death_messages_player, vocation=char.vocation, level=death.level, - levels_lost=levels_lost) - elif death.killer in ["death", "energy", "earth", "fire", "Pit Battler", "Pit Berserker", "Pit Blackling", - "Pit Brawler", "Pit Condemned", "Pit Demon", "Pit Destroyer", "Pit Fiend", - "Pit Groveller", "Pit Grunt", "Pit Lord", "Pit Maimer", "Pit Overlord", "Pit Reaver", - "Pit Scourge"] and levels_lost == 0: - # Skip element damage deaths unless player lost a level to avoid spam from arena deaths - # This will cause a small amount of deaths to not be announced but it's probably worth the tradeoff (ty selken) - return - else: - message = weighed_choice(self.ctx, death_messages_monster, vocation=char.vocation, level=death.level, - levels_lost=levels_lost, killer=death.killer) - # Format message with death information - death_info = {'name': char.name, 'level': death.level, 'killer': death.killer, 'killer_article': killer_article, - 'he_she': char.he_she.lower(), 'his_her': char.his_her.lower(), 'him_her': char.him_her.lower()} - message = message.format(**death_info) - # Format extra stylization - message = f"{config.pvpdeath_emoji if death.by_player else config.death_emoji} {format_message(message)}" - for guild_id, tracked_world in self.bot.tracked_worlds.items(): guild = self.bot.get_guild(guild_id) if guild is None: @@ -441,15 +420,39 @@ async def announce_death(self, death: Death, levels_lost=0, char: Character = No min_level = get_server_property(guild_id, "announce_level", is_int=True, default=config.announce_threshold) if death.level < min_level: continue - if char.world == tracked_world and guild.get_member(char.owner) is not None: - try: - channel = self.bot.get_channel_or_top(guild, - get_server_property(guild.id, "levels_channel", is_int=True)) - await channel.send(message[:1].upper() + message[1:]) - except discord.Forbidden: - log.warning("announce_death: Missing permissions.") - except discord.HTTPException: - log.warning("announce_death: Malformed message.") + if char.world != tracked_world or guild.get_member(char.owner) is None: + continue + # Select a message + if death.by_player: + message = weighed_choice(death_messages_player, vocation=char.vocation, level=death.level, + levels_lost=levels_lost, min_level=min_level) + elif death.killer in ["death", "energy", "earth", "fire", "Pit Battler", "Pit Berserker", + "Pit Blackling", + "Pit Brawler", "Pit Condemned", "Pit Demon", "Pit Destroyer", "Pit Fiend", + "Pit Groveller", "Pit Grunt", "Pit Lord", "Pit Maimer", "Pit Overlord", + "Pit Reaver", + "Pit Scourge"] and levels_lost == 0: + # Skip element damage deaths unless player lost a level to avoid spam from arena deaths + # This will cause a small amount of deaths to not be announced but it's probably worth the tradeoff + return + else: + message = weighed_choice(death_messages_monster, vocation=char.vocation, level=death.level, + levels_lost=levels_lost, killer=death.killer, min_level=min_level) + # Format message with death information + death_info = {'name': char.name, 'level': death.level, 'killer': death.killer, + 'killer_article': killer_article, 'he_she': char.he_she.lower(), + 'his_her': char.his_her.lower(), 'him_her': char.him_her.lower()} + message = message.format(**death_info) + # Format extra stylization + message = f"{config.pvpdeath_emoji if death.by_player else config.death_emoji} {format_message(message)}" + try: + channel = self.bot.get_channel_or_top(guild, + get_server_property(guild.id, "levels_channel", is_int=True)) + await channel.send(message[:1].upper() + message[1:]) + except discord.Forbidden: + log.warning("announce_death: Missing permissions.") + except discord.HTTPException: + log.warning("announce_death: Malformed message.") async def announce_level(self, level, char_name: str = None, char: Character = None): """Announces a level up on corresponding servers @@ -481,23 +484,24 @@ async def announce_level(self, level, char_name: str = None, char: Character = N min_level = get_server_property(server_id, "announce_level", is_int=True, default=config.announce_threshold) if char.level < min_level: continue - if char.world == tracked_world and server.get_member(char.owner) is not None: - try: - channel = self.bot.get_channel_or_top(server, - get_server_property(server.id, "levels_channel", is_int=True)) - # Select a message - message = weighed_choice(server, level_messages, vocation=char.vocation, level=level) - level_info = {'name': char.name, 'level': level, 'he_she': char.he_she.lower(), 'his_her': char.his_her.lower(), - 'him_her': char.him_her.lower()} - # Format message with level information - message = message.format(**level_info) - # Format extra stylization - message = f"{config.levelup_emoji} {format_message(message)}" - await channel.send(message) - except discord.Forbidden: - log.warning("announce_level: Missing permissions.") - except discord.HTTPException: - log.warning("announce_level: Malformed message.") + if char.world != tracked_world or server.get_member(char.owner) is None: + continue + try: + channel = self.bot.get_channel_or_top(server, + get_server_property(server.id, "levels_channel", is_int=True)) + # Select a message + message = weighed_choice(level_messages, vocation=char.vocation, level=level, min_level=min_level) + level_info = {'name': char.name, 'level': level, 'he_she': char.he_she.lower(), + 'his_her': char.his_her.lower(), 'him_her': char.him_her.lower()} + # Format message with level information + message = message.format(**level_info) + # Format extra stylization + message = f"{config.levelup_emoji} {format_message(message)}" + await channel.send(message) + except discord.Forbidden: + log.warning("announce_level: Missing permissions.") + except discord.HTTPException: + log.warning("announce_level: Malformed message.") # Commands @commands.command() diff --git a/utils/messages.py b/utils/messages.py index c7eb892..dd67711 100644 --- a/utils/messages.py +++ b/utils/messages.py @@ -3,10 +3,6 @@ import discord -from utils.config import config -from utils.database import get_server_property - -announce_threshold = config.announce_threshold # We save the last messages so they are not repeated so often last_messages = [""]*10 @@ -14,7 +10,7 @@ # Message list for announce_level # Parameters: {name}, {level} , {he_she}, {his_her}, {him_her} # Values in each list element are: -# Relative chance, message, lambda function as filter (takes (discord)server, level, voc, killer, levels_lost) +# Relative chance, message, lambda function as filter (takes min_level, level, voc) # Only relative chance and message are mandatory. level_messages = [ [100, "Congratulations to **{name}** on reaching level {level}!"], @@ -38,76 +34,76 @@ [70, "**{name}** is level {level} now! And we all thought {he_she}'d never achieve anything in life."], # EK Only [50, "**{name}** has reached level {level}. That's 9 more mana potions you can carry now!", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Knight" in voc], + lambda min_level, level, voc, *_: level >= 100 and "Knight" in voc], [200, "**{name}** is level {level}. Stick them with the pointy end! πŸ—‘οΈ", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Knight" in voc], + lambda min_level, level, voc, *_: level >= 100 and "Knight" in voc], [200, "**{name}** is a fat level {level} meatwall now. BLOCK FOR ME SENPAI.", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Knight" in voc], + lambda min_level, level, voc, *_: level >= 100 and "Knight" in voc], # RP Only [50, "**{name}** has reached level {level}. But {he_she} still misses arrows...", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Paladin" in voc], + lambda min_level, level, voc, *_: level >= 100 and "Paladin" in voc], [150, "Congrats on level {level}, **{name}**. You can stop running around now.", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Paladin" in voc], + lambda min_level, level, voc, *_: level >= 100 and "Paladin" in voc], [150, "**{name}** is level {level}. Bullseye!🎯", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Paladin" in voc], + lambda min_level, level, voc, *_: level >= 100 and "Paladin" in voc], # MS Only [50, "Level {level}, **{name}**? Nice. Don't you wish you were a druid though?", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Sorcerer" in voc], + lambda min_level, level, voc, *_: level >= 100 and "Sorcerer" in voc], [150, "**{name}** is level {level}. Watch out for {his_her} SDs!", - lambda server, level, voc, killer, levels_lost: level >= 45 and "Sorcerer" in voc], + lambda min_level, level, voc, *_: level >= 45 and "Sorcerer" in voc], [150, "**{name}** got level {level}. If {he_she} only stopped missing beams.", - lambda server, level, voc, killer, levels_lost: level >= 23 and "Sorcerer" in voc], + lambda min_level, level, voc, *_: level >= 23 and "Sorcerer" in voc], [150, "**{name}** is level {level}. πŸ”₯πŸ”₯BURN THEM ALLπŸ”₯πŸ”₯", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Sorcerer" in voc], + lambda min_level, level, voc, *_: level >= 100 and "Sorcerer" in voc], # ED Only [50, "**{name}** has reached level {level}. Flower power!🌼", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Druid" in voc], + lambda min_level, level, voc, *_: level >= 100 and "Druid" in voc], [150, "Congrats on level {level}, **{name}**. Sio plz.", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Druid" in voc], + lambda min_level, level, voc, *_: level >= 100 and "Druid" in voc], [150, "**{name}** is level {level}. πŸ”₯πŸ”₯BURN THEM ALL... Or... Give them frostbite...?❄❄", - lambda server, level, voc, killer, levels_lost: level >= 100 and "Druid" in voc], + lambda min_level, level, voc, *_: level >= 100 and "Druid" in voc], # Level specific [20000, "**{name}** is level {level}! UMPs so good 🍷", - lambda server, level, voc, killer, levels_lost: level == 130 and ("Druid" in voc or "Sorcerer" in voc)], + lambda min_level, level, voc, *_: level == 130 and ("Druid" in voc or "Sorcerer" in voc)], [20000, "**{name}** is level {level} now! Eternal Winter is coming!❄", - lambda server, level, voc, killer, levels_lost: level == 60 and "Druid" in voc], + lambda min_level, level, voc, *_: level == 60 and "Druid" in voc], [20000, "**{name}** is level {level} now! Time to unleash the Wrath of NatureπŸƒπŸƒ... just look at that wrath.", - lambda server, level, voc, killer, levels_lost: level == 55 and "Druid" in voc], + lambda min_level, level, voc, *_: level == 55 and "Druid" in voc], [20000, "**{name}** is now level {level}. Don't forget to buy a Gearwheel Chain!πŸ“Ώ", - lambda server, level, voc, killer, levels_lost: level == 75], + lambda min_level, level, voc, *_: level == 75], [30000, "**{name}** is level {level}! You can become a ninja now!πŸ‘€", - lambda server, level, voc, killer, levels_lost: level == 80 and "Paladin" in voc], + lambda min_level, level, voc, *_: level == 80 and "Paladin" in voc], [30000, "**{name}** is level {level}! Time to get some crystalline arrows!🏹", - lambda server, level, voc, killer, levels_lost: level == 90 and "Paladin" in voc], + lambda min_level, level, voc, *_: level == 90 and "Paladin" in voc], [20000, "Level {level}, **{name}**? You're finally important enough for me to notice!", - lambda server, level, voc, killer, levels_lost: level == get_server_property(server.id, "announce_level", is_int=True, default=announce_threshold)], + lambda min_level, level, voc, *_: level == min_level], [20000, "Congratulations on level {level} **{name}**! Now you're relevant to me. As relevant a human can be anyway", - lambda server, level, voc, killer, levels_lost: level == get_server_property(server.id, "announce_level", is_int=True, default=announce_threshold)], + lambda min_level, level, voc, *_: level == min_level], [20000, "**{name}** is now level {level}! Time to go berserk! πŸ’’", - lambda server, level, voc, killer, levels_lost: level == 35 and "Knight" in voc], + lambda min_level, level, voc, *_: level == 35 and "Knight" in voc], [20000, "Congratulations on level {level} **{name}**! Now you can become an umbral master, but is your" " bank account ready?πŸ’Έ", - lambda server, level, voc, killer, levels_lost: level == 250], + lambda min_level, level, voc, *_: level == 250], [30000, "**{name}** is level {level}!!!!\r\n" + "Sweet, sweet triple digits!", - lambda server, level, voc, killer, levels_lost: level == 100], + lambda min_level, level, voc, *_: level == 100], [20000, "**{name}** is level {level}!!!!\r\n" + "WOOO", - lambda server, level, voc, killer, levels_lost: level in (100, 200, 300, 400)], + lambda min_level, level, voc, *_: level % 100 == 0], [20000, "**{name}** is level {level}!!!!\r\n" + "yaaaay milestone!", - lambda server, level, voc, killer, levels_lost: level in (100, 200, 300, 400)], + lambda min_level, level, voc, *_: level % 100 == 0], [20000, "**{name}** is level {level}!!!!\r\n" + "holy crap!", - lambda server, level, voc, killer, levels_lost: level in (200, 300, 400)]] + lambda min_level, level, voc, *_: level % 100 == 0]] # Message list for announce death. # Parameters: ({name},{level},{killer},{killer_article},{he_she}, {his_her},{him_her} # Additionally, words surrounded by \WORD/ are upper cased, /word\ are lower cased, /Word/ are title cased # words surrounded by ^WORD^ are ignored if the next letter found is uppercase (useful for dealing with proper nouns) # Values in each list element are: -# Relative chance, message, lambda function as filter (takes (discord)server, level, voc, killer, levels_lost) +# Relative chance, message, lambda function as filter (takes min_level, level, voc, killer, levels_lost) # Only relative chance and message are mandatory. death_messages_monster = [ [100, "RIP **{name}** ({level}), you died the way you lived- inside {killer_article}**{killer}**."], @@ -153,62 +149,65 @@ [70, "To be or not to be πŸ’€, that is the-- Well I guess **{name}** ({level}) made his choice, " "or ^that ^**{killer}** chose for him..."], [500, "**{name}** ({level}) just died to {killer_article}**{killer}**, why did nobody sio {him_her}!?", - lambda server, level, voc, killer, levels_lost: "Knight" in voc], + lambda min_level, level, voc, killer, levels_lost: "Knight" in voc], [500, "Poor **{name}** ({level}) has died. Killed by {killer_article}**{killer}**. I bet it was your " "blocker's fault though, eh **{name}**?", - lambda server, level, voc, killer, levels_lost: "Druid" in voc or "Sorcerer" in voc], + lambda min_level, level, voc, killer, levels_lost: "Druid" in voc or "Sorcerer" in voc], [500, "**{name}** ({level}) tried running away from {killer_article}**{killer}**. /{he_she}/ " "didn't run fast enough...", - lambda server, level, voc, killer, levels_lost: "Paladin" in voc], + lambda min_level, level, voc, killer, levels_lost: "Paladin" in voc], [500, "What happened to **{name}** ({level})!? Talk about sudden death! I guess ^that ^**{killer}** was " "too much for {him_her}...", - lambda server, level, voc, killer, levels_lost: "Sorcerer" in voc], + lambda min_level, level, voc, killer, levels_lost: "Sorcerer" in voc], [500, "**{name}** ({level}) was killed by {killer_article}**{killer}**. I guess {he_she} couldn't " "sio {him_her}self.", - lambda server, level, voc, killer, levels_lost: "Druid" in voc], + lambda min_level, level, voc, killer, levels_lost: "Druid" in voc], [600, "**{name}** ({level}) died to {killer_article}**{killer}**. \"Don't worry\" they said, \"They are weaker\" " "they said.", - lambda server, level, voc, killer, levels_lost: killer in ["weakened frazzlemaw", "enfeebled silencer"]], + lambda min_level, level, voc, killer, levels_lost: killer in ["weakened frazzlemaw", "enfeebled silencer"]], [2000, "Another paladin bites the dust! **{killer}** strikes again! Rest in peace **{name}** ({level}).", - lambda server, level, voc, killer, levels_lost: "Paladin" in voc and killer == "Lady Tenebris"], + lambda min_level, level, voc, killer, levels_lost: "Paladin" in voc and killer == "Lady Tenebris"], [2000, "**{name}** ({level}) got killed by ***{killer}***. How spooky is that! πŸ‘»", - lambda server, level, voc, killer, levels_lost: killer == "something evil"], + lambda min_level, level, voc, killer, levels_lost: killer == "something evil"], [2000, "**{name}** ({level}) died from **{killer}**. Yeah, no shit.", - lambda server, level, voc, killer, levels_lost: killer == "death"], + lambda min_level, level, voc, killer, levels_lost: killer == "death"], [2000, "They did warn you **{name}** ({level}), you *did* burn πŸ”₯🐲.", - lambda server, level, voc, killer, levels_lost: killer in ["dragon", "dragon lord"]], + lambda min_level, level, voc, killer, levels_lost: killer in ["dragon", "dragon lord"]], [2000, "**{name}** ({level}) died from {killer_article}**{killer}**. Someone forgot the safeword.😏", - lambda server, level, voc, killer, levels_lost: killer == "choking fear"], + lambda min_level, level, voc, killer, levels_lost: killer == "choking fear"], [2000, "That **{killer}** got really up close and personal with **{name}** ({level}). " - "Maybe he thought you were his princess Lumelia?😏", - lambda server, level, voc, killer, levels_lost: killer == "hero"], + "Maybe he thought you were his princess Lumelia?😏", + lambda min_level, level, voc, killer, levels_lost: killer == "hero"], [2000, "Looks like that **{killer}** made **{name}** ({level}) his bride πŸ˜‰.", - lambda server, level, voc, killer, levels_lost: "vampire" in killer], + lambda min_level, level, voc, killer, levels_lost: "vampire" in killer], [2000, "Yeah, those are a little stronger than regular orcs, **{name}** ({level}).", - lambda server, level, voc, killer, levels_lost: "orc cult" in killer], + lambda min_level, level, voc, killer, levels_lost: "orc cult" in killer], [1000, "Damn! The koolaid they drink in that cult must have steroids on it, **{name}** ({level}).", - lambda server, level, voc, killer, levels_lost: "cult" in killer], + lambda min_level, level, voc, killer, levels_lost: "cult" in killer], [2500, "**{name}** ({level}) met {his_her} demise at the hands of a **{killer}**. That's hot.", - lambda server, level, voc, killer, levels_lost: killer in ["true dawnfire asura", "dawnfire asura", "fury"]], + lambda min_level, level, voc, killer, levels_lost: killer in ["true dawnfire asura", "dawnfire asura", "fury"]], [2500, "Poor **{name}** ({level}) just wanted some love! That cold hearted... Witch.", - lambda server, level, voc, killer, levels_lost: killer in ["true frost flower asura", "frost flower asura", "frost giantess", "ice witch"]], + lambda min_level, level, voc, killer, levels_lost: killer in ["true frost flower asura", "frost flower asura", + "frost giantess", "ice witch"]], [2000, "Asian chicks are no joke **{name}** ({level}) πŸ”ͺπŸ’”.", - lambda server, level, voc, killer, levels_lost: "asura" in killer], + lambda min_level, level, voc, killer, levels_lost: "asura" in killer], [2500, "Asian chicks sure age well, don't you think so, **{name}** ({level})? πŸ˜πŸ‘΅.", - lambda server, level, voc, killer, levels_lost: "true" in killer and "asura" in killer], + lambda min_level, level, voc, killer, levels_lost: "true" in killer and "asura" in killer], [2000, "**{name}** ({level}) got destroyed by {killer_article}**{killer}**. I bet {he_she} regrets going down" - "that hole πŸ•³οΈ", - lambda server, level, voc, killer, levels_lost: level < 120 and killer in ["breach brood", "dread intruder", "reality reaver", - "spark of destruction", "sparkion"]], + "that hole πŸ•³οΈ", + lambda min_level, level, voc, killer, levels_lost: level < 120 and killer in ["breach brood", "dread intruder", + "reality reaver", + "spark of destruction", "sparkion"]], [2000, "Watch out for that **{killer}**'s wav... Oh😐... Rest in peace **{name}** ({level}).", - lambda server, level, voc, killer, levels_lost: killer in ["dragon", "dragon lord", "undead dragon", "draken spellweaver", - "hellhound", "hellfire fighter"]], + lambda min_level, level, voc, killer, levels_lost: killer in ["dragon", "dragon lord", "undead dragon", + "draken spellweaver", "hellhound", + "hellfire fighter"]], [2000, "**{name}** ({level}) died to {killer_article}**{killer}**! Don't worry, {he_she} didn't have a soul anyway", - lambda server, level, voc, killer, levels_lost: killer == "souleater"], + lambda min_level, level, voc, killer, levels_lost: killer == "souleater"], [150, "Oh look at that, rest in peace **{name}** ({level}), ^that ^**{killer}** really got you. " "Hope you get your level back.", - lambda server, level, voc, killer, levels_lost: levels_lost > 0] + lambda min_level, level, voc, killer, levels_lost: levels_lost > 0] ] # Deaths by players @@ -250,16 +249,17 @@ def format_message(message) -> str: return message -def weighed_choice(server, choices, level: int, vocation: str = None, killer: str = None, levels_lost: int = 0) -> str: +def weighed_choice(choices, level: int, vocation: str = None, min_level=0, killer: str = None, + levels_lost: int = 0) -> str: """Makes weighed choices from message lists where [0] is a value representing the relative odds of picking a message and [1] is the message string""" # Find the max range by adding up the weigh of every message in the list - # and purge out messages that dont fulfil the conditions + # and purge out messages that don't fulfil the conditions weight_range = 0 _messages = [] for message in choices: - if len(message) == 3 and not message[2](server, level, vocation, killer, levels_lost): + if len(message) == 3 and not message[2](min_level, level, vocation, killer, levels_lost): continue weight_range = weight_range + (message[0] if not message[1] in last_messages else message[0] / 10) _messages.append(message) From e72d732453f22bb79b4646276f0416b4017ce876 Mon Sep 17 00:00:00 2001 From: Nezune Date: Mon, 30 Jul 2018 11:52:06 +0000 Subject: [PATCH 18/26] get_character now uses online list data for level and vocation should fix the issue with commands (/share, /stats, etc.) using an outdated level for players that leveled up and are still online --- utils/tibia.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/utils/tibia.py b/utils/tibia.py index ce8bc78..1c25244 100644 --- a/utils/tibia.py +++ b/utils/tibia.py @@ -17,6 +17,7 @@ from utils.config import config from utils.database import userDatabase, tibiaDatabase +from utils.general import global_online_list from .general import log # Constants @@ -419,6 +420,17 @@ async def get_character(name, tries=5, *, bot: commands.Bot=None) -> Optional[Ch if result: character.house["houseid"] = result["id"] + # If the character exists in the online list use data from there where possible + try: + character.level = [x.level for x in global_online_list if x.name == character.name][0] + except IndexError: + pass + + try: + character.vocation = [x.vocation for x in global_online_list if x.name == character.name][0] + except IndexError: + pass + # Database operations c = userDatabase.cursor() # Skills from highscores From 12caa2b296c6b02c002938c9cf16af8625f23531 Mon Sep 17 00:00:00 2001 From: Falsejoey <11788737+Falsejoey@users.noreply.github.com> Date: Mon, 30 Jul 2018 07:53:26 -0400 Subject: [PATCH 19/26] Update Grammar --- utils/messages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/messages.py b/utils/messages.py index dd67711..bde6e52 100644 --- a/utils/messages.py +++ b/utils/messages.py @@ -176,7 +176,7 @@ [2000, "**{name}** ({level}) died from {killer_article}**{killer}**. Someone forgot the safeword.😏", lambda min_level, level, voc, killer, levels_lost: killer == "choking fear"], [2000, "That **{killer}** got really up close and personal with **{name}** ({level}). " - "Maybe he thought you were his princess Lumelia?😏", + "Maybe he thought you were his Princess Lumelia?😏", lambda min_level, level, voc, killer, levels_lost: killer == "hero"], [2000, "Looks like that **{killer}** made **{name}** ({level}) his bride πŸ˜‰.", lambda min_level, level, voc, killer, levels_lost: "vampire" in killer], From b52d8508f704cb845f96ec2282d54983112594a8 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Mon, 30 Jul 2018 09:56:07 -0700 Subject: [PATCH 20/26] Cleaned up code from last commit - Iterate global online list only once instead of once for vocs and once for level --- utils/tibia.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/utils/tibia.py b/utils/tibia.py index 1c25244..1c4f2f9 100644 --- a/utils/tibia.py +++ b/utils/tibia.py @@ -421,15 +421,11 @@ async def get_character(name, tries=5, *, bot: commands.Bot=None) -> Optional[Ch character.house["houseid"] = result["id"] # If the character exists in the online list use data from there where possible - try: - character.level = [x.level for x in global_online_list if x.name == character.name][0] - except IndexError: - pass - - try: - character.vocation = [x.vocation for x in global_online_list if x.name == character.name][0] - except IndexError: - pass + for c in global_online_list: + if c == character: + character.level = c.level + character.vocation = c.vocation + break # Database operations c = userDatabase.cursor() From 18fbc32cb8f15dfe208a307b42bdbbcbe8bead68 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Mon, 30 Jul 2018 16:50:56 -0700 Subject: [PATCH 21/26] Server settings can now be changed with `Manage Server` permission --- cogs/settings.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/cogs/settings.py b/cogs/settings.py index da6f5ea..fc72e38 100644 --- a/cogs/settings.py +++ b/cogs/settings.py @@ -33,7 +33,9 @@ async def convert(self, ctx, argument): class Settings: - """Commands related to server customization.""" + """Commands related to server customization. + + Settings can only be modified by Server Moderators (users with Manage Server permission).""" def __init__(self, bot: NabBot): self.bot = bot @@ -55,7 +57,7 @@ async def show_info_embed(ctx: NabCtx, current_value, accepted_values, edit_para f'for more info') await ctx.send(embed=embed) - @checks.is_admin() + @checks.is_mod() @commands.guild_only() @commands.group(invoke_without_command=True, case_insensitive=True, aliases=["config"]) async def settings(self, ctx: NabCtx): @@ -69,7 +71,7 @@ async def settings(self, ctx: NabCtx): embed.add_field(name=info["title"], value=f"`{ctx.clean_prefix}{ctx.invoked_with} {name}`") await ctx.send(embed=embed) - @checks.is_admin() + @checks.is_mod() @settings.command(name="askchannel", aliases=["commandchannel"]) async def settings_askchannel(self, ctx: NabCtx, channel: str=None): """Changes the channel where longer replies for commands are given. @@ -128,7 +130,7 @@ async def settings_askchannel(self, ctx: NabCtx, channel: str=None): else: await ctx.send(f"{ctx.tick(True)} <#{new_value}> will now be used as a command channel.") - @checks.is_admin() + @checks.is_mod() @settings.command(name="commandsonly") async def settings_commandsonly(self, ctx: NabCtx, option: str=None): """Sets whether only commands are allowed in the command channel. @@ -158,7 +160,7 @@ def yes_no(choice: bool): else: await ctx.send("That's not a valid option, try **yes** or **no**.") - @checks.is_admin() + @checks.is_mod() @settings.command(name="eventschannel") async def settings_eventschannel(self, ctx: NabCtx, channel: str=None): """Changes the channel where upcoming events are announced. @@ -202,7 +204,7 @@ async def settings_eventschannel(self, ctx: NabCtx, channel: str=None): else: await ctx.send(f"{ctx.tick(True)} <#{new_value}> will now be used for events.") - @checks.is_admin() + @checks.is_mod() @settings.command(name="levelschannel", aliases=["deathschannel", "trackingchannel"]) async def settings_levelschannel(self, ctx: NabCtx, channel: str=None): """Changes the channel where levelup and deaths are announced. @@ -247,7 +249,7 @@ async def settings_levelschannel(self, ctx: NabCtx, channel: str=None): else: await ctx.send(f"{ctx.tick(True)} <#{new_value}> will now be used.") - @checks.is_admin() + @checks.is_mod() @settings.command(name="minlevel", aliases=["announcelevel"]) async def settings_minlevel(self, ctx: NabCtx, level: int=None): """Sets the minimum level for death and level up announcements. @@ -266,7 +268,7 @@ async def settings_minlevel(self, ctx: NabCtx, level: int=None): set_server_property(ctx.guild.id, "announce_level", level) await ctx.send(f"{ctx.tick()} Minimum announce level has been set to `{level}`.") - @checks.is_admin() + @checks.is_mod() @settings.command(name="newschannel") async def settings_newschannel(self, ctx: NabCtx, channel: str=None): """Changes the channel where Tibia news are announced. @@ -308,7 +310,7 @@ async def settings_newschannel(self, ctx: NabCtx, channel: str=None): else: await ctx.send(f"{ctx.tick(True)} <#{new_value}> will now be used for Tibia news.") - @checks.is_admin() + @checks.is_mod() @settings.command(name="prefix") async def settings_prefix(self, ctx: NabCtx, prefix: PrefixConverter=None): """Changes the command prefix for this server. @@ -349,7 +351,7 @@ async def settings_prefix(self, ctx: NabCtx, prefix: PrefixConverter=None): await ctx.send(f"{ctx.tick(True)} The prefix `{prefix}` was added.") set_server_property(ctx.guild.id, "prefixes", sorted(prefixes, reverse=True), serialize=True) - @checks.is_admin() + @checks.is_mod() @settings.command(name="welcome") async def settings_welcome(self, ctx: NabCtx, *, message: str = None): """Changes the message new members receive when joining. @@ -402,7 +404,7 @@ async def settings_welcome(self, ctx: NabCtx, *, message: str = None): else: await ctx.send(f"{ctx.tick(True)} Welcome message updated.") - @checks.is_admin() + @checks.is_mod() @settings.command(name="welcomechannel") async def settings_welcomechannel(self, ctx: NabCtx, channel: str = None): """Changes the channel where new members are welcomed. @@ -447,7 +449,7 @@ async def settings_welcomechannel(self, ctx: NabCtx, channel: str = None): else: await ctx.send(f"{ctx.tick(True)} <#{new_value}> will now be used for welcome messages.") - @checks.is_admin() + @checks.is_mod() @checks.is_not_lite() @settings.command(name="world") async def settings_world(self, ctx: NabCtx, world: str=None): From e27046f28ae952f9f018d766013d3835dc3ef3cb Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Mon, 30 Jul 2018 17:51:27 -0700 Subject: [PATCH 22/26] NabBot's initial message is now sent in the server instead of PM - It used to PM the server's owner, but sometimes it wasn't the owner the one that added the bot. --- cogs/settings.py | 2 +- nabbot.py | 43 ++++++++++++++++++++++--------------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/cogs/settings.py b/cogs/settings.py index fc72e38..1079d2a 100644 --- a/cogs/settings.py +++ b/cogs/settings.py @@ -490,7 +490,7 @@ async def prefix_error(self, ctx: NabCtx, error): await ctx.send(str(error)) def get_current_channel(self, ctx: NabCtx, current_channel_id, pm_fallback=False): - top_channel = self.bot.get_top_channel(ctx.guild, True) + top_channel = self.bot.get_top_channel(ctx.guild) current_channel = ctx.guild.get_channel(current_channel_id) if current_channel: perms = current_channel.permissions_for(ctx.me) diff --git a/nabbot.py b/nabbot.py index 2730313..c57db25 100644 --- a/nabbot.py +++ b/nabbot.py @@ -136,27 +136,30 @@ async def on_command_error(self, ctx: context.NabCtx, error): async def on_guild_join(self, guild: discord.Guild): """Called when the bot joins a guild (server).""" log.info("Nab Bot added to server: {0.name} (ID: {0.id})".format(guild)) - message = "Hello! I'm now in **{0.name}**. To see my available commands, type `{3}help`\n" \ - "I will reply to commands from any channel I can see, but if you create a channel called *{1}*, " \ - "I will give longer replies and more information there.\n" \ - "If you want a server log channel, create a channel called *{2}*, I will post logs in there." \ - "You might want to make it private though.\n" \ - "To tweak NabBot settings, use `{3}settings` in your server." - formatted_message = message.format(guild, config.ask_channel_name, config.log_channel_name, - config.command_prefix[0]) + message = f"**I've been added to this server.**\n" \ + f"Some things you should know:\n" \ + f"β€£ My command prefix is: `{config.command_prefix[0]}` (it is customizable)\n" \ + f"β€£ You can see all my commands with: `{config.command_prefix[0]}help` or " \ + f"`{config.command_prefix[0]}commands`\n" \ + f"β€£ You can configure me using: `{config.command_prefix[0]}settings`\n" \ + f"β€£ You can set a world for me to track by using `{config.command_prefix[0]}settings world`\n" \ + f"β€£ If you want a logging channel, create a channel named `{config.log_channel_name}`\n" \ + f"β€£ If you need help, join my support server: ****\n" \ + f"β€£ For more information and links in: `{config.command_prefix[0]}about`" for member in guild.members: if member.id in self.members: self.members[member.id].append(guild.id) else: self.members[member.id] = [guild.id] try: - await guild.owner.send(formatted_message) - except discord.Forbidden: - # Owner doesn't allow PMs - top_channel = self.get_top_channel(guild, True) - if top_channel is not None: - formatted_message += "\n*I meant to send this privately, but you do not allow private messages.*" - await top_channel.send(formatted_message) + channel = self.get_top_channel(guild) + if channel is None: + log.warning(f"Could not send join message on server: {guild.name}. No allowed channel found.") + return + await channel.send(message) + except discord.HTTPException as e: + log.error(f"Could not send join message on server: {guild.name}.", exc_info=e) + async def on_guild_remove(self, guild: discord.Guild): """Called when the bot leaves a guild (server).""" @@ -493,13 +496,13 @@ def get_channel_or_top(self, guild: discord.Guild, channel_id: int) -> discord.T It also checks if the bot has permissions on that channel, if not, it will return the top channel too.""" if channel_id is None: - return self.get_top_channel(guild, True) + return self.get_top_channel(guild) channel = guild.get_channel(int(channel_id)) if channel is None: - return self.get_top_channel(guild, True) + return self.get_top_channel(guild) permissions = channel.permissions_for(guild.me) if not permissions.read_messages or not permissions.send_messages: - return self.get_top_channel(guild, True) + return self.get_top_channel(guild) return channel async def send_log_message(self, guild: discord.Guild, content=None, *, embed: discord.Embed = None): @@ -543,7 +546,7 @@ async def show_help(self, ctx, command=None): await ctx.invoke(cmd, command=command) @staticmethod - def get_top_channel(guild: discord.Guild, writeable_only: bool=False) -> Optional[discord.TextChannel]: + def get_top_channel(guild: discord.Guild) -> Optional[discord.TextChannel]: """Returns the highest text channel on the list. If writeable_only is set, the first channel where the bot can write is returned @@ -551,8 +554,6 @@ def get_top_channel(guild: discord.Guild, writeable_only: bool=False) -> Optiona if guild is None: return None for channel in guild.text_channels: - if not writeable_only: - return channel if channel.permissions_for(guild.me).send_messages: return channel return None From 12b133d3eebae89ab66cd02030d05d7938215e27 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Mon, 30 Jul 2018 20:35:27 -0700 Subject: [PATCH 23/26] Added documentation for the Info cog. --- .gitlab-ci.yml | 2 +- .travis.yml | 2 +- cogs/info.py | 109 ++++++++-------- docs/assets/images/commands/commands.png | Bin 0 -> 36403 bytes docs/assets/images/commands/emojiinfo_1.png | Bin 0 -> 31645 bytes docs/assets/images/commands/emojiinfo_2.png | Bin 0 -> 19602 bytes docs/assets/images/commands/serverinfo.png | Bin 41625 -> 35594 bytes docs/assets/images/commands/userinfo.png | Bin 0 -> 38759 bytes docs/commands/general.md | 75 +---------- docs/commands/info.md | 135 ++++++++++++++++++++ docs/faq.md | 64 ++++++---- mkdocs.yml | 1 + requirements-docs.txt | 4 + 13 files changed, 238 insertions(+), 154 deletions(-) create mode 100644 docs/assets/images/commands/commands.png create mode 100644 docs/assets/images/commands/emojiinfo_1.png create mode 100644 docs/assets/images/commands/emojiinfo_2.png create mode 100644 docs/assets/images/commands/userinfo.png create mode 100644 docs/commands/info.md create mode 100644 requirements-docs.txt diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c2aeb51..b3cbe1f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,7 +2,7 @@ image: python:3.6 pages: before_script: - - python -m pip install mkdocs mkdocs-material pymdown-extensions git+https://github.com/jdittrich/figureAltCaption.git + - python -m pip r requirements-docs.txt script: - cp CHANGELOG.md docs/changelog.md - python -m mkdocs build -d public diff --git a/.travis.yml b/.travis.yml index aebcd0d..54c7092 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ matrix: install: - pip install -U -r requirements.txt - - pip install mkdocs mkdocs-material pymdown-extensions git+https://github.com/jdittrich/figureAltCaption.git + - pip install -U -r requirements-docs.txt script: - python -m compileall ./nabbot.py diff --git a/cogs/info.py b/cogs/info.py index 659da09..78242f9 100644 --- a/cogs/info.py +++ b/cogs/info.py @@ -114,7 +114,12 @@ def ram(value): @checks.can_embed() @commands.command(name="commands", aliases=["commandlist"]) async def _commands(self, ctx: NabCtx): - """Shows a simple list of all commands.""" + """Shows a simple list of all commands. + + This displays all the commands you can use, with no description or subcommand information. + Note that different commands might show up in server channels and in private messages. + + For more details, use `help`.""" embed = discord.Embed(title=f"{ctx.me.display_name} commands") embed.set_footer(text=f"For a more detailed list, try '{ctx.clean_prefix}help' or " f"'{ctx.clean_prefix}help [command_name]'") @@ -129,6 +134,53 @@ async def _commands(self, ctx: NabCtx): embed.add_field(name=k, value=", ".join(f"`{c}`" for c in sorted(categories[k])), inline=False) await ctx.send(embed=embed) + @commands.guild_only() + @commands.command(name="emojiinfo") + async def emoji_info(self, ctx: NabCtx, *, emoji: discord.Emoji=None): + """Shows information about an emoji, or shows all emojis. + + If the command is used with no arguments, all the server emojis are shown. + + If a emoji, its id or name is provided, it will show more information about it. + + Only emojis in the current servers can be checked.""" + if emoji is not None: + embed = discord.Embed(title=emoji.name, timestamp=emoji.created_at, color=discord.Color.blurple()) + embed.set_thumbnail(url=emoji.url) + embed.set_footer(text="Created at") + embed.add_field(name="ID", value=emoji.id) + embed.add_field(name="Usage", value=f"`{emoji}`") + embed.add_field(name="Attributes", inline=False, + value=f"{ctx.tick(emoji.managed)} Twitch managed\n" + f"{ctx.tick(emoji.require_colons)} Requires colons\n" + f"{ctx.tick(len(emoji.roles) > 0)} Role limited") + else: + emojis: List[discord.Emoji] = ctx.guild.emojis + if not emojis: + return await ctx.send("This server has no custom emojis.") + normal = [str(e) for e in emojis if not e.animated] + animated = [str(e) for e in emojis if e.animated] + embed = discord.Embed(title="Custom Emojis", color=discord.Color.blurple()) + if normal: + emojis_str = "\n".join(normal) + fields = split_message(emojis_str, FIELD_VALUE_LIMIT) + for i, value in enumerate(fields): + if i == 0: + name = f"Regular ({len(normal)})" + else: + name = "\u200F" + embed.add_field(name=name, value=value.replace("\n", "")) + if animated: + emojis_str = "\n".join(animated) + fields = split_message(emojis_str, FIELD_VALUE_LIMIT) + for i, value in enumerate(fields): + if i == 0: + name = f"Animated (Nitro required) ({len(animated)})" + else: + name = "\u200F" + embed.add_field(name=name, value=value.replace("\n", "")) + await ctx.send(embed=embed) + @checks.can_embed() @commands.command(name='help') async def _help(self, ctx, *, command: str = None): @@ -227,54 +279,6 @@ def repl(obj): for page in pages: await destination.send(page) - - @commands.guild_only() - @commands.command(name="emojiinfo") - async def emoji_info(self, ctx: NabCtx, *, emoji: discord.Emoji=None): - """Shows information about an emoji, or shows all emojis. - - If the command is used with no arguments, all the server emojis are shown. - - If a emoji, it's id or name is provided, it will show more information about it. - - Only emojis in the current servers can be checked.""" - if emoji is not None: - embed = discord.Embed(title=emoji.name, timestamp=emoji.created_at, color=discord.Color.blurple()) - embed.set_thumbnail(url=emoji.url) - embed.set_footer(text="Created at") - embed.add_field(name="ID", value=emoji.id) - embed.add_field(name="Usage", value=f"`{emoji}`") - embed.add_field(name="Attributes", inline=False, - value=f"{ctx.tick(emoji.managed)} Twitch managed\n" - f"{ctx.tick(emoji.require_colons)} Requires colons\n" - f"{ctx.tick(len(emoji.roles) > 0)} Role limited") - else: - emojis: List[discord.Emoji] = ctx.guild.emojis - if not emojis: - return await ctx.send("This server has no custom emojis.") - normal = [str(e) for e in emojis if not e.animated] - animated = [str(e) for e in emojis if e.animated] - embed = discord.Embed(title="Custom Emojis", color=discord.Color.blurple()) - if normal: - emojis_str = "\n".join(normal) - fields = split_message(emojis_str, FIELD_VALUE_LIMIT) - for i, value in enumerate(fields): - if i == 0: - name = f"Regular ({len(normal)})" - else: - name = "\u200F" - embed.add_field(name=name, value=value.replace("\n", "")) - if animated: - emojis_str = "\n".join(animated) - fields = split_message(emojis_str, FIELD_VALUE_LIMIT) - for i, value in enumerate(fields): - if i == 0: - name = f"Animated (Nitro required) ({len(animated)})" - else: - name = "\u200F" - embed.add_field(name=name, value=value.replace("\n", "")) - await ctx.send(embed=embed) - @commands.guild_only() @commands.command() @checks.can_embed() @@ -345,9 +349,12 @@ async def uptime(self, ctx): async def userinfo(self, ctx, *, user: str = None): """Shows a user's information. - About user statutes: + If no user is provided, it shows your own information. + + About user statuses: + - Server Owner: Owner of the server - - Server Admin: User with Administrator permission + - Server Admin: User with `Administrator` permission - Server Moderator: User with `Manage Server` permissions. - Channel Moderator: User with `Manage Channels` permissions in at least one channel.""" if user is None: diff --git a/docs/assets/images/commands/commands.png b/docs/assets/images/commands/commands.png new file mode 100644 index 0000000000000000000000000000000000000000..915f8038ebfe990066241769a36c48a19f249272 GIT binary patch literal 36403 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 E3*oBJ%m4rY literal 0 HcmV?d00001 diff --git a/docs/assets/images/commands/emojiinfo_1.png b/docs/assets/images/commands/emojiinfo_1.png new file mode 100644 index 0000000000000000000000000000000000000000..978327adbd6ab74642e1f93509c25ba8eff11de4 GIT binary patch literal 31645 zcmbrlb9AIZ_bwWB(BVvMClgx}PbSs`6Wg}k;l!F)6Wg|}iEZ1~?fJgnx%d8a*7@Vq zT8;PZz22(cwejqyszQIri6aB?fDjN6$dVG@6(Jy?RKVZI;i16qcTdGz;15VgMe%PC z6%zzU;0qX2VOe1ai0Wv>SA78Z8o^FN-4Ox;rT5lfl9;dC! z)&8#b9jMzti*W7kw{vKjjN|`QAezyu0Ba)&8Ri<6u=iLrcwR ze@do}b~pq*h4*DZO0CQo(!=RSp`jTL-luxUQ5S0bRv}w)a%UDyOvycoPCvRNWLDPv zvvR@*D;KTCP2Ha)c0XOlRokF;xW3qpzXcaOPfl7+GVvy0EBIu1IaweHXk5$(IAf^1 zOf7nD^9x{s=ZeH9WeYR8$trRubY}mA%M&&-C5ul%SJm_*8~Q5L9@Ww^p1N=F`6XqT zwxpD1*nCv91;sVAX4Y}K;9?9ONlU@Pqt9AXBpy6hC>BvcQ)s)N$FmXi;;>8@M37x}yt zEp2jm8OON(&Q1SN64sH{oS$}^-ws`>elEoN%DY%%j%QwcOn0MkXtA;~yYOKsz5>z% zu3q0lGynL$lpp;#<>CjGwl2u+xMR0}^qM!*T3e}okv11N?Q!>dK73m9ns>c(p!*=} zilVHiZR{ffHV~*Z9o@;!D&8X}ACvyOgUG6L>;C#3NBO<}--H_uXQ#4KsEUuL2T;U_ zg=cTMArBVH{h?=HklBrZ0(k3PC$#7yn1*#- zhy{%rRiJH~c zfJuIy}l;t+515aKe>6j!b@OPirZcOSbf$AayyS>q(C*(f`xS3~1oQKH$@FX>AYq z($Mbf?BZp!*bJ%Jdg>)DZMp*3X8pCDq|Lx2*kg?IsD|-_wyJbX!FcM5%`-|>i(I?s z=E>assQIk_AWS~-n^`3+-{WUq1Eng|ti+9TP1ri0vJS86 zKTS@&IG4tn7Yn9))Y6Rv&2HP*Y{#n8aTt&A$u+(@E!o$rQz6=F5ARVMG*x%{ELBuG zV3Ygh8zYR_^uFzW@#2c4%8*$77ha&c*V_3u^ez6OBGuZaEH9VQreXx$m^{I1!q>fE zzTfKYCB(lXLrU>(Uiqdm{%6v(vMOhd%T1It0uZ^ltgV}YYQWTn?(-+_gCjxi*15{A z_4WNU?CWoGgq<5Q*d02Mw24C zs*Y+J>lcBiJel6b;^h_(f_b?eF2?G5G}rjOR!@ty(h}_@8l8b{8B*As1j{$e8sk)y zr6x^gDuNMIov-3j+QMp+zlLYU)|ao9hY=8jZHPXx$eDJ=q+*vKCU!irFWTRzlB$x# zZ00f)UT@Peo>9_No%E($x5dPHU@A=W&!2n_Om_YS;WpF%u9WgTpp(&%k zFcw#&)9+6`>~F?@r$0ZmpK%RQz|}p311W$=y64_*_NY};xxhmT1LR>H!f{)j)eeuh z!`S#A%DRUJfz>5n;Gprd$9r6VTxJTPG5VxnU*0vVNXKCul zkmAA!j{#m;k&9VnZYE}K-c>stU(HEoY2=%(Dwp@bZOmVIdurRl-u(9<%4rv(N6S|m zSlX%!Cqfr#V&%=7_)m|xyht({vzU)}UQd3Zf8SYFEao8q>j0j=N~=NK)@sx0J{fM( z+rMhN29<#LALmxBF*jF|mIuA$6vO<57^s+MVnMJYGErRc$arsLBPgO7&*SE)+qi^x4 zeb_mdD-eJNkUotL9yk@hvlP)W@2!f8d5QxJ9c2v>e?pQHI`bIg3fn>lX*)unV~edb z`~tG+-6$t1{7$@IYGU|1W!92kmjM$VR25Mhocg=0%}N3(GNJZ&TY65;b|6ALMIZ$R z2fH;@aO+@*x*GmO20x*1$_6kWs{88JNAz6?aK8T1QrlR1X2{^egVYQaU%~&Z7QOcI z(BwdDCg#I~bk)&?8f{s55D-|CSLN@Py|ZJvNXbm;Y`q9`^wxVtxzfNF6z{rd_6hsk zZ)%yQa;%R-tU}1(9gc6|s@VtK{tT+>K4}urps$qC_b|T_q^ogV;~@WpsCM&yj60^2 zb%F6)fI?uCz;cF5dr3vxqx!U`Km5`nMkx;3GfH2FMFgb^as7c z$i#Xj&w623FfV57l~=6W_~`n9cVY1_VBj4o<%)?8x95T5Mzn#Gce{EuK5zCf(f68G z+bfS4-F3m_?`0c8)hxn2yv@}*w9 zpo@z_o*az7Iw3IuKF^nozSSG=dvfPkZLh~t_zgB#SbGYI=u=9&Z*HasgW;A9vi1lw zm?#`^H(VI5bHA^+SUA)C=VX4h1Zy;pDs!8_eAejbgtm^hyud2I;}S|*;`Jgz zzP$9Ws`CRfaymfb@L!1pSt(SN4dLBtzL7EZ11i}r!3D<%oHdWf3eZ7gK4jA>iYAGj z3D3);*oaYDg9i-Y5^7sMBn(QG=xa_rGr5tq#`b*kvSy=t+99vhs2O6Y8LdnoVwr{*?KPrNSNW?zPGKItZkIx9z77SKI<_jy`Q9NNOCa&xCCrlA?4Z!RzuED`68>JSz{dI+EG%<1drwzY^0numx&zwIv^S-(h#?b*2m@VH38;dfhi! zj%t~1!An&PmK(nEd63WN1J9~)+_bHh{wOFF4v$@tJe8+88|k3{Y$ark5b74Ni|1dGp@fzf8>Z@%Vpp=UeC zWvknpBm(XgW?R?f2LK9-NfV>aia*?!q%xAfGb%sbdh^OL$%vm%X5e{TMqZ7Bp~+4( zCm^)MzW{Xgs9e4jo<9&#Z^x>GZkdrImSsdsO-^=dW?AjSF^wwyE9-5yB6=dk_ zA~tjpwaLRTKC_}ZDCm7&T)IdY_&-WU%I|s}pc1Yu`emuA-FPaao`^_SIlEe`Rkg?| z@P$%Q(Az&KE2%iPpdu__=9c{BX9UM<{S&o$wed2YU*eSEV6l`m^`Q8Kq^wQF2Pe}s z|C;c!W{9(V%lO-x{zE0bcD+@)(H|}XEU;&TaYRZs2E4lB4-#x#LFXllw$&BQ;J2uX z`kb)3-+YC66oqw7b_1vC#rB{e%lL{yPa~dUjZlOL)JW#3A7S_QGf7)liN^ACXo$)f z8ONn;&clJfhJ`!q-$N-qu^F#Xbz?j3`Xs%y{?QsF@1nt~n|kRyp7RTHjEftX^pnY{ zIX%)@TcxfBIoeIJ=?U4{5y{C}8P5c8OKXxEa8%|-i}0|ecIDS9lFV3Hgy(1bdb#G7 z1Xy}}#dgvo&-HfyVOdDtWqlMuQ8p8&2A>uEr{kvmVX~1O z4Rg~zKM?RH!r{}m*vhBJ7+C!0QbyZ(8^;a?%^*8Jxm%Yb?=v-q{_1)8XJSxSOxJ8c83)DY_O<&F zVNG4)-lgJqM?{A+hMx~`hn*eHkha3Rda}kUSKDFju{VRIaIoeV+MjYaPbSas^vy|B zIcL}z6r%^n`KzugYC_o7A^$t0u~`b6TCR610K+pw$}w!Jprx=1xf=xg9}$f)%dy+l zuESn!3bq#+o^za}=e*O<)IrAuvZ!DA`^~+oih~=Zl7p_j*zP48;yx!%#4~DZwj2Nd z9?gfHyi?fb)AF8obxfB_!KX+_t<9Z#e(GjXGY%zkW%e^x4-=GHdT`R|kK||m7iF}FUerkn*17@4%=9#C&xYQkBml~!1i=PTQFE)W znnTFRxydId)J?Kz+e0MLSSPhROopbV?JAWlNqIa z{e}bjbGqx97HY_|^N?={e4Fp({(s0m;L-811-RK1+&k`XT-^YzEXN~k$R5}1^9Nu~xNITIb7?XqwSt-CVNi#qtf8#|_cT=&9l zm25x_`$~YoM#D04bb4AwymaOwSSi$%!RvZzRcAttt1}usTvO=YI9TsddqF_Ggd40# zM=;luuxP7W>BrE?!M-S$w$)~zA5m^mm2kT?mx3ILY&T~M`Cqz{!Vlh#A8DA025mEe zTh4M7M^$(2tU4?yu_bsS0_B%I>UB-k^c1kFwg>-{dX;gS^pSeA`iw)vYr~WL=l=`~ zL~_MMbLln5aC9n@k&v>?*s}9Yralh6hfeM~8gurDhf%vM&_J4>{ zig!u?lnN%cIu;fUlO}_iZMTt4g_mB|VGE|gIpgE8FW|pKYRuyU2K=Jku3Oh$t5kJ- zhT%55;Z6>dHd;et-@+Rm3E&?O>)+Q|A|&L#cSdfUL~FAKJDPN{+TmCUJO9D^{|c0M zeK7pbh(Q>?{zX6qpyGe*PmJcj_i^(7D{S!FAA_5oxJ2GsMp`0NRwAWN&vdC)tnJxX z#PF`V{DkXnFlga1ENi=WPTLqY)V8R)f&OC|&VUkZ%n?{IP$79&@6Ul>J_0et8P|%- zT9g{-s5wmzM7-99+;lm|eJwe39-Re#j{y-=>aQNYpjl~3jDSq_ipxD_U_R2*J~Z3^ zgBp<(H-GT0a;{s>4?nma6+XgYaGzmFE&R(-Tz(kbFb@kMfeh{Q!AV2rZN5e`394>e z<3Y02rTS2Jdvfpo?AyPO2I7`E%hb7WB!D5A!|{sBp}u z^C{l!*{TWJ?#w;#M@}-9rNS-mCjOXcRs2b^=tozHmzA`~7$GYmEqL2heIT+|kuHfq zW4?0!8rgV+&!)dME#jT9*xJ?jI+tI5!qg6`_ss}E??g;GZ+Vh_76)tjgHr+&5I|{n zBV8n=CuHO5%fEg)9=ls~YdE>V)#2`Dvk{xxklBDbcfdLD%*6M@x$t@k`|tK!@zXm? zv+*RD{BzMam%;drTXYKyl^WwwS9kmv3cJ$daX6l+NqsSiT{NqP3FQ zhW-8|p0}6Ls;gb^h}Xc!aI%C&+%Pv}=6kheSjqTg??)89m!y|Kmm(xsPs9lxuu zdi<*vMXKyKUZC#t3c3cQd30EsvDcMGz5fB-NhlgSJG{!gm*~mTerf1G64f8bwLrRE-=6b39;RFzi+z_r{mpEFBDMBiNHp%>yeax$&!VX3C4bAMvTnHv z`{LiQ@DROw+M6CYY`6@08XSn4d}bHhR$TGyhrhGbu?W9?hORb>iEZ~%r>*^IuM=v* zol|8$>Ot2Yz3xK+AoumQEf(h9qnEboV6NI+XZYovY`)_wC$V6^ge)ks-j%55wZ8D` z8%hfIKTSnL)7atQWhEHvJErNY1x92gA~MCj{@{#O!;On`K9+i?&yiTpBu?g7-w>pw zAOp@;w!q3>X9q5_8m$Wr?U2}Jr?K$bmiEoZMe-~ZnSwTRxEMlVL+iPgmucKBxDxN~CtoM5S!$G2zngOVx5IMVZ--=uF1JU#M)|| zsVheyU3T5^1twves=Qa|s+5B*|N53;f=3G^6Y`&OvY8P&;x;m)KTw_?7B|qy2U--? zH2-i71Xw?QG|catJI~L-@{^-A0cBA0-A8?s`7{*@PY(ifz7_n*7DV4=kWZLQ)Pfi) zU$t~U8u+OH+Ii>$j5&iN8`6t@umaY38*1{lr&gZjnWTi7%!=hHhpCE(Y1Xgkq-jrv zFeJ*TvGF&RC|wwtdHYv*s;e3VY4Vgw_HE-inmYfX5@8q`J9uVu+|;gLlLBuzlQ~3Z z_pItF@5PWs)g2wQ3k-IHxtW>A9KqEgp*%$P&69S$G-!qO?>Dsp=5CY)Uo8#@yQ`=y zX|JHEL#8$H{Za#-cw{NTle1SD71CemBIB#q+< z(yXFq_7Qg$v7}1`z-~ImfjvC?S0;=8EADCuxC%~L*nH{_ zORw6(SX4?QHsNkBn>30ZjTU@|x#^h42AeTlJ3&FcMf2pJbMV6OF9k%!O|%#~w6H$f zJ9ZT-Z)Kbdh{;dOyoG2p+^4~9j}gv6%{=DW!WH{ANBPMNDTd>zYKfuBlb;>roiC+nb9ffhLJ)wpfASPS&vhg@;*T z;H$gF3I222v`UI0vAheU^jQb<%B!bacEW4~t@YiT`VM?o8E!+mQ@k$SRY`ic?&ea* z;opSABj$hR0?6yyfM$-ge$2_C`}5t<=ezJN579fP-^InH#l-^#yI6|hswod81ft?=$&m?>X$}{0aGH{Fp43tR zOOo9~z27D$Bt~WAMdYPLq@jDK+`I0ZJl(3QYI^jH4ardy2P#klY;C@4&}OxO15 z85u`i_%HV9>9B2;8yr#Lm4dl{;A?+z!($xiI&_*`;lf#PJ68RFjMpt0ujgVSn>^TqQywqk?6o zWRoOZ@G|AvGK%&f$rfqTPjA`9yT&q6D949}3W z){wDb1r;(y=++szoClYk@g7u6X8q3KwKACr`jNEV`qg;z`FruZYQriN36{lr~sIvEum9YPM#mAt9DU9@Os2ZEWi zUxf~vxuKhav5PMuMeJ>1)s;7x|uz6!_=F@A|c z4)Iv|b{%`JH%ug$-QXC+FKFCdQIRQ|=Hci_W7r$KPR!w8YI@M_`7knER8sPcP>DX+ ztgGXb-S$d`b-?sXoKC#K1MXW;H+;i`w}vULC#D^IA87{I_VzA6!Ej9{{}c8kt` ztEA;@(sDfJI5lEFFVm=kFq4Q^N|#_#k#19wXi_3Dqv#n{?-N%qoL(=S7ZlZ;Lje-< z+wn89*)gc}|5-6Z{YT1n^@EzJ~dF^iP>6l-lx zUM&@ES$Y}X4x^k%Ks5BUKrTIWtrlAXf`tVQ-my|W`%GQ^{*i?#Zt?h30&+q^c#LXB zc7A3-;a5>{N_A=XoZ#v2&X<)AZDB=`U(7w#)YQrim9z^?)B+=GL*-o3xqGdB5*J45 z4d?e{m#R%ghmW2@qzcPch&WBFPXbe16eLN~3rvD{t*+A=m3x;mQi1?c&~a5MY^D>* zEWO>`-TnQ5NjJKWZ&)ZDBUQw6ru32-4hhll$gi5)}vpqM}BJg@yh41)&WE z4Gr2Fh`>cBAmB@c|I&jeC@4rwCMG79XP?|p;ht2vuN^yzi#5(GAW3T)IW}^~TFNO= zsm0WwN2A($QN_*0+0~>vj{q52S!872UhZChjK|9Bjy=nfgfpFdp3MC45ct0& z%?2y2`%G?J7EYbM8_8eq(mHk9J1>5G+FW2ac6exYb!!wP#M4Nn5eTck*RaR|J{veR zd{bQirnm+58B*(;4&SQb_mnw=2w|(X_{YFrx4`nIz;QQGLQ$i#K1`>+JyExyXVfdF z!1SZ%nV!7aK=={^7!DTyVx(@7G&DnTw(dyAoB^!dT$`GuHyjRX=V;lHueIA1=~ z|3sK5f$_Fymg$7LJ2)JsrkyFSW<9d6PQ7ycig5KTb?qi$w*<7)w4Vk}*)0QaUWTr@ zCG3(Bt8uSu4jpUpE~_l6&;$LZezlN^n0H7zb#XR-RpdvqB*Ws%GZW+H%>@NPTu5ZXJ=<&Vd3QD)HA}` z{AneXfK1N}Zmg_5WZZcU4i5gtKTv7AY5x*79Qo?9vrkP;b(zM+#Yss?wY0PxA0L1I z{Mo?3pr)qg`uciPS5{UQoTPsK{E3N)`2zzD4MN;PM@PrS#buhCK8|%2F^M^30ByJa zC?mlAjQ>gN?NRGZw08gzP8=QM4xZ7_>>D*&#Mx3Lq0x51RWlsO1cs7d<8jw)ULd=8V*;$nlG7OIzJXE8>m6oU8NEKVbWm;oaizKR zW-J8{wPiB{t}wT~omZiA0rD{#R_wv8JnA|7-BWSTmFDvO?S)z{9UdMY!V~jcr)mXE zzeE%9YiP^9O@%2pfBd=c8?O0VjZpG+A2w0W14}N>g6clgFe>v)d4;5rK#DuS?g_f!{$>$`y?%6o9Ev}foV89sE?(L zAsqca?d1s9T-bdYMHUsCG}@`_rSZz8$Sw)WnK2)5{v(A6H9=3qq^y4@J!>&&|Z2 z+3Ny}1;Wq?>*@P;3H$T3VZKAV2U6VtTi6_6Ls?bf9Gh#ZW%RaE$NRB+`;7Tw@VGP zOLI83oz+*-+jdUC!pj{yqZ+nh7_#c?E$Z#g8yzkf?x!92woGJ#rBd&QASG~>^US%N zsjnaIsN_O?HMwMz!Q5*-9iiSDMl|cKW?weO=u^|y+};u&{qdS_m}pg%$}GL!oun%q znUaDV1zpp)xTqEb1=WjyfM5@9Xpo|M+$5ZTAx2QmP#;kbfpw7gP&zQ8)AwEWLKARS za46kiYEmdrC}ewkd$S42!td1&`|kF(3rdZWk`e$5>4yyviKUC7wxp3ZLy8oqcnjWP z$FhN8!}d?1D|F5CyHL{*@qErOJwnyh#}SMi9T54#4w~=I@)@P^mq%(1?)Vc)UizDl z0Pb=@*LOUbg_4ryb+;M{kf0{Lf~!=3Nk7tzgWJ9~HgvYfuroyB$ylMMQjwDGwekdW zU+}eQiRPMeK+Het5pDMH`MH>}C|MZEEOI8;FF0qOY%yh=3cP zkWg!CA0Jh?>N{8PuEYYMr+I;s$b{e^J^S(k z*~mfBSD!&|&a1u5Oj@n$!zg#B5NjwjwCAiEangb&9Ckih z&0mk-lIBYidis8ypWD=Ayw$Vsej_o}q9^RBpzshaGt17&q1&NSUwZl2z~7&@*!-~= z6gKkwEbPj`z(6{usbA9GQdkvH=vRcqAO0C#i5*TAja)p3ee}dm&ebI`v}_g9iJ%rk zjp?PPgqg+j8r~yJDotug5?NW#rR06g4p+q9KY&#~hkUKtSWA9V%BQ9H@{~5DLP*vBw=5y^Q{E&LI$~26sjPGqk zjgd`%__O919kD1)e2Cp+ahz?4X*fMesn`Ul_~lHri8y76fPiDMNk!x|`h(3+%+!{u zcLBNe@(y8gu=3Imsoa#cmqTl+7m3(e_i0}Tc?`5>S4DZKC*^4AT~dA*Se?N1#R_6gZ=S!9ln{I19g0zd64#KWq@7x|!3)=8xDvU-0+$7ZQi4 zB6ma3ll`jzZen7vkMbfSBI@e66kqHx5218WJd)T^O6&M8wlV4Tt6>I7o-_7t-3zN0 z!N7T%Ov6}2L)taCw$aS6;QBcN^EUEN{dL}kOX~Zq^|Bw196y^rH7W|FGdIc*k(al( zYGtbX<0X-30-E>Eux5h}FmkbvoO^E8Zgu!<067}DBntPDyLF0Xau;*Chq^9S*G@Mu zG%JZy@DRSG*Z#}l^J|5pq*K-h>=(4}FFi384@_4D{o`>GAPO zH8ohm04ge~;E)h}eEintW@8f*@NPuR3ugF6o3)gz6v7NXUT%H4`-$mSM0p#odhL&8 zZt_ZY0n=#b&F>dOzn^fWD|PZ8L(hdFkAZg&rn#i)JZ;;j)pMJzad(w8c2C!Pr7Go6 z8^bgO9D2RuD`!o zhX%WX;S%&ex+LLND>CYExnE|&*S^x-~C-!tQVt=GVEN6pleS>8cE ztWN)ut8fy<*r&<=Wy}eMNmhf9#~(yn?$7=BnNJ8b63?L?2@nE)*`%Y!D(%)M8#?JJ ztX1iC$)XG=tCjeby8B(ETL_bpnkI&(#sIi$Jhy3#d6EThtZzIQo>Gp-CppK!?ejK4 zj!Go>qS*N|eMe(?9fw`1q2`3~#ZGp3Y014c@r-fRyCUb_bMYA&&`4yYq{5j1hcA|wjOyc`am8bP zQYW`gsd-E9IOJZLH`Y;==rMd;Lm<8Kt)dd@1zTF$8v& zQ$Ad;o)ey@u-x$g{#hO&@e=C$7R^)*m%N9j+SqjQ>@Fhg{f5=uih)=nNgr*HySpYl zJJLHOX-Y~s7UP&sof*rIO)%Elauy@O25;UYo;jW?y+R3yX{5~R8p`L z>~%gIInWSRDO;>Lk-wjjQvDR@tB3^>cH29abqm>gyuY&y>=b;|s!XaV;%iNf6sr2* z0GuyY8!l96>p{|7x?YC^VV;Qh+weW9wbr*?J&Fw_3wlFkHfE$hGaZ1Wsw_5Y!zxJS z<50_mqLr3VJK7*umeywYcb;v21b~rKfe?0K!Jf3IhNckqj~V*|Id8W^F5Qa;Hq$Or z$kNkOt2JE+8J;$%taF%>6F%Z~h2weqAGwBE-v@QBqR8r_lqNC`db>)_Lz>sLdq23& zxb2~eD$LJUgLikIySi){8P#B5*eNJE##5kGoKH74_=mV`94daQh#ZtJwVC=%JewcU z35{vC*~|^B{RrGkvULKHkGL{VeV%UHo3{R)&JClrxOjunydk#@{Arv?F}RF5c<0Qz zBFT1NeR|5a%WdQCxb9%6YG{D6^%)*3w)%HW5>YaMF|&Z5L5p_Y&yFMV=8Y%K78Qw( zQ)28+-+qdU+Z@em&b+M79GU&F%=vJ-Eu~`o;44cI|5Ez8Rhz?PYte z)VF=v{mEl>`saX8tfH8U*Yya7`k#;xxT;T8Bofy`B?kuwa8a4y(g6|@(!;|8Ts}-s zPwRCL48R(>k*FIP8bZgB3u40$A|N5b05LEz0i|AEuUHHtxPk)U&6m8nQlc|$*i*=q=Cubl3wrhbYQlFg{CZH z9+7@TRmi5iTA0xoM`A0n!;!c(t&QhNY(hvUT#2UODkz63%PnVScDP)Rm{_T1eZd~2 zN}{}BL`tp+1feldZL;x?yvpY5zeoqH<6o~7nwJhYp~D*@IEBJ27b|+bqtG5_tI!&L z`v&Gv>U8PP?M=uKqH&-x;By93s^4bi+IsC?!=NTJ`SA$|wl9d;sjRGyZ|j28c~0i7 zR~xJcJG=p&59ivQK5sR~qhQsLk&|m{X|1lUb(?x@_aU^g;hIh+!i5BiN6DwA)O)Of zYTB_cvV5+yx4n1T@6S^kCTg&W5p6pq#yk0J`af0X8S8AtzrOa!rE>?lQthlR)n=1+ zLkg^S3#?~%tWSZ~dDvbXj9z@3T2HNi%00sbsPBvo06e1Z?ojUTQLgV%?r(S{y*;~L zfUq0w5~|Xo-@ixTbDjUq77-N{-Hi0>{CW8k$%`}q>Au4UFRv?hdUXo+KoIniE{(NJ zC_gyo%-36OB>b8$tM_x@EN7BIgrC$C9uS9}>~eK;A-aEFj|BScyKov^6Oc3p>| z)%C8f!QT~*miK(!oQ`SmZDDH4=7Jk{cPFT@|^MQfHGWt!P%}!ezn~n7J=ia$`QkGAn05-N(L7zTeq;Jxebm<>3 z?`g5ktj_NaiZYXG)egOkRTie*QAN19nCeAbd@}GqilxVlUJ{4);z#; zZhX@lE>mEGxZOFc0fVdY1zxZB%rB*cSeh7dW(noxI5Nb%+~E{+ZeA(IanPlC;Sbf` zzJ-N3ELb*8Ni?TDgaQi~HfKC+at(2G4{Z()Hb4slXe`UI`Qe4-Sz<;S{@zqt4U1b` z*Ep3kBm<{2D}6`}Qcme2mvKdy}Cibyk14T=lKN z!BwoKwlPjp(QB*^{G1(1)KwsaM~Z^36|z%~xUvHM zl#-eXg=k@B_UqR#zeF%yutA6ROB`I`=SB4N^pJeu3kui_#~JSoL?*?=K=}ZkbhWg| zE{WYl^}cT^#4th8Kpow8GVvIni;hyN?n;Ul+Ifooed=S=Dl#eZ5Ycst@>z3fDVt+2 zIFH~e!LeD*(QU{S_<-1%X>^jlfj40!plUDxFM1Rfc6zyQ#N}(k-)4HX*`SLc7gFHl z_i2`=+WNdC`{W*!{g)qyz$lEFLEw&KAzEjy$7~ug1(KL*8!y$9sH+< z!BPO~oDThI7M+rfTjSTl6 zr3Nl1Gx>L@Scz6wL$YiOXhyL{+30$I4)XGia|?Cl|M{gsVQy|fVHOyMfIj9cv=RAS zA1}9dca1LVk=P+@;UM1O;XHK(krg*3ns2vA6QR~u;jRTW<365yTG`7UNr?IFMbIs==@ATEvf0e|R& zfDrBrT3{A2_%~X*Ua&jfy$v!0z*)cfLvL(c2NAuyq*a+w19}?Awm8Nl&!D&4bYkrK zhxDO#;ASEBI@8QkXL+oZTInj<=n`vGIg5Y$aOO!EQZy@nEmLJoR&qp2U>k)ymV|gM z`?!J4?`d|=o^UVFy}W`e>d1HQsbcBRV?WJg6U@ebn`zy_6H(o&w8$h%MbzGztyk`M z@Fkk3vt~5ub<)Du!NdS)C#ScaK4BB(k`Wd5lJrwx^jbRi(7(D2C65S&2T-L4pYplH zO-nw1^KnRzGO#jv9V@UZYbk@q95Uh7&$hY#W-rcU4oB>r#e$B-sO`F|e6$vFK+LzK zX}{yMHrRvj8l^mkp}L+?d!g5}y=Nl)i510$Y~SS-XtDVSH3&Ke6K+s&gfWSb)Q}(= zI=bFcu5Ktb%+H^pSikxD`a(lP0|0;+CjWshUp+J+AUG%p6xH<`OvL@c9fAjasFbN; zqO-~AZZMOQnh(ceR#)$A?ozukxaAl5`9YWysL=47ml?;=-2|?Rz}?9?X7AV)ggB(bjh_RX%2-)3iA{Uq;QYrK6Kg+Djsj@2D^a?x3Q22F{@gubYvaXYz4hS4 z-3eEjPP>UHF_`Cu!;LE}q#jBa%H&U`Wd_vT00PAX-yX~Nz2C|k?~J1b2udsHTHXpS zUDiOJx~f@kqxy0n<71Z>nl#GW)k9qOVp_Yw`?J;4)5Ks4{KPzhMZTxo%vsZ<>0`@% zXMv0bC)?#9!h6%}bYP;#g%&}L{S=J2(s(u|{QKqh&d$utjB14zf)mQC>-($wB~$t) zKnIS3g2IoCvnJyb(Om+?Z=}ZFMKt}j^zG;@bnsR0d$F!{>P0!L z9EQ@}<~|YOIgV(4aJ(^Rsq(l6wuFx2NzfuSk%23zhPzvd1pBXnyto7mmE#pQn-3h> z3Rt<-DtYyZ)P8oeWQ&$FDMhRrw5&1c$WDxr*mR8 zws&rm=a#@nz*^Joe>D@QTso!iNNNhe5$T8{9KmRB=~2pYNxC3HV*L>!R8g7o0kGEA z;8B=Z42)rL)~M9qAY`(dzFo12F4)@gc)CyK;}hehu6tX0Jf2H;&#i=`W{vP@e0}JO z{To5>_P08YGs>SM7_|Xev!M_$(R#s+LphOvOZ-4Ic<#jz*uQdU0(p$;RH5B?h z+%r~Io^Nbrm6w|f9lPYmBH+j+nd*&!6xRg}zX4w*M3h|GjO3uGU#EtfgV zkYON1I>?%;?2zevCR_72@DHUck7I012(QY8D@~^*fK$>NuAy8SRpWU(!g=N4S#y<( z`*8c7a}#W(8tHJrc2;3(p)J4n^&`%vG{LHk*4ei)F13vf`3wgc7nRx?a7|y8+fd`^ z|LNx(a)A(9#!mf8DbtP!Qcoq_-}(89OCkF$kO&|M&cShkQK!vKpBS9t9PRC^Yips2 ziHDj)RP@xGR+B2$xz5_<(^}-#JX;T@w*wY~6i*HAio(t>qJ)_@pj;zLw^jOwVO-CIHUmCn)9;$BVz#3iJ$` zm;wAdg-AMc#iMo3bHOKxc8BAIK*hzyqlpZf^%j_JyqnSJG?z(_n1FL{6-dlOJAZJ% z03RlDO#y<~MgzoYks0H%u@4O+#@&;-}xZ(sx#nM!H52F0@9qlI0&w$LY(xRX` z_P{TO%^2Q$Uwo=@P;?xr5=W2y@2=YJM&0pUhPcwJ%5?J=t0F3Cl1;PV8xf!_DY!bL z|0G8?rDx{(;|RLYP!ohiVHtr;l*pC8CBS1VC8wQWl-}D1u1rZ>f5HfYH(`ZwRQHek z+%1&&hgO(5iF}MT$GcVhQ61|9O5DS^_cG?F^z$l1tF<~=m3bq z=RzTn@|y;)w{<%h8Bu)k$3UL-Qtm{+t~Hl`YvHx(X|$r@8LIl%5Oum18QsE= z@~Sf8VsfMK^Pe@NFY$oaVqpv)gOtp0yZ}wB^Zm45{A8=nnjI%mTB;22OY@qnSttmE zne=4T=Q5L<8h zArXB)e6eCJxldakP&1!EtK`_*a{={Z#+VaI2hO}ocwy)LV8peHmF(V&%pSth>9V~{1Zg73 z2p<=pD9+&JBIku4gKh)&>jdfx4IUCt*rxs-jm=r~MiYgTyv)GBc%jGxx=C z!Rg>WtTy_*e11V8uasEGaCkW?G!;J%4j&l@Az=wnhkX3}f0mYjqboR=hK;Q4h1@(u zOiT={1;-Dc@vcn(>*v`66B`o~+Mu$eMrwTcz=VuNL9%^*gQ2#}VBE1lU9B^>?LrC` z;iuk-dkfYR8dmBF7fCvPRDJ>!d1d5ZLEpy%%B zTTdP7!j0^1?i^1m!`cr?8%OwI!zRlJnnLdJmrX$%N_{FJe7zhsBO6+P9%Ir){LHeHilW&0-3$1dlvy+>AuHaQ~#zlCQNSobB%fH*F#9=Ev)(#!L zXK&4^;M+EoZ@mq=8>k;waL{wakF%dJ$c5uPp5t%_;+J&vlw+NAghsZuz2}F2vG~k* zHYuB|aLId{MU9Rchr%nndDtBGtRJHfoo@|5n>REBacLCN`CMpjXFw?MYZHN>je7X? zPRFfBcp2#XqMBb^-~PfR=x^=_4kRIL7l2kt0-WA!b9I!RnH@&3gu@cF$tpa9V~?53 z%i>$}hG}Boz@18=fA74HIJ>^|oA0P^YNFs@sP*&lP{-ATR{{}a^O|@fH2OiC;M}vF zpY2&bwtefRkcVZJ_TWLkROmYi3!6hg4csP4N@{Oz!8pr{mKPTTY5z{jSiE``S~bDS z%+hl|7D1$H`n+{=Kn#Z?nn1{6l!vLgLrC^?kvdF^T-%9^8%iYZMv0vPHhi1kN$Lzb zew(ehSfGgXFN-CM>sZTH5ht5rpiF!qbC_|Tk;;EL@q^$uxIWw&TW9rc|4|7v0fDhQPO@DN~Yp zl6rBG`p`^{`cx}xRCszQQ-WxcsAp}UPNHh3MqUW*Rv>6bSeb{J+Z+z%;}#Bac^Lhq zT*6p(u%ZKmAi@9xS3r;1wl=UbqpI;WzrNZkyx5W=)jYerJ#Cv45Ym z${P)TW~#cn4)<5IA7n_WmoUZ>GyW(Y;5>^`ar|Kp>?b*O6&0A$-1KxJZNyz%Lc+cE zb(nFu&g1~Fz(-Y8*ypXiy%%9rG)zn+B>j9@L~ni`gFF00jMfswiX7}bey^ynU(auy zoXV$Zj^v|WTM$5J0fEgH z|0nN+AIcem2z1pjt;oeN+K%Jdd}rlogS%XuoSjHbp68~MKVwai-#KQZlkzw_o!hd~ z-Ov8HEN*y^w3$E2IwhbCFvjcn^XU=g+dh-U9xp%l-BHmYaA_$wGGuK8sc6(UYFkzl z6OP_G1RV~!AD8j&E&lf&#Q`=6<|d|B>J}GQ>y4 zmgZ*k>o|Yx!cNNd&PyBZcb;YrK=^XCi0@iL9=06zWvrzJ>Bpa^e>j7G&*o;q^jIMy zLqA9FqQE#(TGN%eni_I=`3fY-8L;NtON{zp0Z68O@1#E-0*)9e`g zcM6G)ZTaKdZkZlg1r~_PY-XwppUAvXKiP4jAUdB~ERqI^OY@T5t!1stC$?J2^Jv~E z86x@NPq_GWVwDixd`R#=Z5=4t5 z{dD)oC{~wYbmbNK-@73a!V$nx?*fk6RJoF-R(kO;C75x)T!B@JHf6A+U2<{vr{v&R zV2T#*z`WEU;io#j4;+jxZ^BD2f6BVx)IqqTm=Z!Y7@0JvnPfW$6-HStW6EGq`GV=;kx16XfSR0Yls1-6ZwNYJ3AZE83c-U29a>;*)Rg{SnNzdEHlL)M zvF!9oH(sF!N^Y_{WX}B6UWx`^V#-We~dl7)-jvxc-w6Wkk}H zC4Nme47MCijh03=&&YNSJgFsC#Ol%H@`LA>R-*`p?{JFnahKTqvGQy zWF{7BEl>xe0PQzD4atSS6!G(WN;H@6b+Y~PL)T|>dFR26a?S1E$uHG3mwH_h20dEa z_A;M>Mwj{qzjbJV=%vVDYm)Ql(Qu5YjOa(}D{I2HRue51WaV{oZMr5m;>rR_fyBQej~D5wHY^ zvdgp6+d1jD(Mwv$A>h=ixuz*4xpRZRkIZ}@oct^^NgItlu4L*Tp!IKsd=ZnYHt%G} z*C+q+BL6myEhv$qYawFYN}SZVKc87qSq;qmKIe})dNX}l+v+q(SZ?=Duz<8MUvtpMUnAN?b{+)SUODEA?j!P)@Y44N8wBj*nTs zQ4WVCrtro34p>VuF=#*<5B(G3BNbp`=Iw!|g%i|Z1V~6p;oN|%o?jKjl;C^!cy~Sf zc!Rr}zL(TOR81Uiwkv$Ic924d;QirYZDzMZ z9Q_+*9Pt7i@hYT|pW;lz7D^viu}u?)>fQZg2v4R%6!FgD@K9okoKTE;y@F}z;+9-u zKAmZc+T=q7QA3*gt0pEFJ6IHyX4@@ujJMqGNNuS412MyB5+IoGV3^=Z_YL&kE<38$ zGla{FruXaW{87Mu*g!68>WA}Jg$BmO<^KJfn_T$4=>MGfWv&jIC|#<@VF{HM?J-^M*_!zB9f@w&?Spyt$n`CfGf{ zy&YOh8jx0AsH53gnt2K^nE0jDzM!BalN@idBheSj@h{#1%_zZkLP1`eBgh51US;$A zc(*s6GcqxOqyxU??jIXNe?)Q?^&Ano%@W-M1*95%1h32MsZU+n5I5D3!pkNay5*nU z{WhUQhr2z-lqtu=ur+M2_xe5T&}$mt>BGHa&QoQmA;JxsTVDywzY@RsHfE55DHDC* z_0zsEKJ4xE)6E)rI7Wk-Aefn>?pt;Bo-{?m+~k7m`RCu2VzyE#cmg++LIHQX#_?gW zFbH9p^oT^(`%xo&Q6nM9@=DvdA52WMhDM&1vTjnDmr&tyvFtT{G*8%I*=ez5&FHPU zoQO3(qhwQ7rQouXn2S1c7jS77Yorz=;ufE$&=wv}7az_rH{|QtDC+Gi=aGt?eiofn zqlIst3|XAKJ(whtOA>mY@&C;Pi7|&vkfo4GaN)ycIT-7m`Y|- z!7@6wYg4#i#z^?7*xgohGWCa}1-mfpTa(FLeJGlsZ?+2uf~AEs@zT^E9}c`UI8>Bd z>7n9LLJY-x7YuV=BU!>s@H##V9^03>uJ8?g{HHFP`u3V5#9ff)H5I}prc738O2?qA z;`H;Z`SVEK&*g?#BAxEVX4-#!5>mU!JmlYOT)f7|HdFBnxzrPCKc6GsG@eMXUP#Q|1ZW2sXnTuVZ=EHuOcfEVD&I*n&XqntsF@wuyE?3CsOzQo5 zC$!-10M&BLfY(oHuK1hQ7yRsGPRHLKC>eaTHWRtnIc_5pBU{DPwu4HZo)#mSqx;)3 z4}End%-LU#90qyDCEZ=Z=C@@y&iovIBjd@4izjDix4G;oC2Y0){5hJ=4p$fq)LR0D z>Oj3BP%*sjfJlF)b?1fvYH4W|Y2wcI2@rW1Lcp?Go%6M=p!9xDmQd@{L>WTIW+&uX zU->@8OI>8-*&}HtMz(4v)6&Y{lC^f-COBzO2jBX9DJiOanUy#ipf7q~Ste;-!XiYY%~z~|Oja4{ZSwMNo%b{PIWn0``!YCy$6 zRY6lvj6j=BU*%(Xq(NldR2aYvh@Hv|T}(@+jZj_?ms!^Ts4K6atD~*43kDvn5r8%8JH!pjpBO3)>d+Aka1ua{#i zQ4Faryo{BNiEd#g8AIgX?C4sGkbds)(801 zGfk1n^jSpYi-!jg6x0wA-!rG8rIm=6M7X?GciarEXXua<8+T#W4gSXuS%4-MKa`6@zQ@?WI%B9fEvnfWm;62^)P=ih zUCpqIS`Z}Pd^gWgJ7!{qnnKu^hTCXTXk}(*8W9~yvB|#JAqj<l;-;SJbN?%p&euC^F2g=r9~fIksnV<+b1pg$LMIx8xU z^@L+~fkZ?)nwoe#J>C=V(LzujwY-)dZ*JwYj*yxwf&>F!RyonH!WXVi?6!I(Zjq0^ z!rfMtpP;WEDb4!5Pd@HFZTlWI#4*O;40cA$UzZcV07ULNsjN zFmpgjj#tKjP9=6;@ym>1_LzRi?})si^i++sP<<#Ebv81yh&-)yQ(#q)<~Kjh5I1+< z0*~77KO>X0zP{sscS#+)KDWd@5W%e>Km5d77s2Oo!1gnXV#1=svV=k$u-3T~ zULqqSVSfWf+t3h9ssNzaE}xwj28sxYT{l`6o4!uKL;fJME`O$BpsT4QBk@s2TU$Y2 zUrJL+T1!h*RaQz$Q&&n#Urp|#0-5#~SKUNT401+NX&?=iLaZM)cNE60ZyDFUYO?v& zDO=MXUWl>fyE%&w&T0M5O6kr)Brme_ibG1{~bzLc;1Ume_CprxiixGRaI| z(z&0G={j{jg+xBzNNk|Cjdc!d-v!>YU$5*B5yMh7TTO6?**>NGy#We*U}CPw>0%9> z{O#ZlQdIuMf%=cG!Grz%pMZbt4K(ZM`_A_9%ntb|oKGc1 zr&Y!IW&`H+x=mtnK}vMR)J67>_kDc;nKl+3ow~1&RFjG8cTy^L*Ry4q#ypTc**`ca zDg>NbHBC(dPXXiXurS2xYTLyRK=Jz74PTOzKj^=vD;492#QKulr22^Zw)FbUh?Ka9 zkcxz#;ep?q0wbCNtJCr$8WMg+R)^$96qbFje}~M@v@KBt?{)itIabdpuYUDm%N#=R zsaVSFyNuaaS(_4Rt14xi3Pm>6k}37VhS|cJse*>-e5c8R^T`3cPYjA|iN%RMVQ;na zGZkV!L>I%fCbFd!!N&)qb(AC}bd)&;lH6)dYK2Z{&1^?ZyOgG6X#$@Iq zFZ&wOSo$-Ly(5gGsQ|I$JA5-};}l_fauTLSG;j&>Zg6lASrDn}5nj#Q*cc`?f3T+q zZua&U=-an%UGweOzf1kc&XcZq04_M;OU~n%TY>N`-upJ_YS$`7M1q_8i@Miv$sN&E?G;l zzV3aDZ;&Gy4%kN~Dmr~vif1HrPHTQym#_&AKMY&q`Du$1emS2rhWfT*Y}GIXxjk-q`f9#l&{)J z;!%~CkKdbw?vw19Q;n0ei{p8<3rdZPT%F5sozo!hOX_<9$pyZ1i;Drq!>Z?Me~-k=n~bI}JCGl`0afmvgI1Gz7%TjW|}<{D@V5dq1? zW1$mS4w>6KH+ZIy19)ws-i7o+J{>$S9gVw1F8D>RP)M~kO3=GHGM;Z#NS^|Q98?oh zl>#&3@?;@O>6~#{S*@A5rGYUh<$*+~9mrYLRIL$s$lNCM7^>F(I`}#U>Y@m480^H{ zZ=DE)v9W%DZ$%@bMi_yL?=GO1H5+J{1ENs-5fg%NfevdB&-HzASe5tfvq&>MGsfbW3DxWJ;UyJehqZdUEW{W7#SHI<>;37>PE$IVZY(Q zdc#3*IhX1@qv6Q;8=)K$oTzVjyM&MI`Ovtxud3h=eqm%e@w>E?*n2_Tnl6$Dzx1GX zbQa0u6S87lx@MiiK2-nP9#W@O{*s#di}bd2*{I!@Uc`ss2dK^S98^UP&o*AFuB}(L zl`Z7+g?IGqzfg08NYs}v1jeEyOz1r(R9KJ?lGqDHv1_n#Bl!Z>WL8$v3F<*#2c?50 zFG!r%8KRs55za9<;dN>F`Q4?k{^^g#`2GMBdFt%*Wqi^h{*=D|iRb`%o%$i42tdrg99!g6sLj5bEND!M zQa|>iBR;H)6`nk^#%v&HJGi%4F$<`gOJmXjMH>?*WM-m7R|AFyYJ{K9T<1FkN9LFa ztoIkg2$ut0n}bs`GjOf*)6+O7jz2MbdIm6RmZXC)uxV-2wzo^cXMdnfjl7B!p2v_Q ziFcx-jyhTmccjQ|1kl^}fUUrOvKHXGxuD~)r;V-TM=#9r#t@gqQqoMQ>3z7ehL-x> zMo<7UpzuNTuRMT!6t(quY(fr$5Ul|P>fRnO{U2(^Wp;7(X?fY%#5`#(UQY=gibnz& z6f~t#L~H>e9$~xp^kOtjh^g&cwPi%*wiS5Pa@C#7eAVf6VG(ok+Y{gL)k{GPUvvm{@C-rc!>G;kn zSq)n19oB@k;YVPSus4tqvY16}4RPB)FLGj4UqHINwX;bEkW42KIVLUV5v{bF8N`XA znK-mV48jmk9#c~2jrS%pxkwotS6UQ-+d6`bLIMYJ;5QZay{%3eoGqP!P|+x%p+>_) zt7tCvyB}(~8o*Jsn8@1APX1jjUOp&u8Y3QP$kkI&}CS65waF*OV!+;*pW6 z+m^mam#lPpDbIZ+S@czIYRBky*PodYn1sp26<(8}7g0P_hi4eG`?g+FUIC`3$Ll8F zwhT?JE;-tkJOa09X4UTFNU#o-d>6bMbMc7pZXgza!PaD<1HZp zq1{r)p)YmBO#E6fTcbzl6`Qeq8O*V0o+ts6HjKKE_K(t1FCg>)czY(;OmIL0r!jWi zGss5(2%2E{`LH5LMpDgd*=jstL|_O6c~qHE`* z5S7&lJnB>w9A`2~t(xpw<~K5OOVkYH#9W&mzE^P(=6oHXWS+1m*iB4Gt|_?i#$WXU zulZEoOtr0EclfmGdgoPr)z>(HdOTpDAS8hpCav!gjD;^lOsQM(u&Q^I86D4;>yzqX zu&Sxw@Pcf&%uiwZ2rBwv#niLo3;D4=dbnx)CSM>V__|F~yq9pu>~4y}B!-<^=7*Zq z6FGy*|H1;B9kd>t)YTfi z<>~VfR9#)2@(#2F8Nyet3>|;?R~;*8yz^nHE`Q9n2ed8NONptres;9rADCsJsM$F_ z;MrJrVXIYyApfnBa`<+awd1#j`O3%E=yRF@18S{>$nhGixCARMg8Z~U+44^*Z}1U> zhyEVl&MmF1K3LN3Fcd;wf8aOs0Hg{LsEzi4U+J7pR`E=~!+$uoAf}Z%n!=Vst9A5r z3a@8mCZ_6$_{Ca%Kl*@%Q0<=_&F9nDX?c8-%i%5p)}dA(wUlfy%mp0m zKW^?oRQURpC+C&DdGtz(8LFzsw+NXB--FNGtIwTkQ7{y|M$f7jMlL%V(Y} z_&vZC0>E-ToeGn4320F_+D*JC|IVocL>6W)?v0n?oC3xg+Jq(^4I7_kW=dC(wnzz= zgtU#F1bUACkd@Lk3O4~8ig=`=R$(w@uH2w!%2Sa zgv?||j<-6CF-{xE?=7T^*0l&v)Y3?8j`9xGrpB?>j#9+nig1ZGGWGm(&;BcH1T+>O zDcQmgmk@%!TR<~_9mS5&zNpxY@_KRtXXh=gC<+;(Wl)V^Kz^E`X3E`e#yIr`%AG$b zOTC_@P)`_nSD0*2N6_f>nKPBN>qv4+Y=A?d{JpuyAE&WYi+Z#7$XntGqLNVMIv;_* zkvi+bfK=N+vYp(P{D03J(UhMpMis-6c1$qGSSOBYwZQcG$QBatowdn1=&PB?p`fq* zYPFgW!lV!$Te9X$!XuzIm;~@Q-g{7lf;O5V%F4>T0mv2^{AY8XgUWP{wqTw%Lp;hL z67|i3p_{t$NN(-MNsVv;0TXLm|7X@j_q$RPA_&=a!kBg zSL(ehcPb>FlIJ#~uOw;9BPEcl|8ij6p1&Q;cjp@oYG*nNV8fOJHHxCZcRGP(poMa; z&L%(tW5YsrHU+t3NR`HsxY$a}^vqt;lH{fDIh)m-2E%L!!3OzgHVf+6zBop8h1UF) zB*4jN*~I}TPs61mk&*DvFKOm=#CwmDuOWHvMB-)cDTQc2O?_{!Q7Qb9S&>$Sj5E9 zkWV&;fyX!3*I}=BZfa`o_n%2v(~M)Ci@?;_wM^0uuHX6rM>M#YoA-Yvz}ztck$4fe zmoVY<`k3M$GV$qXXv|N{b2ke)Ml|2esCtH`-R@eLea0NO$4p~HOIr=#Z?KW}%<0f> z51w>>zerC~`xT{@WjQ+v9wZs%6^6H*J%)Uv%LiO9qEXvI8@)_ zq7=5|L0`^(2iJX=!|gr6qg-I876w##gb_t)s=XYYB;d|^0gSWYhK7z%>*`4NkfP7E-$qR_4`QJgKD$24x|9#zjbjIVnpW;% ze%d>#q%^G)2F|Hag`lL6l92Ss#>Uu&2GsxL5fSQf%A9w~P}AA!H*l)9UV?d=H!BQ} zy<-kC)rh%_qr$}65{H9$`glRNN?TjQfo@RbYBcR%-dw90gOez(wRNzt7b5E-4hR$+ z^l#KW(=(K16cAzcx|!HqF6|TUpvX}%2-taF-bAx>C4%QD`B49upVEbl2bcFN=An}I z02>cc9qfw=et~;tq^F0bR#S(9;DmTZnerIn(U9J~>qVC6=RDZC3>E#kR2Tf6sSD53 zG7pcbJA%9iZV7lG#ZUKki)q?c)(gY=gBP;*y*_19M*Q0*!pfSfT+Hae?Jbm57X>Va zK##Pfpf$=5@aU9!Ya*3MHOzk*yw=Mj0Ro>C9H1@W9AW{k@xQqRA_srVvg0wx9mooL z#g!#2vaEmFql?AxQE=(PIeUu5j@<~B8{gYIZh862a}Nq&(iKe|W@c9BJbqyot5x3b z;s3Pv!cdU-%dHI0-ZcXvIR7m{A?OJs-8lkBtP92BlJu%Z`E=UWz(xPTt$wJrGi98X z0V-JZeuV^6`Ju>R`OP1g<1RU(j}Ebv3edCdH6Yk}{sDU-Nes$%mn9HMx$9n?1f)Ma zCI+G0V8RedB5PT=7Kl`BUN~Z-@cevM@}?#O**`r_Q7yFDG4?4Q6RI64c)c_$ogA1y(At% zx}MGBVba^b*l+=t6Eh!Owo&wpO5d24v_B+3O|4BkC-w3S$(W;i>Vh!aOVSvi-5Y- z_8(7f*8l_0Tl?XSp759ZL)NxS2!B?to2GQ|zqoF9JXjK#7dLL9zE*UM6f!k10ICaq z#nisS_0%q!C^yaLT&MH!j@y=%xD2&MjkJ1=lzAs5=>EV)Oi&Mun1hh&7Fw1Au4LGQ zWsYWxu+`@R15xUc4L;)~SP*8uncq}I0%2y>1s|iRJ&A6ckHVf`+~s$u@Diy&rwlQ; zzkC6gm7VxfRw|7fr-o;E6~}QSgGI0oul@lp6ogp=8>UBv?HPUbfEmPIGXj0bg($5@>!^xI&>4@UXTg1IHc=9=x83kgyJM z1=o4ieeFbTmClxV775{fQvpk`x4;K{tx&a)qJjd7K$igg(-IVxAY}DXaG&%`XUk&Xov6Yg9cdXy*`qFrQDjX1m?u4L^j>>4QSANbBP`iZXp^AE7`!6A{) zU-QLi!m2Zjmv=z^jcp?sWp;8JVPribzN#2e@KX|~>d7e|R9CDNIKP95jf<0PQjDA| z0;CsSu-JM?N!kwLFV6}vnnCA(ppTVLXvrPfbc7dEyYK?tu2=-p1&~3&oi?^didOfn zGr=zQV|#`4zp4-sDNn}_9HL)B!F_TG=2Q_MaC#SzFKpp;xPwyieDGHHjR2}4o8t)j zo7H~?I(_0zF%Zk8h#B>Vrt*zt=sjrCqb74P0Qwqic@_5 zT0gj2C_*M{qm(_bZt8_avNJu20t~GVmxhB$qsvG;D@hyQ1H5z&2nrc&yt_|^_A9cp zZzOoeo0VsfYin30pfQ#tFTO~dxn!5~FIquo%U0=bVKA>sP^3oZu#dS|bJH@_6_L|% z$xLbEMyLe( zjZ=Ne@FE$*_W>C_;{kfD#Jk9Ezdeau_`w-s2;(gB$vI+Y-0?Ldw7 zi|6B0Eimy5HNrEBIm*$2HC>(=Wp$kXOTvIa&{6XFX#26w&aW2DGtnDH$FsOsI4>-{ zBd>0GZN+BvU+=#d{iNf~3om4w_db$Hr@sTr{s_?WaSHN1+@fyQT-e>U#O_Gk%xY1x zl-b{Y)JgwBM9%N0C~2Vl^sRPh=9@Tf8o3XK#=qu!PZRF%Z{p+f)U_kY>qyHmS>&&4 zqB85)3DJ~nL*f(C+nbVHUHij^`wV%QeGI(mfaxkyVm+(~b{%dBJw8+OkKX4kDgY!5S&oy{or0R@;FAOP32Qeb+tNIGdGrKJMO%pLrX# z;lc3_P@{3Q)F4W!BR*#F+cbgSCJQA9 zESezwdsu0Kv{;fuOL1O9#VUL=9IfmY&xld%qsmYnK+Ig8rU)ml$@dp#CnQP=$RG%8#PXNB=19YYghRxX3SH>fsXg1?_c;%;g zihYgd>(OF6$(^g79fUQH);f2zwsnVx<<2W0c2yw}vd?02;x+Rm%^>s#<&pn5xPH$Z z%e1M7ZIO2;(FKm>M~r?mZH1za_aZ66)mrf{C-=ptc(;DiuLNqq#ii2TwX%?*C|s_#MO1`UnE8xF2!*lthy z<)3;~t-go_o;Ed>I7{e&Q(vEFWdOEK2jF_gT*}V=!3E|JQqgb*0_~da{0#0q{#IfE zaL$XqEQ&SVv?32(B>$`gKjKAj?N4aJ!U4~nahr*)?<^*RW9OH8!;xYcPI~J4smBM@ zPhFNFH!ILHRFqL_KlrCWt$3Dq2EzPjPdIhT!|L7((VH_T#dWK2uosoh=8S3aj~K%ySuMgyN$ur!p^Kp!xz{!wgt_J*&(T9y ztO2_a$?goNO80e-+4D`0D4vY>0ThMGUY4oN1H=??8V~&8uQe!)={BvYrK64V_%VQe zjLOlAdUxu6IQFZiIZ6z5*}1xg{STpb_Fx9A2jn(q2zShsu2#d%KU7}j!v>rsKB21{ z_XNZ)a4szM3k4i3>1B=_y-~#H*KhwCt)Q#W`l2dp0q}%tbw9$!HFW&w%x$Uth{WiE zq~#L6{i_6Z>w#o8N2%^O&&f|Om)H(t&zO9(P8qjKz|OCYX9~Z(CTg75sP<6+0f$=J zz3+9nmQ4zko?lfbEUV(z&UWT5`&7&j7MPI!1Xx2XRV%xKQ=(U1e-9suIguvk_}ACvD|BpnR0j6ue3~~Z{ce6eVd1eVit0VZ zYkzHceuxcu|4%sthuBsGP06O`MsHe;D34Y!nZ0R<-zuJQ8ys+YJYL@d0{E~mZvY_ox!%?*F+2d_|9K?A z|FQwrF=rNkudKu}`##Q{YZ>%pDKnGj81!-h>|W^Y3!pDBC+AkL{@KazB=_$&D&_wg z!6Z@sI2U}YwuUz6e?Pan{O66{>(qqJ4j?>=3A2rqd5MKnanaUFG=+v*oM!6zY?XQM2>c!7&r> zmAw7p^ct(yQ-8SoTa{xVblOp$mXLA!0Q1>aRQ7e?!lMXqmJ;dy;h$LP`=_aPchx(_ zy50RC90*$TwGX&#Zn%&&QJ2>f<`l}&C^R)9(licxVI*nY(HC9`K>tLbNjO- z;LTHVxw;m}s=kWr6ri!=0n+PqtT;B+8zBnuD47SDL{ z=6M#PFZqA#_W{-~Ze?||hE87CJPCYkmhSg$_c7+j3D~Dz5Hs zYu;jB{MGek?bmKD>Ztyz3TEnuzxMKLUrstN{@z!`?zelsN~;QRM$kIlshSc8hK3!q zYz}KK17<0RT^{@AF^x*~d81y<*cS}IkDdhfnBdL5`e7?8IW3Yfa+uumW0a6|MeXzQ ziub=B+1K?`0*0_>ON7U*o9lJ)zobjps#HJ`Ah z^ZqTV^bi00exF>PyMo?!rd6Tea(&pdA^9V~uea1%IX^ewZf|WZzW4Hat6$IoOJ3YZ zdVU0xIo^n;?TNHQk>ZK&bw@sS!2Qqcl>Z;2p8roc>`#Stb#?9S6!Sd^k8e?yy{j?) z=ZZiXacpet*x1-lim+`=RbzQQo@(Pi!zC)O^IxDu4KOdTgqz+(Ng@*ey#RTn`@aq@ m`CnS%|9-N`$LXbqfOX~o@R0-NdtiDtjI5-RM5UN<(EkQun5#sH;`{Q*7mR!^VP{r0ivoD-rTCytIngaQKtgDxo{q67o;Rss6?2I(#Izb?X)4Rm

    Z1WX^gFV>gr*Y=3`WoE-ht%%yUV!A09 zlwx1$cktzLzd}N9v)G+X*^ps3w3YxjS31X|Ln-WLhLtVud{q@RfuC!9quwx9`ZH!*}fe@>5LacB}bH zJUqPBPOr41*O^~gb~*#yHpP}F{Z=#^8-H5Ow~VLF7c1omAwFn^lKU9bLnlFJZ9NCf z7q@;8tk=KH8Sp&q;2J;oABy`F;WP1LraH$!A5K1n`=`q5Tku^Q@IsRJgf(%qaXKz) ze>o}MJMDCFbs**UxHM*9NRx~cLnBos9>4iCg)QgHwB#$IFpQynMe74{#)VG=tH>J1D1jO={Y#l}r&n0)+HQM=rB-q*2F(>%>TxC;OCx>j$+l2MWIQANT zUhHz?latnY3zHvWemZ@yb)4y^p$8Zj{teZac(a3r#F8uJm0?*@HI6#=>uiu|yNOw3 zzG5{(R*jJ7$eZ$60mlLrO1s(TgaVND=hskhEn}k(*NFQX?|MO=c*M%f)0>!yT; zPHB-f45zhIuJ3!%P4`iIf<8+NSL74+DP}f2u6xN?PY8?J+TL4B-bEQ4m&w0D4>Fm5 zKS~dF3Wbt|KwQ%)Jm(o=b&U5_jjI@1u`**i$UZ2zN-TY>Kctm<3j0>SN>$}Dw5=@4 ziM!~%WzN}*snz4-{96cA@g;OTc%S#a**nY|4+iG1;#3C2^%?lXcYC2}zQW<2*~9N{ zumm_uCGvzvP5Vb1Wv+O6Nk}G5M`Ll^|qOqo$j=dxupTH39!H=hj zVE^q{la_sVf`y;b!r&5IY=T7tIp91?ywv$-qKV`e+=Mx+n%x9VB)#X3?Q_>D#aJq2Otr6(BYlhZag8$LRQj>j&Pu+p`=6%TM5$QQ^ zAvW{^QRYj7smT&MI7T+&W?d|c-PZ-NCA7{&QduOhLglQoSq-ur&f%Du+~=F)T;{w_#9H0 zIo|2HPmWKZL>a4RFjjH5YvFGyYJ8&+ZjMA;N+`-#i|^n4Sk_m-)>B;%`SJ*?ZEpp% zQAtw81*pY%K3=i!f7Bd3h@CpO+=&jbenim+28V?QlZVuA*>FMDGPIY{ObA_^!F`WT zO}49@{{BpllY^g_aGkgpIX4^^1zHr(=Q*Q9fNvw}%jo9Jj?KKkbIh$p$CN76_;};y z*gNT>33Ni^L~%X{rK(@t)i&e`_uFJpps{McOUhEv5n`hkMGd+ql6q00R= z1mcm-_Kwa@S@J|)>D8b%&zq!BD$EoSM+_Sxq^OAm90z7vtlqTdanR$relnl6r;(*v zIHD606CesL9!n~E4Zwy?WtJ=sjHoS74x}zO*b`pR{iKh3iEPBk2(VfH9li7ihs$n~ zC`{1vK`qQp4KCtD!#cNH;G(Hty-7d~WNDTY#lqvd^D-CPHdl5e zWo0y77tslWY}{CY_;N{Wvkg@-h!P5XG!IXOD77mttve zb6bww9cVVrS8pIhKD|zGDkuq*9AR_KjgMpjbmo?ZE+nA^aoFjXNN5fM#>iqE*5WINj9mTv|NX94Y*>$H8`7N@bW9br=U&*6G zDpf18VqG_NVa=J5rUtj`UfSXsvnW}JXs6gV@J8BV{C5_-Sqs#30T|zc@;K)E+OXF& z8h?*^ZvRp|eZ26eBNw%+A-Wm9^nQ8bp*;Apxb|Iexu)u|&tz0Ow2GA8??Lay8x-!~ zqxaZhd@|QPAKZBd;yZob?m)Kt!73o^x;MM~d@&Qo@?v#Bk`x;Bo485O`s9H5t{M^; z9IswSNhzkH@_quj>dDd7Hav_GoQ6Yls6BIq;*VB^2onCQ{5Q3Kn^s@OerPzaE|v`W z^hDPKrmh2g+lNP?GQFB6cpe`_brTKBa}jMk&7@xlUQt%=egBIYtm9I4Y}gZGR3Mm7 zW?FwKZ9#U-W8LY|8bo$Hd%=m<(&Fy^)97%E8;GayViL!6e4b%2 zU$fdKUR&L}ocr_H2(jzJwF@iHrt_C`Y$19*375xAakl(h9UXqX6hkg*>S+qeIkOEJ z$4K#r9LU0e^(}*QIOG7+Gn&TceL1ZG_$fBYiZ#E)PFWr6XTS~nu-*8(3oK{uoKdEN zuD1)DXjQJFv4vNS%z|6P7RSfRCFxTK!(_ZY%A{jMeXy+@;YM!pC~N>EuO@byOsx&imQ1c5y?3@n#7)d2P#UQNaxvJV9FBL)1MfMh}q(!8H3g zgo~qJfxI*2lO<4Ri;VOw=wgKKw(e?+Yd{N)n9mkcHxWg$_ry<#Xz#0y zhtAcrd|bMhdpSal z;l7oWH)l&Ukw`VQff_Y_96f8>ps+qwWh+u8aLEoJC9BV`I=1CYfpL_pVQKnFXTh5o zp*yRTnd~0=ylAHrObVQ#V@KV(gNGkYCr_MiS!^NLuShGxaAj&=CK$n<9!a|u>xol~ zoMopzE?t?)wPafQ6I1o^`zYU(0BNRSlj#z2rh<7dhhdD?8W!O}$in6QkWt=E+2 zzX9E$?!wOBc5q!>OdHu^FvQPoQ}nrY)T60ryLOKI#83>z-C0zR{ukwa{Xix?D`4 zvwnQ~{W^54x)Wo^XpGDI^E0U6n|9-M3eRrE=hI*KVG`PIr>)HCc6W2Axjb``idbn~ z>yY7gIsy2CD)-RqlL%_gOj?_R!zg>9-ipS<6`3Cb$Gka?olS5XX{da0MB%7>@D^n> zD>IW$N37*#ueb28% zL~>|$n7ft8DUHIXDol*Ul6Z|cNc%ykF9I&Ng;&h_VP1xU#8_mo^C4oL-X1b zb5p`0Zl@TJr-Fs(UQ1p}*%jk?az74#P+8wMNi&v34yCBe2Cuznr#4jylOe!#pA!C* zWbo1@nLk2btfl;OKt&xmDs;w0_X9^|;o~++3?Bd^o7$p}2PPYp4P>Y7a%9Ul#>HT-lfkuA5 z<8gejNlcKqX6WQlwazr^$8m=Gi}lmm-iwyp`-go3`4%4lSJU@^L+dI+f59&=X4S{l zF)RWN#x-1)rLuXM{LNhh(Pl|ej{|cprrJpptLP#0FrB zPNdZ%lGdjelJv~X?@yzHXHOOBQ%)!kW9b&1-^UNK`nywzKzh=2uXk1iNerg zvyD9VJjEd9G@pw1oq6)_?>uJRNZIar>HK{!t1@l%JJ|o>@(gbb^YX?fud*UTkkVX< zye-COnSdjQe!QX-c-Z&{Q{So9DD@Se@g0y&0vRpDCNKDOnw#vY8^4_B^624R`@iwO zZ-4IZ((^pl_tJAWcq+1bB;cX;>L}+h-`Q10(C&OZ60aoNvFqA45%h9iFFLw^>UlX% z%0OtZcf1}<&`t#YI0AUa_(w5%Pn#m%u=pEdl72dV1uVKZx5z+;p+}VBrpq$vhu{4h z1vU4%_VxuUv!2Ft3UF4}kL@f>=We!>c4bJ2xJ&d`YQcz!T^-&aCnv-w&$QL-J|q3R znN{2SeB_?;{#Kqy-OTzPyN7uh|JO5wXt=)v%Vr8(k+XE&-#dBf9bT*?I~c8z&H3D> zP3U(5$8#r?X}kf2ljen8S6SU5FK*{)4sLRcy5}udM=VKIH!0uQH{6Nq48n@Ey;&ja zu+G24NK zsWZ)c zhbiv}H7-!cSnfm{I8unlTQgG1y=d9ku~_tskwq=06-C~@?r<5HbuNjXx`ooWi`YdQ zn$>lZmm@b$xzVL_yJH>vvH%PxpVsfZ=RbRN%S;vMkvzNTPEj6u1+QQr+%0xsuP;V> zA-(W(YgJm-T05p!Q|gAENs_Q4n|kZVTyh{5gtLIYNa7l!1{^^Nrk=TgbW);cj zlPfoCWS<9ZR=QnIUyNF2t*RokTHC9)%1GB^UZE|RzYgagCLlsPXe(r_9(UrxJ^cC^ z`-1N7-q(Lh;@AZ;t0#}}s(!PKlU`c))c_7GM}~%nqjXH=Z>Oj7jN;~K= zqh#*{?zZLoyEyIZuZquhh&TZ~uLLsTFh91F=L&r+>PXz=YvtFzVHekuCGq_J zyBn>@d%uiSP4?u_=sAA1__?^SV$3*fk$tpK^M~n`;nAqiFiDocMT3cSXA`5dZT!U1 z^Z>fpH}d5FgFHb_+4NuO$RtC9O6LZ)-~LFLH6DXbd>O$%c($`@B3Zn)1(pUL@lp5+w?B zHxax-USnnZ(euj!3QoD(HJ=*Z*vqF1CVG> zJApgiPSE+xq&&@^>&bJR?F2lTySO+1z`phXJ@8ZOKZL#8 z72`9lEFCn7b5<1_*HY2{XzK|asY!{#dPVlYDSIY^PV*}>{+_EIiT|5 zv6qeo%{z@`1^%DSWY7ObK9!d&(69&xtHI^G*3>bs*4^Ir!c9vu)4yx=AuVrj!F9Ywc<_SZ{T z`oZ%vNLWqyOz>1j#IOhCX||!yT=w#wS)5phB6j_dIrBsNx-TS^ZEn${e7PU#bi{?3 z*%reJGix?l9eT@TqPsdi!y#9_V1JGqdfL_*dGXG?7u=anxwRV5HHA%}1m~b=0R4mG zUirK^^bc$jTNAlf=vT+vJluh7J19VL^G~Sk`5^(p#@AgN{Iqexu||GaWEjUx@yDMh zEDf_JRt>VY*`H%sR3q$M7mtY2P) z#H}=i{)pYV>)317#MKv;GbbI#XHCtHbcM(3W?QjSrmSxwL<9yGuWfEr%LRpT?4tTC zG%AKKfiuF^865uHG!Qvgc2JDm@waPHFB6A&{-QtHZe1;rcBVVIkP}5NF7{fvuU$QVEUijh2qLNgqXd%2)ENr#Mt!s)tP^WjL8tybpIeOfzma z-bu=NSp|O*>~(r>+O8lnE|(w(mW_=q{j{ojr^S_3AJ+V(w0hD;6#+lo7s5N-#M3;v z+v;I$=GUIvDe0ux;*?PltXNeVZ{n#2K32^tN?;hV$81X6vrn+RYYg8|Cg6~Dxzpxf z@k5EBgKwM3Df^Yt1(D8I8VlU0K;`#G0V98ef$0)3oPwB_q#r+8y|vY9krN+K*GVSAj-Yy z0;${%Qo{t`M7DYf!)jwuiy(em-?E-4zKccwQiqXnjM zmvG_k$zzfTXcBp>t)N6Y6$se?)?3@WQrf> zrfDirwpERf6u$zAwwt6SKK?qFOL})s)c74UP<+>huk3x!0pF)T1!a5hJi2jUV6q9$ zlFTw+PCH>)T0PmjDnVX17(PsIY`WO7?CLk-~6V0 z=G@T5mGEBe6%3B}x&70d8ce}KkPzrN)Kn82ZnzE4Cbp;&`(dc~9?yE1VokrZpEGzx)5livACO8myHjLZPi?dv z4(2UK(L>tZQ0fqwY~AQQz)vrS48~XQ4N_E(2GF40Gl8CAa88G+f~F1TI~+8_>mm37 z5P1?XKbbR*R$Y%&MdbWKhO8*&a5I+uBxSQx zl<2KI(R&6vn#gKu#kUg?930esd{i9s{WVHtI!FrRi})sBV6p?ktFqtO@5TZ1eP0e( ziYq^oU4M@+rJ9x!otwWsr$a)JrK&k!u12@{V=TjUKAX+u1i@+mQfn$zF^t;4Niaa5 zURqB&runS4<}hsP;v^edRY0tZ^Y7{om|*0UNf_S(KJaYsqSn1Md>ng4s!~Eg3p@Fj z%Mu*PAx47wJJr!d@Qr{ateLx!S z8U105;KX*QtTPl{Sjo#XI6F0E9h=sbA1;CAK0YyS*_z}dQC!7;VbDXES(G0;bzmQ~ zOCla+mK&9Or2;q~5_=n72HOq$7PPsi2Bc4^DsZV55_f&mjbu_Ec(?O$ja)<}abYww z2LD?beb{B=wTx~4#TP3FeCUGYfOCWC#6a052WN478O3*>)x@;eU^8D+2Oj_!Mqqe? z&2LnpmgVE2II^|QXjK817<;KptK*50#9Jg1}GPd1(rBZ>Rk0mM@e**`o z8ccrg=$&2W>!S#!P=fM8xwID^PRHrX)JhgH)8w!WZHSe+_b}<$1;T8hhx&jRFkrw4 zV8{LvDe&zKrH}-swXnsyDGL3j28%lSG{-7ts!%&kb4fPC*aA$J(_O%85}y><5;03< zHIMFSfkX+=b@A)k4cfacR`7A-Vvy!@z#oR6JZm_!uEU>xa*JCRgN9C>Z@_Q5DMiB& z5#SJD6J6oRau056Yb*@JE3FEtW9qfa5B{T1iFLTy+4`lL=u zqAxv70iC;`DANqkEkXfZbZ9b0adK({VW%mCu=t*>^=$9kC4=1O(sh^JNwBlY+HzoyB|8oZnp$j6@gZ() zu3SL}PTn%g>-NswISsx7-xg0jk!ECE9)jue_5OYeKbk8YewUeYfve&b46$Tel(rFeM+R1n@hk$zmbJIo!s(mi!eFgO}-BB{&b_S zK{eD|-p#*ZMMX1>zr%k6ov}_8t8El@1Ai~r(E~Eea9;$Gz=yIcDTb+F}As-h^{BKo)WOwOu1mO3B zt)0wA{DxX*nJm0FWI7m#^4E?k?Kc6J5zZ>oA|hq)brpZ5{h7*+un>4oS-=aTnN5Uy z82Tjy31#nGW#Py}YKo*f^{wRntxg4|_Cw=z$ZY3)+t8O28LL&>^ZT&y;W(% zB=!+{@PeaXNnXi9Q@;T=eNASo&bcxJJ>5EJF2`v5w{)}V6PO8Afm6Dz;6aXUzE77i zvf6PIyHTNtr{hJL5A`v#eAP!wI4k}zWX$hxx<@N##?IE7s;ye_y9`5hcXnlu>UyE%z3_2mDPL3@B?bHUp&`_VE3o3v=(1yK)o7&q z#7w%}*RHstO+&JEVF~hugcq+Klji(!<+une`URm5Q@*f>_jo9(;WBk@BLF9qqi5Il zUY^e(`Frx_G838vTqfFYdfY~c$g!Ejk%p<`a2C2YInlPlIg_geGzU7A)f>emefBPu zzQ)^iF>8y&a543j7}g=>jg}?6qs_Cvw8JW79JT*x2Um50@ILWkjl|fbXY`!w=cEn1 zsMEfyv)SWp0SME1fW~cTF76>on|SSW77*%>w^T)}_PHWk1uaOEtHx@UO{rYE_6f*u zVpv7mIwq=QN6?4#oEtC6R1APtz9bk<@3E-ST4FG2IW*&y>=m&V6(Ej-K~5IZDRY;T zsif8&_eiC#b%sgPC{s&A9Vg``%Idsugq)}!cqK?hHyG8Q)qF0oUekP}R6`5IXjX zh0`jj7E+7A1&fG$N;+Gb;hc}B3pFu#)A`$7L}C`Zsd_`q#6w9#!dyco)Ne*ieJjiK z53ve%ywwM>XFlJ_jmh!YdixsMfR{gVEC70zL9P~jK>{>n^hl}U^8K~p4*0xfsizp( z3w$YhAXoMdWf~*eiQ%67d4e+2+4`+@t!k%r4Wl4OGmh>GEOYI61GGqRNz3KugdKg~Jc;I=IW}BR=BKj(G#ctpvp|I7s*2uA7jyj78bHlnT+WNZ| z&-xvW>Z?MbrFRm>I! zcZqg&IxR0GOWp3XTKAPqGX^C6pt9J}2iynWd1_}}kz!Q8>C2}5X=K!DS?CS1q{&Ib zZ;a_HO5w(`=6AR6NCU$ZUd;q*W5m`-VSD6da~bfAe~2L_Q8OV!+w6xO()c($n*4|@ zI@T%L{Oz&{snhs=p>O$Snli2(tZ>~vJ%3~t!~tKL(|@MDq|@s48+43#Ll~Okrn_Z- zf38mQJ(y#k^&Q!LoQc<4c2=tfdu@T!CvxTNhF=HXDE_wDH6J!M6~d-QW2x_Og2X)R)N4}aUPcSCR~qLJ@xNLyc#S{ zhsF$Yk`2UwaG<4ITQI(Zm(tLWlu02Yhl5a|_S;1obb4bkMm`HPWVV){m48Pf_NHb| z8h4|bQ9585i?JBICfq@&G(L#$4wQ9-rQp_z^bp*HO9b2dhdV|D9~ZyU5qLS7`FBpl zK76S3wKce{WYUnO#bJ9#mR6We!hB2HD*X`|IsDuK#FtcOLD8u`Rn$$rXBsLS@&L)} zR9_KhB#}Y91v%wTgS5sf7{ z%OvTOeC1H4hkjyBwQRvBh8fELNfo~%d7^=LgUY4NJ{2zwR8H5Yge63s?@cZ--s=$% zr%od#EwlP4vHm6?gG&6U`!vN;^Hm8$g`dw>nW77@AcZ|HcH={qIu1gBxcT_<(ZNYU z)c%~>xLzeO3Yz148ULA#PQEx0i>err9F<}KpG)Q=h_f=BEuE-GfLnz+?6D3g94SV2d`-$) zUQVFrl-Ttxjb76&;*zNOL0X+K(?Ex}7sK)_w+;eVhK!8K(2G8y`FFzh>$!F_wf?jR zZ8%vgN9{<64L*0v@6eB!H47Ng4+8tJbma@%B4m8~e1AOll=%=nj1w|eC>FqSKecuw zu@Ey%_Zi~Ziuy~4nRIo2*&RA7Nt|YeH=JJXg+=c4ScRi|On;CR#rq{YBt3`f`uX%d z;jM{&F02b)7H*fcwp5CctT!t^Qi#crOWKW{_}dO?XREif8_q`aeah&R zA`{q4^bK|7_$3!gF+QF;;oWl7^A1ezr*T!E0-z;r3SFq9&Pf^4Edz?)@4l4==acr< zlm~hWkEW?7cmJk#Nd^QP*BR=tlh1(-Zr!U2$<4d$?f0-@rWBG8dL(4|>eOh6f){TJ zJ8k}Htkn&5@5UmbaZ-!4(k|53Fhu~w1OTZatvO_);Wus$JEN7F<3wn0`F6e=dv9zYO=>=W{k z(v6MI7h%NK$^W+u-H}gR#GTSv@*$O}^=kV=;mSH|&)6Ez&IY z`&c_5e+2a4zy;8t$pa%=h(#(-HvSgxeHY{!=*B(K%%HccE2?9=gJnY6d{eHD-OJvNYF1*_wA)r zs+c+n5;D?|gEmCTL!Rxw7rgg@8;)k5>C}h%)@&=^$mE#w-+WN;DZncB^Kpco7jN{^ zVg5MrXb#7_o=iyV*sC7O;A#6Vv)=FTefZkD3HuWke?9bkjKPGCLFL5zXx%;DIpx*)-o)5tY8N4p++I@ou@RpDId@k=O0LIA%bwTtHN9a;M{nfo&tS7zLqef`5TM79ZtaL%Iz< z>l2C5I-xUOGv;0^h^-~9(^ruL>IIr*dfN2diRN(qxqF4}3@O?D(_a`X>w5S-w2|>P2s>RU%s6uhgEH-z4OQA0@e9 z5&lCvnV-^U&ZmOUl&$mxLS^y}Xf0V{@3a7m`A#lOmH|mDj?SubP17aqVf`s`uBvOB z<2TjkUY!(OU&NXcXh>*{l8MAlHg894jwl&a!HH44g6}*;eoj|y9}ONCjLz=d%fRQ( zBm74YY|9|eT|Av~)QjV)^l^CN*5_0((wr&Eq^Jg>{O;G<|Bex0{u+F4t@!%&a{OO4 z`5IK02O~~Cz|QpuL(@!uv0%pi_r~5d8vVcY8GL9T=O}~+-VrlCIqC0}PRE7rWr^9! z-EiS(RAE5VJhyl-U>B%t&#g?2u6^(0_8?VRTgo3sV$bUv;HgiD8UJz^c9v^DjxQcLLwYdXp&iZ9H}awnE|xEDvg_* z|KW=;o>F|i#SEM7VzcZ_T+pa}O<7x#Pjh?fv3T7_$)nBfgA)d^z^;s0XbTC92kZv` zY%ybvrZ35ZXu^gC^Eg!@)R#MFb(4MxU<=IsnM;I=Y{-7|OGY#WyJ`ap1DQZofVP1INh-p>aC{W};=o~BM?F2!udK8v)1X50pv{Vy z%dCk|Fx#Y1x-=SqYpV%GUSP6md?B|SQXNcBb&yT2cux}q4UbPw%Id9zQ@ue>XjR7W z#+1O#Wzk^dQXlkpLHZG2owo#82Y)Pm5fctD3~D&9xsvlK?QyTps}V?tA zzc|ajS{GjR9kt6+(cI+o)=pPIgogKobaiFqGB0q5v0lKiqTcoILCJa1@QixIAnCRsUo@(MkDDl>enpmrL zT8eh)MY1NnE)t7f-6jUOYAns*X8jY`jTO9f;k$PFOpR>f?Zx{8gB; zQZZGZTWdo~!Rm};BK`>$itE8AD4jnzP+&b!ZX}Di!~~}k@oU*mQ~p4;EYoaBs+ue^ z`D@jb^FR0Vf7+V!-+|C?S1c^2ZmZgNSc4stS9Q6^TRJy3`I4ApdY~kH^`O8OjCGrZ zvi)ddo2i|2#G9kK?CTluPg`wG%(w&h$%81z?hS2nWTggy^TEe z)q1-4-5;x%C0tR1oMnOr>EamBgaVAm=MR84w84w>rVA?zSkn# zg(EyulWS`ZyBJdxa##Q0cwO*N*y=l2HqDXW+A8=O!4ffHg`TBoMYn3KaoWU;auy6) zZfbbIi6LmiB~zt~2#w^3d|NA~z*Q_Yg*z$*yvb*qVgr3g?pjA0wtx1Mg0dk0_FX1S zIHYoMaqZXga?WgSp#r!fYs96crPsB|c7JM`)=XTdVgyZIT+xz*lkQ_yyrpSc>rJST zmPvY7eypxbqvvOppcMbt#wiat=n;Jc;9fV{@EM-eH{{GECqeqh(w1NmX=xCZb~A-j zp%J;@BJAqgx7Qx3C^RT-4{%O0=Vp*GS1if2=|G07t`>b}?vQG;K3Fkf%J|byVUDd& z@zpF1xYY4~-^i=}R4F;xQ(%Y__Bzh^A)HHjL6l6Sr7HO&yQ-{>o4X}k00mY4+pK9& zn4*D(1EsDsW`WF*stX50Y_EXcGexbX4WcS#ln4`?2yV?tRT*yW?K|h@D(+ALzPDGb zRW*4nGNHp>e)*hK3^tW9(mnnnC(U%!ZELNKnHH=nrNtHFQlaX9x0faxXt5P!^&Q?$ z%Ln0$ItDC=laM4}FQ#et^VFd zKne>?q~tfktHt)z?ic1DUq6Pf^xESlPXc{g>5$?(GA^e``-$V`U3ab#`C{L{!11E+`eBQ>&2_cKoAh+UJ&k5YIth>rv1bcDeQs!jRdhGe z$W9m1oD5QcH$pZ5BMVHhLlUzgmqH-WW_+GtwIi>k{3tXYRY%<4Vq7)y$UOsH`B9rv|iS<5_c*11G;yVc9mIC_I_I z#3Zyhm8$Wrl+0@H5cMoQ=kjNfV_UOp3Th!$?cl~5mPLc9o!IYY|J)F$?*KC@AExM( zu>@*tv_b6>J3Z*Jox6(u+gR18hzdx?cGOTuLdHbx7N|^JEJW$u0SH6L!a2=CNHaQg zxNuor6(vMoePf_ffglqo7*9{UZpEiHiJL?|b0vdpUN@+yV&&MD1|W zl?J{2IE71XfsAEn$1?HmyupJ>N-smSSyL|un%SEqw9wlkq8$l0EINQ{8~`2k#LU%h z_pe;5{xIr^V0h~60f%se=Hu+D)(Mu#xQYvIQ_^|46w-&h1>Y!U{O#weG%!IR%}9Pt z;-mON;LN*biZu^<9$B4lep=60wn|{I^OsWVWhzp;j9G?SmMG_!--IdqO~AnHRro@D zgZ)aYRE!HmK8@gU-txIZS?IujJ*xk^Vf4RRC3)XM;;vh~)7mk#l!QA`uAlU`_{Z%t zt_dWs#`hAM*m?V5Sr$;=8C+8g;d6WsefTHr4H(&1iHow#g?x(@`XHMCDl6aBb_m3z z*ilrzX}MXu$yVyl;nz$5hxutGSQ`!h8>h~adwuG>a+lA6kr3U(n@9gEJk(T3Yo+^P zb|^T%hPLBq==r+VoBspU6t2@kFFv$sQCm*L+h~@wbt!>(W+U%&ar7FF5TOCS5;U2TUHP<27y zdA>=Nq?YILjz6GCKc?v0!AaJgb^-#_`}AE0K#4{}Oikg)%UL?Sik_Cn8l!O#V`deG z&KS%TkS*`z1~Qno>88z>uQY(r%!LP-^B=9B+f5ayN6D+|Af=IAFCPh%dH0zK9Xwt|DSH@8`hryc z@?~g#6$St;+j#o{EIYB#{$j1@IY2riSEfd)NTIC52#>Q1;;I1E}zDbRNrre#^*S~9J#uo+tYn&^0J2(Kk> z(11^6|2cOHpR(eO@jmfCoyfiq76fo0!iUE4o=Tf8b6;Z)S~PUemusTL^IuA@$E z_!S=7qWAg2#2)nRlD3s6wrV?ihgWoR&`i9ymy>F16}Nl@0sn3Up`%sOx;Aj9*Z#c; z+#!vK=KH$bR<@;>8aXos|2K|#N%3IYHmkueUy9l5a(O+)dW4G^KTq?|$Sd;3c}=+_ z53i;ZL35-buR1@i-Y?xPUGC^T2bZ0-a^b`v(vHdi98WnrRZm_W*Er&W(wE2+y-Mry z39HW}kL@hr&qAQ=Vf^W+*#;K>QsyC$yXl2NCKu>H}%hw+!^v$*x%zdswy*p&NVrAwhlN5@p&c zt-$1f$~-q&@T>p8LpQjQae+tZcesPlg6*C^A$uMIV*6Y z7e^*4)#VkD(oSxntAD(lfa)w+>oGW2o%&DXoYC$lgBe()*jge%blPmNlKy_6!|NHS z&X9jq;!#IMeQ1atN9-L+2VC!9EUC#!MZu#{#8bw9rULoJvpdzYtX+b#LTmPTuf||< zgj=)!6~WKYtIW~Z;H`37(eP)JH}7j>8!%(cnv7KJpE6D|vUhrRBC3peUC6Id8i4J8 zm^^=T!og|rsIKRfEX*@{!c4NOqkndmA)WM}LV`wU1i)>&tXtPaz5H06=3*21#F-|9(wGf-Z3b*+%D zoN^zQ)@`})KV=N}=U%ey%x9hAJWYr})!Jhr)aG;}ou+0SM>KbkYB5=&;H}-a=p>~{ zTe}@5GV@0`y7taOjRhNsF+U)826wjj7q;wbH)F#zJhYHLakM*ol&n=kyM^yVq-Oe_ zfKM?FwCte!6b`yU5g_f6&9ZCniiMSM#b*|9L>gBEB8Gw-M!wt)Xqp9c=7Y=4>9{;} z8N}D}2VaY~Ae^r(0Ngs$mFOd}Sfiz*=IKgl={{wS78HIwJ)_hpJ>Xbq{J*Kh62Alz zeV~esY4gIYE=i*5G9BF(TE5z+IME#ycRDF4-3&bN=?Oz5{kK4whum&l*Kck8D_c@C z@$@WOM>hL}?@1+34dEtcbv~hEV#5cBeGA54qFyO>%11n`{NTH3Vb%z__wOkNXrc{3KL{C1!g zVFh8CDnXzI0zp|y;7!29EaQy}CX-SQO6$%&?ms>$CiIHigYtGfS4Me2{7svgyQ_?X zIeP8I;-Ivxor4oolamI$EI|sz5cU@1&{&*2dyuji@PJw!-;`WkwviAn(CmM8a^>Mr zu5BEVB^_E&(=np4QCvGf4qA7fcKuTC`ck-D-x#dOqxt>2 zuBYM|KnZt-X(guKofj7$!|^7tfZRow!yKk0Ks>xHo3M(87x9*D zvvId@BC(u`LpVs-agZ||X>xl%Tc7(Y_JLRX^1v&(9YAo``7EHhuEbEY$Hg(B@sxu@ z$gNLlMy&w91SA>xxxXu+W`uN0f?}PnHMKf0J7s_F$3XxDU?H8;+Gk(?8nA2ZZG$!+ z)K)Il;>t3=JJ$a8toF(kL=T$6-9^fmnF4tb(@TSrci#MM=h()7oX@18*0SQ^K~($vZpf7bAmnrCIs*Kx6$msVF}=ie>V8!KjYZ% z-A(^JGbLZQd4xH4>>_M}s<=f;8mWc9q%j%#a>($gWAJcbYp@2&_ymX;iLm9fbSaDqE9^-i*8*VjHJg_`~SW!J0#b=m{*6;OK1Tkrj40)NaxE zodjTFV*&nL5j3}Y&Xn-AVXKhHn2886?+`VX2J0af)jKr|#?%lTNg+yAcu? zI4nq$^8I}E-q1e5OFaoz6?*phbPsTDSO)%a8N{qBONvsjBqI-R=<{Ky&QfF+qwWQ^ zuIp)9u7Qgax}axvgEo*T+~JIErfF>2Xo;{nz=Wx0Dyd#jMpfZ~)D zdcO7xliRt7bkt?Ij2*X5^8Pu;*&*0b7xkBTm@T7kcMvsMv{}|oL+#GAI)AmA?Tl|{ zJ1eE9WbmvJ^kTZl^H!Pgr~}pe<`K2f4(wO4Bdte47FCf;^QO&6?^Ii@={iwnDd5X) zN{Er}m-*pBg63>?&Y)A<%4!ywzlSCUkK;b~mS}UH@!K3DYDX>w1>BbkFb4M_&VZ93 zW)p2t7RKo4sJlg`pDoub?$#Nk)1b?&R*x{-rnx-c3({+wt>S{}W&cDd-q@=?(`hB5 zjD7(rA3Wjm!oGG~+?_d1E@vC3rO#4S$HeLq0vCIIFm}DFmM`jHSoVNNh zsw4Pq;K>F1CbLA3@cBn~9s-KM^DpBO*Kj~axN-{E?<)X$+aDzg9)&_>O|ORH(R(CZ zuWDJ5u&6$iLf>g6a`ILa-m><_Hf`_rgR=f)8m+7u)}gZ{pz9=Oo?`%7>(Ycq64F-t ztwksfD#RoXI5#;b1({}(S@610p<_=~L2qi1FmE41+VkF!{v7KB&|GpPdA+9Fwog5! zbl?3iUcWd2;gJK< z5(}mj!(I2s(v;G14`|R_Rl|^!uu7B+(a}dz_US#-DRu4Bmy`)N{TF({R>`$T>V)&U zRH_=x#SG%Oe}taIY^5aFWM7X2ZVE#{%+AgUEo2}N7yJh4d#q#(dO2Jk$?)7uB1<%v zSAzHt`9f{X*Ut*chxBq-WhP{OwDvq5Y<*H1++ZydB*%fns4oDx98xiNV3*yEcE|~p z$9eLp`@{JoZi-208Md2}a=^99_z7lj^N@3jp^Dlt^YA{jSTi%G;VB@`q&R7Fd^rTK zW&9qP^|Qhj;PM90x)z^^qb;J_2KnIE`qE&qqD>MJ8E0n`*GZI*++?)8*>NuZJ%l)r zhcMUH*%3oo4C^PaZz7b5qYMeBBt014RMP=ea2-~F{E&m7>Q*~u^LpmmCdH-t?kA?d zEMF*%I^IQ^$U3IejhG&&FLk+Yw{(1XRJREwzSan`S4TWclpl9)^2K4b8yKt}Vs%Xjzp|BbNXe|;RebrSAiAr-?1N6Y0_2>n>%jm{PDHb)yOZ|AhjT8Fo{r?z}^|!u4aQWT# Yw~jPH8k53s1B4c^g4aFU=2FgSxxkN`o01b26LnnH2t5?mb>RnZ5?|r(1Kg&yEV~}B>prBxXk`h-!L3yHqf`a<(#goS? zbJU!}j|WsIB}q|~VmQUl?f}+Vo^{+JU@wxsJQDMq`uT9kbJ0p zXgT*;uCAWCZRR5sKjfs4kjGQ`z$g9zQz=}XW-f@V?1?HnQMP}kIy3~GQS@UQV~`_b z5ckJ$byT9`7n9Fz2qv7hK?4UW_t3s_X z10CNmj?Y{1U&ZSM{T)`6W8YDB0MGFeA%%d{gJDEfRTZU8RA|MSxK%-L+mGw;vS`YK zKGA>f)Ez|ygrxsUI6h%6>Enr`e+RZbJHut;o86yXqcD_)NIKMv8gHmOL zGZ>!e$;k~34Y^Oc0s5$2;(-hJkzS8YeQ*=k0ldYdnmOqJ6`If^<}56{?w&lskM>RA z@Q@ze8JF@fo_0jJ*idnLlsVC93o+8XC)ivk&Y+dL%wSpcc5FTRP}@qnEA8}U#wYc5c1T}b{~w9@;KERik5y$e5Vpmc_P6@- z$7z&cxoWrb3+J*FHd!^dZ1_T=u!-4FmXU;joa5|6mdt5AWWrxcvrO%tmXoc%)jwG6 zBlJhO%a@N143EJIvM$%ZpR+yKfHOBoE%7IfMJ&?H|9uMeF>1AJ%)rqN_2z>(>fGBQ z-GS?|!OrVH!-3ldn8jbduoI7pHJQzsho$}k$&X7p9TNWD&QV6135*ufCy-lgd-9EL zq6>(O4{q{NWeJ5|%lKjHns@$j`E$sz07; zzaGkXAX9qK*v%cp!sxk-}?9wZu=S=j@kd)J%gPi9t<>+zI9GqP(5}0S$v!#&NigD^Z(v?OL}_b|&95v-{Vq4H!Ug=cAhF7UQ1vTp)ZI zpyjK}|L4=pp6lYqTG_MwP)w)LLn%p7BAwnX7{V!HK>KBt1OMjpu=# zvr%UyZ=J68vmxtl;+e^t_rXO@!PcSTjwOh>$${zOG}fDUSAx|JH=2vc-1WBVTGjf8 z6lo2&9}Pr3wbJ2IyCDkHy=!rL%}PJLPLEAakM2oQfmecY9(U(uRv~_aS0z+%7Jk>m z-7Y7(Td2KV7I-denUy&0{@vNksFg7u3d&bV0FRFQA6~O<=XoL~p;?DBRc5E98ryCq zTp{J?lVt|_gzc~t-sXpuA}cH|VI80K&z~1|ONg~}`>+<^jwvk+CXS++LROOn(o1Xv zFCL=0e~O1;%kqB29tz3Wc@No>l26I{E^54UiTqEhRp z&wBq&r6_Hqk7SU8&VoOC%bjcf?pWU`7C!^`;d_r8h%-$*e73p#?k?wjrZnQyeo!MO zH2yu*EJ~Qk{qE_!*NMuVTUW5A;F;}7QLaLIQ)?908Gvq-3nO@Uy%_(B=@P)`A7(R7 z2UHpDb&C94P`?F=pvr%xPkzA=VSh6w5FJHBC&*qFewO!%Hk{}vRUr80DDqh0#0j+w z`*F#^&7AYkgwdA-u{~^z`yxhb^4!}FtS}4C?r>q^^Es$6&yZ+zN6P`7y8R`@8SkQ-X>AM6p3aK&4n%e{438Hk7I_9b+D zl56tkA<||XrUqQyWF6zpclHl5HJ>v$eDPhfr-j^7WMUk)^D%s;NDLVSZHasCVy`+F zpfNyh8y)k#kKd2!yzN)XClfVI!0dwJR#>e9?~#Nc?YSW{1@ezx)rOL5A!&jh^zXWB z6#2dPZlbjyuja+y&MS$|`VW7cTo_!4zIBorFgf!fvm@M0z27F7Co5gu%-LLuOL zn%lv^8EU6LZhoXEUret zPb2hrm`}~zlx51vkCp1R+o$tT!CIQ1fSw*aiywYOp3fga z*Yn}C5LFt>D%Ta7b@#QNQ6F1rJ~)Uh8%mc1FT(2tnPjHr;-;K@S>CPY59>Yd}n(}Olx9JS`Wm0Xhh zcPH!4)+L_+tlL2s*h4JhWRGW&sL}=1M`v9p$XdN=t<%+WCy^@=GkqDXQM;HP-F&fX z=MtXDUo5Jf7l4U3^R6_i|Aw(&_G*~59bI+O@e$0|LEC2tOt<(l|0~^lWN6C4&j;Oc z|JuIZgZOFbOB$9h@%WDIPR3+jh-+J9m(v!Mich|FYxwMA?GyHQQ!8|uufUBuJ~^eO z=D2vrRR^N`s0MA=Z^`J;L`dITt+WGgnsP|~MGz5C_T5dF+v4Wv6jc`eXCY>mp~6CI z3L8OdXYcOIf(>sSC&P^;yG_|Caxu>F3oS}~Zm;zmk^z_IHTndtaM0v58;8?;!g_LI zmRJQRCws6a-U>CF*|qU-->k>c&tMgn^s%h%^T-N1sAO%-V$)H zp_0cYF*gQVmwxNGVnRr?_&P`GZCm?D``PvHaLXQl?g5S@D#2*NiicXDuj@h3plcOT z2_@ikI1Sh-5#;a!sm)~Lfd7v64eQZ)S*UkgRxZoo&Dj|01YE~{O{?(ZqFet$G&otH!6gNjvUe_ zJCWQQGv1^)df&rSb%$V1t_QUuje5{3p4@AE%LBWOo0RO~M+8d#kaAZb_n$w`+8Y_2 z_SI!-~DNXCGb33Gkm37OYpBAs!ap;?5A4VRZN@Ik`NC)Lh zDis6Lm>WawkUj_Y^FVLcn$-`7IbNsb9h>l!-!zVCXPD3!)!@;9^|DtFHbQpwn)XT_ zj_>}$tL9rdCLMmCl%xzLz{m=nAn@JzEf$6ME&QPO%gPi1!j#@qjRH$b#{x9nxW(Mj zc4*Wy1D#GaGO|9@H)u4*o3w(J3kDEEOqey2cv6E31{POY1P!tJFioUZ1?=uy7fZ)TqS_ z_J+WVW4m&J+|qh}UiVx6V?HxtlFT{XI<~LR6gxe*t=Q9AzU*}5k!Q}OHzlh_ftm%#7iLjdCI~u0iBnej ztWN8Md%9h%eHZ+TB5K#kDv5u>#RkQmxSO&+=tWBac;u$nG=kRSV!3e#?VIKk0f z@$CU@blfz86O_Z69c|O(P0D5mv{}$c$TFm_cVdHku+oM23fJ#?^$|-=0)6-ojL?rz z6<@ICmOJ7)a-aK&N1BYv#sHJt$2m_kHHjui)8FFNIXDoR<9v3<%6<=e+l%>HRnP0p z0=-eE;Ow)b7O>BqS@dZjDhi5w{AbsdMwh(~96GO3n8GnAHK9qe=UV}%zPplkWAI@D z!*?szV!d93>u5E%6aDV_Nv1`jMK}e!d+YU4^JEi5jdjnAa+3_(i(IZq(O8Q&APH%W zg&>n-e^}63e!yhX{F>!|@x%X;Y<v?ZYIWL%y4`@IaA$wS}CY%gncd62mZ6UWZ%BV!XI zWjUAWLIc+%lC0@QryWo+1`VyGuun*X@gXvA>(dRHRNr*@#1W8r$RRyAI&}U{zN^7b zOT59e_$%+6kNZ^%^n8{kXz7u9!159VIJ=k4CUq}s1m03^qO+R~Y4gs`sVZ~?n*uGE zy++l~|E#Ja&e_NFX>7utV^2sDM|NXTl2m|nz*oXZaWX$fH4n&Kb1`s_;k9eNNOwYN^ zS(5gmM2LNBGqTIdCwyTj5Xw1PwfKtjE76K?v-OOcjBv&*c``0`LCPFRi- zB_;iC!YtCY$u%MWgtr1Uc-j5F)Ej}d773%d-!itDf|}Y@c^akv%BjGR343DtLzc27 zW{z{Jqt#t1{UfHBqB1mDoU_{sK1yENc|9&y!OX*wSY;5&{qSpxdHBSQ{P0yS+P@vP z1ia_9uZRwlk6qTi3>G-pVN8NfO!&d@ZhI69u^>giR(4!cvLzqxNOfSbU<8Rl zPWpUP8{qK)Ty;t5DP%rsIFIu@v~oxEyB#jYNQ%tE`$I3shmCOqD{cuX##H4)93oQtf2g1Il%x_cl9afn%45cPhG|4nVK=C4{p=9 zNnt(B&RFFW*&6C$q&(KJwI5Pu;Xa|*xGba4YHD7YnrXMRdd#AL5}Z?Kw`!1mDaLu3 zW>guC_W-WA-`}nrS8s=5>MZDhDR~ShT1ROlak*+Zr$HlJ6I9L*hb;4>KEQj&;{5zR zAMGQl9w=nq@@jQ8K6FZ8Xr_^te{gBY{AaR%o2c6yZK^ImY|fLX8_k9ROt`x2`pAtQ zXCM@(;dm>(THHU0<4N~mrpC-j_f1#v7!OI4dnSC5{EKmLJSzkNqRtzKm0#EG~+}MPIu1_zog&$+jOf|W(QDPP~&P<0mnoG zhqIAcm9uG@Ldr3T_x=}jHdQaQ51&^Dc>U_ zsQ<6+!86fI*;E!hQ<+T#sTWl2;XCEo*XvK!;t6C9rZuo|Nx{Z(hhLetLVn9loAp2>?H6d%4YKf|M7V9PvARYm{9tPXjquw z5F4|RR@wq!op)%aZFF~Ji=V-p+fmJv)s2)u;$Y1pGg#WQwr&QVV9DO)8s70Ub==d@ z8?ki#ZAzV@KR7v-f@cl!%->*A{!IMzEpkR^%57#^_u;Q9>1_1K6vu-7srlDLJEfbx zHQb%^C>+*}n{8i-m>Nv1qfF#fZyM8jHAc3G~iUY8&v)_pf;M0knQ|_L%z%dETi{^6wpteF6yM z2NQenb$iIo@OxPg#>0@<~Zqkix;h?NC^ll-kKsC^XiyO#+w(^+(B#Bq1 zxAND0dRLNhZr-lM4tX*^gJ1m>Cr5@nF832l*X;IO#{7e1;(>f~9KwXohkGA3pT8`n`xva6l0vmEih7V)=%8%)~D_}#o==^x2ckv?I0pTsG4@&8de zzhcooQNpzx_W-;VY;R_}GpwG+icS~pNt<0KF6HPnTT%hz3M7?fKxx96e^l7^6uR&kn02glk7Iu{W{o^pmoPW>}j+y*m??mve@2}{^!0KxLz_CDjJ1QO*43BGS)PE$| zUUy8OxV2E!p3~)N?MP1X88OKZ;0r!w!5NFU9{c}ylK@bh(2*c;R=s{xaR6@8q(9#zb!pIWy8-7&l#r` zaNIgT1wMVu1W_9C=#zGq>bn<{O55Pw_y*?I{1VDu`!i*6o13ub&yikNj@9Yz=@2G^ zaS1IZr*gQ~6YUp&19PWQ52N;2-SYY5uqPXKk*~5_Fc}#Ej}{XXThz&QmrLH=-3|&1 z8dI6>3-Da)!IpzN=oJ&L#pYp=upEH_hEiu!^)Ij8p0Bx>-6CWwVY_>~ zVIyO2-}|z4-H%&D2P)8SCHbyf-zNHb;EscYr=M%*6=QC}wbsjaYh?vf-7-|{qE2a{ zd|t&X^971K?SXkFQ;`t$uZ~|IAMa}tBV+dVUdLKemmhURiT*|fAjHSt({+Qc;{e zQBr@)$??(1Rb#VqR}I-uhmxD?vJ)Wm+Rr)w+x%LuAUu*wPCnrKt@gl1f}P}M99$ie zW@8=xt2b$Z4W+f!*8(vQL;_Xcd%xJP_>C{Svqda-*OVCyH|^7@V|rm&o+u6rm}pJ0 zTqRUoHrNP+9{u_Ff)>EzfzCRtNR`&)W4aZ|v~HhoeMf9VQFnTLnMfFSXV!rUnC3b0 z?S}VCzi)m=ZfaA&B!zg6d|fysnd5&%Y{%5Fc=yVuF^|Aav5QgS*F!-Ji#k0D%3@O6 zrEapkL#k?FnF;fOjMH1E)05)FM4d@-@Iu+Y6Pv0Y&y#jAEE+;mXqOI$xy`j zfyQyr)7sBA09}#y0?i9vqj605F?3t)Wb#TP4wH*8FPCS@TV91s9fRNN*hX%Dr#}1Q z0#aZYy3nm`!e9!SY`?f%tlwBV;{imkQ1a~axjCH-d0myYKQf07{=?zrS7-%|UNfX7 z3j!4$z$teO3pt7#d%4TmQtXF6Y?ltYHaaIfaaWD0f{k=4GxL!9C>;}!RSA^=>nZ&? zb6vfO?(u1pnjL6y<3J~rs5_~pzDvOSrr>zQW#?HHF7<>i|GA64xe|NLYQbvY6O>=% z?41+4Vor0A)H|%W;;RP(fd8gcM%|rRCXGjJ-E3c{@k%2fJ&d3|K0qh`ZqhbtsFYo7 z3;1Hiu`UTsvNmZ}!y>{ZBgw-<07`O`*tgwWJhgTJQ=Z4K>BhYC6$?{b`S=eN}_`<|Q8>N-F7adGO{@2V(=+0_nfd@zwB+P(!$aCf}w5 zVB#!TICAyJC>yE=Cs*?P3|}zqv`Rsv<-1${yjR6kvyBV$0=zLg5`^aC{?-daB;4~` zKf%x5Q`Fbs42zGkn0gpp+VFQ=s=KQY=VnrS+`J?te+zgHUv&LqszXBa4CN0^!D9dT zMyR~@wY}#yBp)us;Kj!DK0Z!&+dzigME^Sm?>0#iDM^9`86ce{XpzL`M~f}ka{X)g z-jq12j&V+oK|zsG_HO|Rug=;s8v5NZ9jZTNe26nrs(NK1YwOf}g2A3}o@pw5yI&hy zr-|XHzHks`ZK)BE){yrhTLz@19fZvjaDzUK3<=K^;saC*vNonxG=7xlXyT47x=Mp~ zg@(Yb-i=3KzC5JRJEU&Rr>HPb2GW9QzmJfsN{pH%9E>RBs*^DJ9c5-=Lm*>o6JXAcge!d<5=`z zWbE1K1?Jg&t$pfRkahh}@jGUcrWM8V6N`do=cn369>@Pr&qd&U9q)<-4#+Ka$|fa)xA?dMe}h&9T|03R@5 z9j8BXU9!D%MYRTHrd&m_N3 zz<0l2M^v)NSTk4_$s^yaSg%NS!zz!1zH|Yao6hH76H>MKX{KLFc zz`bvY%;Ip81+z>d^MxG#0@HkF!jW7xMe{uuUxtdKi9#MYZU;F@b+{8l3^24=y}<-I zW#@Kuz8jF-$T^caN}UZO6A3Jh>l-fIZ-+4noDh`dD2Sn=Y`dOUao-@eY$DN(WNg#( zniEeX#RBqnf~1^}yC*MN2#f8V_D_cDhgqz#hpbtQGS%eDCq~H;%ZXDoa){5X9p?7| zF%#h;(F=pFoBIh=zr2oH+vgX42%X&X#G#q_Kcgi|xwll9%2kyg3|or?6c=sX^9;6| zwtf!6T*jhwUSW#lTKP6-m03>|ycjTi*EJeAF=u72E#$$$eb(R== zBN;*LcgGO^qLhr!u9fdU$VSV@NnPn&nUno-O*%7BV!}CI6A(_polF| zh?}nC%V>L~2~IB~BtC!yPVxGC?!k{Iux>?fBlRD$<}H?O5=?kLjk9zPLvD7z5yOor z8UeHi*mJS$c0J9C-NYS%;S{{?t&bVO{d^1H=1o1By+M5KF=dX^-xSY0P+^h2q)8XA zeJI`2S)pV&7uSu00zB#5afAHU5SIw2aNYEd%4vI`#eJWm6Yh*1C^tfCTmnQRf^xaTtTqsUB_*u7fzR|$~C3B15&J3&G=9};?#Tn91sP5+V;lviZJ&A5K zZ__hZNXpL1l1y_OF?*n7*Z+ZbV5CLXH5k{s|Gw4#!4$xw^X`wzC-e^+wLY8MeC|Sz zI-ILcsH;EL9?VH5j3>SB*&+KU+Q+2T zndpJt{s!!W-v1-mJb86h*tG3!%QZ_@3JixT7wiB7blJ;xW>B}c+_aDV0xS+m9T`a< z)J-tdKwIw4WaavxQ)Gm{R|tn56q{2Fb*qht+y+wgGQ+50a%)bTavT}5THDPH`?R+` z)DnBxQ?4|ogLKhGika{QaV!Z2ndxqcb)y{ImR?y1Bc;bh$a=kBz`LR>7z|+JETUu_fa~VlsTmK0-cIz5dstu50uy`P?J--QG#6Xx5z`&@T+|J8{p-z(g{5 zl42#yq&sp##;IFqrwF~B4qsVi68poDiK@^CU!wJ`BLmytVtiHb`>Uq=#>IPZaq{7| zff>KYsXLmHj*ys*jMd(+8*+^Is?_gvu8|Q2Fyf9%$%-lFlEUUWtBHiP=W*#XaxM|e zb>|C{AGhFMHp_ftOX1hlkbjs@Qld&gy(F~E2{2ZHGt5_Nby`2^Cp#Dfy-NCu+4i?yrzC)*Z&ZMfk8AIP-VrG^ zNkUj<=B#X@fxiClSxCx~J!R3T81{uE^zb3_h?Zs#HD(#-qu!Fh)oxqsayc=x>khXG zbJwt_@kE_Y>X$x~Sy5ixqmZf@LyN+t#-57!Qg*tNo)B%OprwV5xb#prRW z66MJ9r3&sGKks>chP$bWm14p*<|}nXN2NT}6J7!0Of-e9ZSd-LR9^AUnJW*xoFayG z)7P(^b63haqmz>E$|iGVSU@_u@4L;yoL@;KJ@X z2j4oipV(m5T8M194ldlp5-nCAX;+9i4M}+I;5bms+Vx*MM65YK(a}NR9_=N!CN)~b z4t@W8b#vxoL5$XoO=-er8&nMcAcUDILW&lMyN%ol+%%GepE%E4uGKGk8I|LHr0mlk zHh!j6Hyd7BMrL6pNqj)~zmc5LjNov3>OC8?h3S?Sr=amhW`yE#o|!omYg;K46-qsQK`YBUz$LzCP0LfWV0KW zx-Kg?Bd$2o*?iCc=t2+ULJFYGjbBbrcTXr+mPAa&=)><`x5_)gHjYWB$Xx6F67=BX zJZ0MgF0iYd0b9@>7A~cmtbk?h_sNba+tQrWfU*sxRm^hOF70dfqoH}roH*z^#pBnrhfU7+oi|^w zzQc3e`F1v9Z)3yQ)Qsc1>E-A&69sZ>*JFsIl8e$Jhm)4ZH8E6`A)WI3e=~pcA%y{P zaQ)4NM~`O4OwB|7y7+>r9}L0jo{M3DrA{uYU8#-0;s%6DCLWlf-6;+bQ@w@umpz z0q_5S>doOnZ&H31^y7H*+(>a69G`qFWO+I)91={WTzhGH0{}qIa^U#o9Hk zci&}I@4enW{IY^R{IryG)0R^Z%#>!y^}%lNb?Qq*T*ao6b&A&@)>P0O@7`Dm0{xPo znwQG~8Q`lbVd@$XL97`~k2Sh2^F|@eh$p*55z=psE=Ld!-9Pu_v-q5#EP*U%s%7pv z+FY5m^0{an5&&lq{;NHG2848ESLM_xYlx8J*m9%`b7~wHiOJV zau>-|cbDZm)A0~uZXLwVJ*z;R`yN67D_w#z8yvJIx_$NNT`;9HeV&!+Hu7tN1q1Cy&WR|0gLs?B>q{L#4m0q(rM?$bIhE5M5 zS(nYZ9SFM_BIO9=t?b!v)rB8er(3V9y1x)p#BjAcZ$-3equKpyXU6K|q5QT9S43V8 z{U1>m?U@7ud82in)%n;H6-|5Em3DE}yyRyrw9R3>kLm95ky#$k#(Ei@%ht3lVr)$J z3@p#{Tnzo9tY)l=Z6ZbA*udBlWIfW_;RVtgTy{sUhiJmv1DW;~nqtrbGXwQ%W zy3dZ~D13cm&|}jyBo7}G&ASG6YK45hhRf-Wa}Az!jwck2W^$lPuJRfXCuowo*g{aL z$29@groPBNtKq~8&+t@efUTFPEMtq1IN0t5qD4g?p zcW*XXBn3M10#$pX+~13q32k^6FaMP?fdjc`OxbH z*V7*g*yxI3lAld^6{{FzuV*Hb(%Le3;hC~&1cQ7n%q`Yq?H_4+uy4lZ-Q%cCV#j~l zZOzO;so5LVprgp%E&r#7S)6UEhZqkR}Ut?C{8_LYld+ zT3^W+m47GuirLWW-}saP)0JuPa)2a-`j80EMyRG34^*u#Kw6_A^-uqL#PYzv~WpR5i1T zdd%y#Z+%xRHf*Dv`D>uqf~?*|l+KSl2<2GwoCKk9UnV>BCjFB7Lj=T^XB(3B+&Ayy zUDq~eNZ=HDpB}HoGj%A{4@%0hXpUw(+~ka=cKUELuRY38M{E9?)oH)<^nq(n$-zc{ zD*lN#F^qcAv4?AZ2NJ^>;h5Q^-ORZPsRyRphQbd0;*p z-Q4l0&1bdddpN23Cfs8oJgpWOAJw4NGT8q!I{lMHpjF@a87>1T7QqkXaksJ{AtPi) za08FskOH+uG10nVg<(PJfVsa5hKYIdWn#+$-x~}0UHEWl1n}iKHCZ%%v$IaGC}iAk zzL|OuX4Q?%Xm}!dNcHl(Nwf{arp`E-EhYe4Bw=MWD6g3c>>Kk&=V+Dfoh#(`5X2WH zc?J;F+4|cY16=PPeTFbaiflM=2=iALP@#q#B(3?x`%kD-WWt(mrjLP8$-?p}rT;wa` z7!E7%xh+I8J#LUARe9?tx0K=~itgP}{^zQj6Qnp1a0ys989{Hr-1Gm8Z@)ngXP>FA{dJAs_f z^Q(m7(Noptsam|GOPCDw;C5F(BsulIniY1KYUXMiF4l(ccRnlDoP4Sn(*dE;xn*&k zbmo|&*q;o#NFCBgS0Y1t4zu)Ehp)U=uv37Dl|qVv%XoW zg;h2eNS2-oN|$Ortlsn8WbWT;C+`>Iix!>8ty%M&yc6{9&aeWHkm60F#MS|Cpp1G@N~q(y3F zDv`Z6{6Z}p6lD|Uo!)2PB09jY_duZJq@>}S-2s0*%q##LF=x|W#%kkMe<|gd9V5Q0 zx5$@u_|yr8?4iQ(`%b|i(tJ?_a|Gp|EkgfRA9V0b>>w643rT%oXE%I{w-Z?8?F=3IhLP5)! z*Z#YNv181NIEzu1g2DmL@bdOalO!u#DO#@?H)V@g5a@f+NQ8yPFwd?E>n@HqW5Ilo z)%sl%N32ozIDaTz48$xvrQf74LfxBeeV)YE<`yn zbBDZL3PLUGQYwvO(jz;RuS4unp}oVvB7#Vb7Y0Hg5E^$>%op9?_8f{f8r22f6Q51w zJ@+o>1xt2MSD#bWd}oq5rG6=?t`?~*alhMn*B=+{gapHLWiEt`6sA+HMmp*r z9wILs0usVB%D7FHem3oNW;{1yOctdVm!s^QWn(ar%S-LZ-T?0-I4)XWwG4;Hm%_Te zuc&FP`V-2Ghg?wsu7mz}+AwH($(Uv{i8oFM>j+H-{yg#YmiU+-jYivqc=>C7GQxPZ zX+}bxW2HpYa_%&h&EGndHa%Oar~YpnQ#(A0R+q@Yx_&*?ol2J2A>i?v3Z zIJ?c9rwoLZ+j1hqu@(m3&Y$8ad@$w0wA7S#9N?t!iJcCJdH6eMy6n|%P*Rn}{p<|@ zP7(jrIAGZm8kXtvvqFDjAY?m)H~tHkbRN6E!4zk|<+dwph20`|OXY!)u=tHmXtTsx zy!EHjgtaV#A{!IL%jBKAoWQBLJl1_{<|>K>)ij`)s`i5&N>s|Gmsda0l z1Jgc3rMuzicl#OJZh53Y8FrcYCht>F+q+=%CQrT61w`m-IdA#|nYVHbplvYn9R@Ni zqfjs>4{y|Qse0fHKZi;8-CM?BocW)*?|bR-)H)G;NM@3d7R$#%A`tzyxq>U#Nm2n) z^5WZmxG(c9?iHN$`s#$P#l%~q8st4XF)BigRFB|iKYWh0Cqmq$(&#*+tmd}l_KDr! zL{p@qr;a3i3fVbiu@68kuJAFloT>L_b2@Cq;stE$B|nP zNq+X&_gHDDWLfBkohK~O8be9AB@C!P6RW{ zCy|e-`x(CvyIxc2^jbmS5wf-OIp%(3XV0R08uLcLPGF-hIP7i;D6WL7_{N3r$4#Mr z7w&R*2^q@QtAKB7m2uYUMmr|CX6(k0RW)#cfL)rhey(@mvsB|&A z%N_ISecYVd%i7O!0BQF=EW=X8K7T`9jMSjtbIxxt_7@BXG@m0nO~|dI%c5*H)*9>i z14`2dih5f5vlrDWfhT-k8IzDacUps>`WS9FuPO z7+EiUyzT-#Iw?S_5S?8R_#0n@N-M5sw__QrH&2;a%8&zE9V zSHGX5{PD?T4JS*eHCVv9ePiUsg6pKkXC}JT#=TFuY}+sV>%bP=sx3JnJ3rpzao($I z{OrqT*flf18j-I3y_lDqUm|J~S9c`($eZChCsy4|wUpf2ubA^WSwO-w{GGn_51HRF zyJAla#?b9-8eWlOVPP21IZ2;YtkICRi8LGQPpb+CkWJ#vRK}d9tTTG5r&vosYa<9k zHF96GL{U+6F%TIFEquS7R; zX2wJ_D~`7!+Pe#wO~0OlWM6OgOISI~-Tt!%o~+O&iO@P(D8fG|%(Da6`~Y<`b=z26 zpu0osooRqPE@EMvdB*NXov=o|4WqItM+Cq|SkD0e)r6LtU}ETLYSX(2TSBWnT`sc3 zi$^2Uvi4O2MvY)qlvBo!_*Qe$4XYl15`15mXYp#RuViYiB}`>Xr&ODhCUYiC&scQZb)V|o*GwG#16!>Am_EHYKryoD4m_Ukr+wa2r-74Z4z>?~ zT<4UtFEdl`A5Birr7+I{)8`s4D`7r`S<@{vzlo}!YhPOeFhY2qKJ~Apbz?wp`S~>g zAC;=OkMgwA1qbWJ+-Io(4QUP0Md>DXC;78M^ZD=VYEI3c4HKNfW|-5;~@4Wj!H zJ#G|~BWgk@X$C+#UZ;W0vQImts;@_d0mCL_3HQXHMTg|N4x&K*UgPkwPBQO)fi!B$ z$xKQY5Aas}OC9-lnsO~{&4NLQiA(U;c6J&KcDc0jaYjFAZ~A4CI!j% zXEXMNFjVNpFRjbpe>rS=F(I$cLuFg-@Q52se@9#a_ILs(Z*l)fN4x^&=FSggB?Wcm z9It^-ze1i7rqAl#Yql{9i8(lDY|rcSQ7O0Z>JjSbj`hIf7|(`LfOmn~LFn;=*Q%W2 zbNy;r^+ECd$0tUm_)3N}Pkh=nH{r*jJWp@gjiQyKW(L2GbSf>defNev8&U>Vio$7P z>5JjNZbvm<1{aQe@bnB%iBzQcboCM{c5wZ}_NOq^`mE6=riTOVyZZK7X8<}vy#Jk^ zb~G_D2}NR1JOh{^p!Dk}M8hLLLOHhkX1FF%<_z1`g0R8A50=&3gO!nK5+%O6GyaeD z82jz=Z*7Ca3C)@Gf_XwS@??IrK=Z`4Zfr(fJ<6nnOQXk3Ho6$~K6?Cp`54E31-v2p zNO%9}Jo`7v-9G#;yIiFFH=p{|@Lz`a_nQO%l%XCzzD4>^q3rT>1=(2p5w%>3MOe9Gu2n*^b|$s$Jb=M^v}P}o7Pu%zh_UJR<{&3W^kp_ zTw(7}W@fPQ(FIHGP}rJzUhJh%qAU@$`tR0#)=nf`Hzu!qQ&ISRfMxiTKJVv6l|sl< zl#-WVfrR%hLF}S20FkqwdaHUbMNm8VXp18LA8l_P6=n3liw;PSbc~2}4K*}M!_Xlh z49$QvNFy!MLy9zrGy>AyAv%I|N;e2dr<5X`7r(y~f82G~Id`4=A8XdkX20*=`;*V} z?1f#1{}g-=;37Md!S1^fWjzjA|21H<7FzA7A>NFF1X=5z-YC@4V2hFKU^j@bFO&nk zZNff(o!T!ZP~*^Y&dF(b0KPS7OC$(9>1J*%5zA)0s;4^S%=?hJv&PUQsYhdwd3vdg zSABSsbeb$VXHa$?ZtBIR$+BWojfP{Q*W7ms_9J&s$qX=L83)5cI8RQ#YrQi;yEv~G zDb-L+72&p$qWx1*)1NrqiH&)`!<~hd%splOYfU5KYAqyrBEP8e z{v?-63yH2*#>r-`7!L^nU$xwiIl`=cQ)+~Mp}{>B`(eCUSodny-a58)gpMjD*-SID zedvZ5bV#-W7dC;e@D&ec>gkg@V-8^&Khr}zPyuS=7KW35B#MRfNY6(VOsFE@DhT|x z)UkyS%i5O7-uJ=d4o@vjn~hyx6w1sPq;%JxF;~9cLQ|mei18x_dqaOFEK<2Ca1W{M zd<-AddMIetx+B2b9r0~p$bsa9%R55+iUncuDPcD!#D5}9#OmZBmM5hs{w2@*W`RCl zKCw@Q2KlZR{Ae^*HmfHU8SEL_x(9k;!ZFKO z-BuY??q?CPmHKYq=r_BFUY<};u;=jT8_x2Ks4&k*Y`XT(>37|!LDI0cBwnkq4E^92U>x$gF>) zhkdJFq@pR=k~X+_NtC>6PeVXS5FZ=q8znU%W5}+L1jm3soLrLCkaVLox-OM6Y|}Q& z4sJn5Z!utqpT3Mz3B0miD(I2{q9PpCk0W`Q8pnpz7zQ3t4(lKDva=1BH6kYOEVwSz zl?j?j7#PA82fSbPEr$tjqLP@89;}@ha5CdAy7bLLql)~$v-R3Giqk4`A{-ISqiEiQ zh>MfP7v?(O7AWa(8-2aF^PjFaP6CTWd+)BfRP)D!zLcD~Ofkp|6gHV@Cyn}|ZZ0*D zx+yOqOhKiP=QL}Oy5oRxuHO5&G4erT;!z8=?>jvJQh`6^WT&C}SpAy2RlyF&Ev|bc zak#|3hE!yAR!?lz&;U6smp-HRaIpRbIyb*`!O^*lU)#RiFb+%o{KaI$&i4?zFOC`< zRi+HdZ&CeOSp#0R)@eV6Vw$&-R!Di8Nql$#76jSvND0UHTa5 zM&@rkjp^ozvzYFK__6-*6LC%yE)1*Sc}Jw8gnR0TJU+Ir)Sb;0^*Tbpl`2Ti1PkaGia~w3EP`HQwcrBeK^F(9qNNYZ-Unj0c?;bW7LC;r@5~DzLH(Yxrd{Y z@p$uxLd9u~oTDw-Z2DlD#@L^U;?NVWHwFArDj!vg#l%7FKLF>j1qb+pk*bypkI?5E zZd}a^(c|TW7zfi2>!emYF|qEnS$IWuWf|-L#N4lzA)$_b=T$1} zaeyNL_5N2W@juREWf2bbZ-24jWlW=Jyj@&mXuJ*itsi(h`J1Z&1LT9fg7*G`O4Ji| z;Y^SwJ0d#W$Ul_M8Su(#$Fz4jPK&C&&3}{}CqlQd(Uf%4dW$;?Ktb-hO7~#v($L{b zOLh~pB-ht}-~P6Q-1z4rWMGSf9W{;v05sraS&J!@ZUCE^!ZH0x!Z_t;?NvZjOrp3^0FgHFZvU#vJeA>h-ByDe^mYV|9 zo&+rQ8v4)0x$Poz_tZ?+&zQ@L?wRv5@scaJ?T?$b?Db!X~%HlS6Ckq@+XtEn29f(!01w@N{4u@!zyaaFm?tskIS%d-}Ib*Np}~>=yNL zmFvCdo${43;|tuRh?fL~4o<2GuCvZ>OdL1-YN~@Xs+tiWji8xXX$(#BO#dEc(mQY6 z{k-BdcIMmA^_+0jPyMBA1VkZABXh4lUY^JWAT5^;b_W`dub|{l19z()5h;)=tcV#<2;}ho`r#?_Vbkc9|G_9exd! zR4s_;X{_&;ciTA&cfuE*ht;R4+$et*zh6r$K!0qfwcXhjKCYfVh3`@%35jlTBicJ@ zT<);zybXcvIDw>ZDfCmuXwW+UD6kDG8dMqZ8|Doy6#!XCj;M%3@Aw z(y|OK7;O=DkT;%d-W;qw!1mUVo2V_*Eemg3OTz<`vD@~ipfggDCe5Y7Vmf;*X^nCG zszV!ca!9}6W-0pT_itLy0xe`E+t*9GW#_69FEX+v zi(kw%7TWkOc%C1Kc|9tT{O^0Hj%UN}&$h^b?8dYff|vVMt2L~gN=GtCI*74}$w z^RF<>7Nw|sbZ@-D^=w{qWzYshKYAL0$B+F;5zhL%)4(8!`de=~6+VirKE`Bg1(IU8 z2z5?H6n>v4RKZx;2f+~YD9Dc6hijCDfd9Rh}F%X~2lA@QjFz31!6)K({P=c3Ie8}^A!WOG`Yp7)-Z zXx*>PCO?#1$5D5nDD+6AesQnpen#d~kdxP@mae*nd84Oog8a__$Q9g!{A9FrFr|+b! z0MO3wlZ9i(-|s1MOFV6R+xV=`!E0e?jwjYo*UBR5Iw8pW!AW=IG{{9!yZ?LJ}=68|iId*4ktRPXRqxtO7jWI;Y|;^D7ASH`15kV)|;aL!_ViPd&Br1MFI9Cv_FNoV#<7UMA8kq2oKR%DV#YY+n<@L{Fk6)r>DARP zpX2)0F`uB3=Ka&XXzW;?4_Q!{l|EETm;&EuA#x4w$EH_d@FVOYV>D-bqXS`Fo7 z0B10SzRYrsPX1BnBMY_QZSb*=v4)zz~v2idjPoP}3{=>Emk?8<;G zyT~|tNve#8(TtS?HrW3XO)bBAp*lko>cgL#p~Bs&KT^6>CE0szuX3p@RO5BOpgy+| z-w{##m&yMnla#MuDh z5qJ=un;W&ufe>re@8%Qg#Z2Y1SfDJ>U6BERq=0Un|34**|InTPn;GXBa;A6Ssg;Gp z_pniBos&`pgY2NMxorKZAAG+ezUgdCeX^&Q_4NnVq(waluxiZsW>!jrRqO6t6l7Jh z+|Jxj@qU@7HPz*E9chkfy~|8YHv3SI4sC8_;1Dg;YJ6z^afNl%Xlilaf~CQbTdXlY zuUM}n)Q#)Ku?rZ2fLP_z^N6z6m1kf6c6CGId`(X`{OBGF23;y)+=Y;C*4pt&PNU(2 z0(;9|rhbT^7iu?O?i3r@l*JyB$P*^5qB(E4)6u3$?}+u>@9q?f$2v@=dTO|87J?=v zGZw#rf`gnL!e5M?7jh=XK7WL{_6=-o{_b0H#cwdk1t=LC!OB(78sd$a+ELzPD8PPF zmEo7xw7JW|bhBG3q$1szu+CCd&>^|eR!HP|fBbFe+1HTjUb0vvQ zxHV#Xr2{Kpnob?LH1zmIjBBsSow}>n&N7_9uw$1#fmgbJoN zqrO%GRUaOnMc1Z9-UIj`?}&P2{*Rc~JwKDuuk(}Pp9)0i>Fr`?db)4Vz`nET5(z;W zUkJeXcrgPBv5=_$C3|+46$5x!0;+dj(;@V-*B*LtBB6t}hG8}XW?%R= zlFXToQi}(Oa@jWXqzNlYFs&%!CZe?)Gy4!8apCB7GO6^D=hl;v88j{12JBWvdL&O* zVdX4>p)*z2YUJhV ztkq*!J3Z$wxE&r#=}p~K(c~#Etz4izu*!JThhIlA#PftL!x)*=LsZY;5>IoAUJTLU z!oI6<7V+@Y&99+>ESp`=xkF|~Da$&8EB>4YqX-N4WCX|$Pv5v|`e8;4q(!#4HRf@cd{WlZPe?M&e}K^_ zYQhMMsXJqmqIuiQ#w*Io!N@ry$?`DJkEf84eQ!vz7Yoz?f_(Y}eymKtU*8IiRDcKg z`jezbRmx**pIru1S(?-EZ&=;i5>qb5i`iczz-G!LDU4?EMic#ehh}xD| z5%J>MMr|Z*t-h=$RtXrvd-s5y<{=*9ecv>M++J#e`sk=bge&Ztxp7t!KE6}^BMfQL z(&c5T@x=b&6q%@g1!N*u}0&?fCC^VU#vE`E)lnfiOD1-?+{FPX91mCRN z{VFO*|IHV!W-S|z^ypEXAwf`1ooay)cT?eOH%}3+-oB8XO=en$lrJOJ#iS66m;51h z>s3lmR?tm^=V=x{E)Hoxi&m4;?VJS$^GvdnqVD`Z+!_QSrtJ?>>S~ z&xOOw&HB9^<>l-#zXq$;DLhP)!i(1iXx8#MIaEEmt+PxkAHG>X;{_<&?SRACk>Kqb zTuso*TU%~;0kLqSiOJj?b=TdA1nW@Xq$#hPs4+ONuzh!;%(==_bTmeWQVfRxC&&3r zoitd1?x_8!B4n{YK9IwaMWV5oB~|gJuzKZ9^MHNbD~~SJ3>FnP7R9=t?)6v-Qkek= z>GS_4(_vWbH?>wJCf63aRS@iazh3tf5bFux+#x2ZN)&h48=*k6zGxs`SJA|?c!Hh*3jSW>}QAG1b7L`uexLQ6D^LQh}Wb}N#=(yp8dwS;1+*t02rdevKHs1+{h&C-Zk zkf(1k=9k*ZRK0i!I*PjoLyVxde(Ur^eG6x;nQrH{%YGB)zGqgob5~L%swr$0eIkzC zMPXGdegOAq-MQZ?7yv6xa=4VD^IDB>X=D>z_)fLa)@C>gUu%gzH5)zFJ9Om8R4^c0 z6yTM^X*avn_?$w#SdE|R?#&K;sk<(~57X!v8?@h@Miw9%orn3(@863;7 z&aEk&YgEc54)MNDP;?%+RmY%Z;IE!3Jf?ag)`*emRA+ z74UWeGpGsNd}OwJS7?@b`o9wx%wRyI0+?F-PKhp>GB|bSYS84iflhu4sJaD=ySg=| zt2wTTPfe*tU*JiU>uSX-VwnhF@Iwdy_C-d9z*(<2Sku9>|MN4b|1Wu!09BPGR4$P1 z2VLN~nnxgg^6M>bYv5OilikTF7#1z23#vzx)%HN2wwOz;jECpuZ&Oj{vH|*GM<)(tp_qxQykM{PO7l(nSh+ptuRG*l)qYMVyj9!*DKPntZ znT(^CW+VpPQUNiam$~d!VP^j6$@X>1E+-OC4C_XPPW632RwqxDz8MF8Qs{trE}80&tSD_=t=FER>Ji<3XBPGhsQ zf~SvLTrD!=x(9o2MfUc*lB0+Ur&FZ@k3+aC2>3h6q^r0&0zrv8`@Oy+(IMHJEz(2d zf$yc(?B|X?ru%7XHcN34$VZr(sQDdVa~L)Yqy4(;o|ouNKK787^tvy4IQCwc2}f>g z_s?`Phr!7)DUazjRDg`u&+lj@#&)E4e4lZZ{FbEoZ2UAg5R=EYC-J0{G|?Rv2Q(z& z{!G_4amUTQ8K0&}>QtLmdQfw;$2*cE&8~LuP;dne%Mkyr2dC^~Gm6Kn>2{Du?>PDy zcv03Y)*N2C4C1Hp=uJN)<&{)zWuko=K)F0;vaRaLlm59apnGay*_x)x8a{p%J5a&0lw{@~RB}Tx}=mBb&A| z+b6mP%s_Bd-ZS+uTt&%6>0kEmg%~;LyuZM|3ZA~FZ|Lwzstzl3rQ%jeGjd!7_I%RW zZo*MK-mSa$t;5NB>iQ-d0r+eF*LUwAf!DHncDRm=&TI0ul+;{n8^do|y?h-GrAcFe z@mbK6{P=?n$F&rs9iryDrXF@9!5;$A4|#71{v8jEG2g-#K~8ecsYY-i-jnPgq<+4 zj58uFs_R>BNDO7M-T4il5zziodF`#A)MF*V#qq_l$!5q5%H-kp3`pGsZOK9lN5BNV z1Wwv@^TfCML98XvcynCh2fKcC;anS{x>FlkB9ybP`imFu zwxs~Z5>RI4?oPPO-f@V?&22WJg=9a$hFw1f!S)6p*aIF(pAAv|115jY^fzMVr$9j( zK0a?cY0gO0+ZE-%lQ$1Ig4@AdNs-6ewqjh236x>mS{r|#9Gxe;%wx#m=rHYJt)YHL zRH9L1vY|B4eG$n&UQd0s%xOqcxa~_5#Xp`zsNYM#Ns>}4XObm(F9ePmB2mMLRSKkM zSMt2h^xu#V!k(tuv!Xvnd`qw_@Wcf+qZ|>!XTOX|kiy5vAhN}E=zXWZe=>I@D=)9b zn_q?jQoznWGf>`7i;OLSwp7D14t{Q~LY6!~XiO=<^J)E>&j8cX&MhYf$ zT%F){o!&M+hvr zgTyIxXLh_#FOQQta9_sne^Q%;kXDd9%va_QP~#L!_FLKdDm~d61UdvFa^RGniBsO0 zBR2q|bgsIdI8Si&(r~OV>Q^t>w)D)~*7%uJ4Q&Qw@!GO9u&(sX8>|*zbmTFPBbZEP zjGjfxd2SfkU8+6&J18H}B+q1WHW1)pw;U@tVp=cw$Bc=hH(y4@{{Gaua2B+!0>(Qq*W<^XK9glcT~Jn@xAT6F^!mRuE5#qt zDJVvx(Kqi!PkHtAnV2ZXw!1(jR<}2sjmssU=>toZXEJroE!BFe20kN;e=jf2ouvs0_r-I^^u5Fh zE!_y0Q_;alOhmben#hZ}caY)U1R^&Jm+%rFS=A-fOr7Fh>CRx}Q;n!@_O1EGca%P9 z@!pK0gyS3x0{k4oKALRRXXR>L_f&Ma5Bc2bY`Vbq%FzaipBy?H*GB2r&OhE*R#DHi z{Tlu-aO1xlKXhj@@U_bOJ;=wFCnGC0(8J5ZP=rPMB|Ow9HiM&uWN}ZwP%tX+YNkOr zTxb5ANAySpa2u`eX@%89<&*pK4_DoxxSD}ChULF# z9r^h+1qI%V1Aj%20B`{A6Mn|jg^*e7eTgR!VM$I={hG)3Xt+}fC2`}ZvN}L!Vy7-Z zx7-|(4Xx06&-MXW?;0#>n|aPG!|!{Vpp$XAh8x>+pqL>svsXc zYFGQ|EzH~GKB$r`V;=92clg}=>i!YFX$(Y8S5z6oMuE9k$BX*Jv0j;wk?ald;x_#IbjdK~ za1T?)wq)tz1TPlxe77ro&i@ut%K`#rVVlgTKvGTHqb8 z-@^{3Lr-Os&nlSqm!A*Uk8P*+Ay87l0c;p3+2j=Zs{D8ZC+ zfb`Y#&jtVF*+?v|Hc9 z^>9GBp+M7-8p8B5kA#FZg{CSJ+%4>jk6n?kueq?(+yOzk^V-A}jJ=Dqku@dCX3qmKpWZ_;@oQBg2#; zwWRv?plV@6x~-Eo_z)dkdCe7=+ zggM_KRVGaw5KE z$hoR{oQitNHoU*$n5S|??QK3T&`GX!dxmo3!H6TLCnY1f!wSg}QR6$D+(DJfFH(;jY zQylWNMR;bt*)t@F0tEp~rZx9cR)a&d7z@`Y_15u@TsB^=1WKw9N(N&)=wK;DBTVI7 z9fylCBZfCCPUF3NrgC^Q6O~!flkEAaeTJ567uVG2LZJ0<_77Q}(Rd*CM@?buioo}@ zL8JI{-V-DqGj!1Bn#c?;+vM?Pu6-g#o z3+?^H(rYc?UKq2MpV(D9V)u16DZwQ8j9(UGfS2WoKpTO*I)KFJ*DFuSk&Iom69?c`)y(f2&vxrMkn-y3Moq|=qL3;=~} zXbzPwo+dc>@x8m=%=Px?5oi>hq+Vi}BDJaq0Q|7FRwC=A`2!`f{2)(>*$aaF z;n0h3R9)zAaFM)@exDb+iLt(`1>kuM`VOVVah@jv{89o~W~MpWvjRnotF<+lbndq> zP_6^806qtUiNZ8k#B@b_k_GG1hVTe2jQX+j1!_!>fj76I5dE(3hQasGnk5?c`b{E{ zZWO#q2GqtE@j(oR+N4DNKouC$PO+B~2aftxE>Im1N33cmw@spFP@PuJO-yDyZ&~2f zT3x8BW-cECMhS~LD#{oj{M{8DePY16$rOmaw|E|&AM3`s=62lWbCLtdR5*+Snp)Cx zXv-ayKwB0-GWn9x;`;^OEBBFu;>8nL?F?&pKxURLw`PS2servFDH5`9R{>Dr;!rN+KvC`j)gyy^Jlpq1P-Ch>-5RM*mo2-p0t|xLlxakEYi{QB$CmtG+yx z*B8>|wG0L?)AUjc$?^uCF)TA6RH5Xw=z-#Q!g8aP;0ypd9^foB(=5gnt4~%|31)4d zAepXiW)X=Q-n8nMJ$^b;&XZ`)&wp@4)&0B#&ikfN~r)B)|92O2u$tDrY?uo}5}9rI5a*rH={+ zjdF9dWlbx>v3nB%2TXP6fSav`a9F{dtSkz(P*A(nQ;1Q%+*KG8x(IAV4FUlIIKX#` zt)x<-04I(_pDGOn(z*B!tR48($5>;6Mk#~9W*2~7al2c#u<-IgfMLf@cRB~J#;p1F zAO*1PYJlozy9iKma$Wt=nc+-ONrvx~)B^l28QIG#ttee*>AtF%ggy)BvH#FFOPe&7 z_ma}KH~-)4R(D&EzI^!etY@=AVoj5!KGwb05_I%%feMc>?Ye+JaGr~t^L(1whY|xM z26UD~Ve9mMZsw`}tvX^3^KQ3?PwK41+5=K0!U&QYK)F92t=~9Tuh|Oge5=|C=m283 zihssk0Q)9u_u4KEKY{L$KTz+L5xid?0BQtKU`UTT-pf_20?c`1Af8+6w4>i6?8YSu|0so^YUngzPkv;fGw z5qx`dsxbL&9(&#yQb4lp=8O1F#DzvBk_T@U_lI#N@_>TguV2v(UccpAXtI9VnWCHb z>w963z#)Vfr~@^8+ybQVkJ#hVsF#TbRp@CiB0-Xf#+0^!XJq2}y|5 zs|A1FPKgeKB@~C+@Z;NnwaIN$DZLKLYZDup`yW+JSz$&SGD_5x6LGq`OU zL=}t8k^~2R#T%$Ze>!aq&QK>-u5>)qtgv_n_Kcs{bjD-&Eknaaf%#dS=x$;nNF}{C zGdBUQM)lN6I$ngUsW`r_en<}O0;zesEW`_g&=)^SbiprJtBW`0^os{x*^p!uStC5Ij@)S8V0?+se}L_9 z54Pw3Vgn7}e`jh%O!3Gk)~#7<0US0Eg&dKx2RtL-N~U_04sh#KDqW0_B=o@qEStIm z;RP%WR~*GuNm)Y`m3?X>3{%1s)2o0S;{!)Y-vlx}8!BNTutWUklj;FexxD(>%}qsT zKMF|zkL^M&OzA2ZNt~qkc1?tW&GvNep#yoEk|nHQfJzcqtp9tzqyPja&EbDtfA#j_ z+BnJf17W>db9}E-`WDF?-j8d*d;eS5s>i>k@rP3XT4OEj8I(i1t=*3c_n-AV7nE#& zSJTIV>S~1;u$yD&-o(cbb>s4B=?@|vVN<^R;W%ctz?-n;0y@gZg!yy99Pi*J_2yu6 zPtm4I($myxi6!2*F^ygM+9EuS>aF$0XP-a8uu z)l}7qv$``1s$FV@xa3^#EFnFFW2;fVU4~laGNxP#s07AilRZ`TbvB&pfhejF^8Ol} zuGj{NHG3UvqtS4*Ud=sTNfriZR}Xd6XsiY|+kKDJGzBvfm^{8O?8jq%)vzNg{B11` zdRf*s0eaCHC{uHQuxE(B)S#05loAR zO`rg**w4?YHhn9a+8H#Ey@Tx!e01|{76BOGwkPOQ^z_EcB)e*XTEe(eMZ!P<%T!L%3nb9#0y9`zKPajnV$$* zg$q~{&-;wqX$}LZ5#^L0WwEG)cVDZ2qSjNCOA3T090pJTeSnNJvV1D?O1Z`f`GWe$VjyJwWdzp zJXeC+CL`xs$GL9aE!PDh<}+RAzi-n%zB=w5?&pq&<)pR6Mwt? zGdwOCy^ubQa%H^u*3lZS-%LU&(>>vySIx8(xA#o9gt5ljDwnr6j^MM>Tn0Aw8@}0J zHZ8`FWHHDOCD5$|bY|ug6leU72X6$KMMPOe?*G}HCtJJ^TH^YQav4Bvv9(G?JoWZu z$6)~iC~~!$nE0@?hieb1(z0kBC;HTyA2xtVY`kctvfuSc{`w7 zI^2MkvVzA~>+v4ctcB`VVWmlg7@%B{75JG+q>Pl@Dw3Tnl4J`Heyp=zM{Dcs$&?q( zrwEi^09azagi_$|5Lo=Rlj`eIZ2|IG4XM2lBUwwQfK1}p^iX2khGu|7whnoaWyVV( zl3aVwiCJw_L1i$uj_~Q~$uk{F(QVeMGd{daNQkG%>;ro?%`jWNZ#r3yi_~qWT)qph z@0TWpic5->Va^wccDvVP@bU9vpM73z$6@Srf4q~5?*pN9eSokbsuVYD3IiqvM6{*5 za4=`WR?U_0x?CEN-vf4L`}Z**Z!~YUXT;pLg%^|04Hqm8V`6z`P8*>T%So}84#1GA z7ht&$u_3C=qU-Rr8v1-j_BT`mtc}*P1;u=cRPnl+>tqCjas-GC_*O7z6cF!K`lfvK z$b)8>lgW8V@&Pft?_4x1KZEQztzDyoEhy z=VmVT9g`lS3$VF|qRyFdn?W+F>_|X<)S*Ra5+-x=_3;SC@8=6B0E8# zd%KQZU+o3VgXg)BGlL`f#$h%SYc1E@MGAHzQwq9o6X3*HL3%*Sy#rSdJ~kDyk!s`u z2$$mD9kUi4OPqiGdT7zCa)Xm;R?iruC}HPuN5KIo=hkjthK0bkn)(30CM47yBtukQ zr$Vn`iGS!*<;5s6EyRX(CWkx07%CF)!dmLi_)7+4N7$@I3;Bduqcw4JR^W+|9J+jX zPiC6w*_OUL41wv#0O|$8PkE5E1xLU-=A5aooc3 zMRpxCyoQITJR3O(o|_ zo|$;G#7Z^rx&oRbN6f3$a0#%2van2w4^Gi~Fre7+zL(PSY6ZSB#Lyp;YuiW{j0?e( z(Fa~(CXn`9DM}Ex=>nf1W97AwUwD3WDe+mvJnWSaE!CEL&hH1>GWv|la?5*{rmR`X zqKCQ%eDiH}Wk^O2Ee$DJz3Q2fzgF+l`$0el;7;JW2MMiwL(FU|!~obU8aeHLucSld z78^H1=DqK!7`X#L{yBrXYeCC0^7JkCefihdah?o@DVYo9`_W4>wNX)(;08SgHCKNJ z_{jisxRuPDxwG4gP;`~Ct?c-zR0Q>V^gU9{&l&%LNV8sImydo1_&Eh(#m}Yvghai8 zC{XchIK9;eQgeV{2RMLMEZ4l&I|?#UjWU!%mIM!U`{4hQN4V4T{ePt7-f^_nt=U;W zLO%lFR^N7V04|*_N^81a!q1*Lb+&vDw!(*a{lFCS>4tX#keFZp0gU=*RL$xqD$_v& z?FiPYngY%{9o`jMpotXBuPqDv&XoC`(&79+7tt^R8)5@aAz*VNzt$RZ8A(z3AfQQ*l{eqZgv2jsH*+@j=za1=oJ=gbWCQGHvLc-(YoPjRqLU zHs(1wbWtJC?_RLOJ8UE}`M?>`Q6JQp^>3mR}PsU;O4>+M3)4 z_LNERB_P%E%>G@QG9+IPV|efp+GwqxrStRFwO?L!UNWs6jbX^2JMf{7tGjPSZeA&O z#zrBzcWTnUottTB7;KOA%%O{o$IMvBr@tfC(|k zL51!Aj>8@ZO#cA<5@RC*J({GPiI{cR0s0@ywZsF%@a`^*+&>_gzY}DQ^g&d$fMDXm z$|L1Cb*luol>P|+UKGGU(5T!an4Q}&&>4o3)2McT4~GY9|0#W63LrA9IAC#SbNZ-q zs`o>X)M`7onV^bs9hLOc>e#MS8>_2!`KuhFI~~Fcpn-*gb-_QcAWo3r619h?=)N!7 zqONL_y59!A@jTPHQU|-6)MJ;w_G+0(<7|iyk+!kXdqhV&Ws74zKhBw=BIG_naS!~5 zti|pFEu^t4_l}d^N(z*;Jj~^6pgiv%BvB;y{7!&<>Az%k@0;b{SP=UqZQu3nC!byL zu|zuoKo;)@3YFy?rTs(SN^`5@`Uksuo?l(j_!n0oBMFn{#up;}{pVH0V4SP9S&p+C zm+1-%SZ2md_nih%(A$S+%*Hb~6tcM~^C2$|p|IYx0dFA=3Jd^DDxls6_)-9Y3I@!DP68%bo|af_ zA+ihptPF?6slJ5HDS#!VeW?ZLgf7Ve`j7j-%K_9r?*0Eq?x)b*_*jiT>c(a!d+Aeq zSW)>#{~CSZwZa;|9CCL@UAs6CodHV&sCjre$AWH`Ueo~MJ~s59Okn^z84%hF0G?(5 z1rT~2#})w>4%oyc7XY%{Ty4{owvZpxU*D{yg^W)pQ6MHTuN7@iW!(XIk5T1(dXDa1 z{y>WWd*QyG0iL8lDA&*`#QKAgL4a5zn*~h;Fc1iNzgdeALO}4q{E>eHGNso9TFO~g zO!+*9f?$B`b)>?21^_RUvlw_d+GwDa!{M2CN88E6B+sm7l8U6@;r)_ENsf~5?a>oM z%svFh8$AOa#*HyzOn`us)B4AE+v|mbflp0z%qe)-tTpmBXHxVnsKY|bf>_a2o?tfV zv@e`wd4zx=om4J2aR3JROanyz!w>8gBzO{(oYhuY8@=+X-;>7>zG7o5>>>S4u`W>9 z%~xyw4u!Yg7fAe? z%=UT;3XMBs&VlF^;(SZ(Tb~0kA5H*}-THsbIvY%S9Wpa2lf$6Lok2-Lu|@t#Q|wk} z2ky3Uz@RB6&MFc@nvuct3f(gjcXDi5%e0M=klDTtMWcq+eh)@|Ug_=fNv40OA33l6 z1?5x^NC|^HL|5Q|xnlr$dih>O9?`n=DR{b5vK0;DlJ&7uz!!olu}o%U|HCK}``-Z$ z|GS~=f0dv3KS-ef+h`i!bH&>e!dI&enc$1w z{qtsTo1b~5k6)Pd4e6xMU${SE7ic&Y+SF^LiFnUeFw znd`Y`-ND)=4Z8$h2P)$6Dg`f9Cks945pIo#>OK9rXxjsFT~^{sI@vaM4Z1{Y)dMqe zPpsGXPcB9gp#+b_bd7?fTnDIuqtJ?TcMDtcf<(>F(G)664-ttLvrO&Zn!v^EmZXjC z$wy<$R^g8lGuWGQ7(8JuI`w})PvTyRgcrPhnOlD;?*Nfnr|Vr`+c-%1_%U_4M=&F! z^3iNSDbaGNXtM72ZxPNVe^%(WrT64Hlq$`|FGlvCrnS~Zr8Rt9VdM0^-Mq)*cyi%- zIGD|>Yt`{;;}wxg)7_+i;O8Nb7hGj=KJu!{e7PUI?j;KR!plBYqL`x>&Pe)mf%aOJ zr)5OE`%5UxuqbzYOW=Xb@AG8I65O{WdRnkJ_XCra+|2SMB_e2>mR@u24&&JG2J*tk z^b3+{%noK4tFirNFvQCCTElXmI*0PUIQSr5dWj0oyK_5yv$G+gU z`b1*(qns*pfaq3}ymAetyXQuWu}t&P5^CR2-*`?gTS|^9b&*`6IkDyM1oB$Yq833U z20Vr-@!eyz>$fWCc7;|>l)Q>_Esf;w5F1{~pSM$UZ}M?rBc9bbIifob+xvnNLQ^HS zh9u`_$g9t`UUz^?Ua1{t7CEp}AS16f@$`zW7zHqJb$An}hWADF4~13UJ5x2#)ot8* zJ=;M?t2Jo$nvfy%HEeuz!mi$zz9o@M?gv;OnV0nspshTuPaQ3C7nx_ET)%nt7wTUB z)cCb5w-MIlO0m-zG~AH&z)gWtbCFeme?4H{{kUfRyku}+Jnm7SsVzbMy>x@ zYqb@odu`fkxEjxd>NbG>x?mv`AQN|aqTK11DqFSuqmxvLVjJHiG{WKt|Ed@#y#(Rt z8Q8>#D;V>ko=Y+3oR&Pyy#8wx1NQYC0p`nn$!wwXs092>9Je>_<2w4)M*6yDSa8b` zTT5(wD~T+XEG&(FJh@7Zsl_mv?bTjHixw;w$Zh+8(@;J>PfWK|#wk{Riq{3)so}wM zr(qH)IlHE;L~0xl!J*p7Y>CnI=#n+uI(faqa=0XiPesJpR_*EQdkdhkD3_RMMOYBW zst~ElHm~5Aws>R1@5(m)&Au<#&(+_CJ!0K0?;1>-$P#Zl9Ad#0p*K<0X{NOE zy^LU_VNUt5agS@dJAQ#ffz)-N8pio@MO*C^=Qrgs>J7}<3}2QnE3$@=V|I6Opr? zZxV^Q@#lflsw-mrb!UAJvvI~hJQUu*yLwXBp|oD1O2`=kD?pT9ryRjJo+r4@BIL{i1z=Z{qmuu@5SHW*{SL} zZOO;fU|IIxDjITki=S{AT5>qQa(c6r``kI(l@VulrB`2T4GRWcp%Mq&wm3^q#nDiB z=hyWQLhaA~EoS%IX0}u$bZhjdTkBVMh5yU@pSr&mST{<7rv4MZFM9d!tY7`fbFZ%e zXXk#T6LUxrj#?t`?B?|IEl!Fn*|+{Q-%*e*20VZR=p>u^e|uK0eEIS5@g;Isp51?N zNPUvHNBf!<=@#&iULLoQ;v^-A9$+U4=mmCAFNEO&@Vq48>>~rH!%s4IZ{^QV|CvL- WG1rByI}r!e&fw|l=d#Wzp$P!>iv7m` literal 41625 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. Optional parameters are enclosed in brackets `[]`. - -## about -Shows basic information about the bot. - -??? Summary "Examples" - **/about** - ![image](../assets/images/commands/about.png) - ----- - -## botinfo -Shows advanced information about the bot. - -??? Summary "Examples" - **/botinfo** - ![image](../assets/images/commands/botinfo.png) - ---- - ## choose **Syntax:** `choose ` @@ -37,43 +18,6 @@ e.g. "Choice A" ChoiceB "Choice C" ---- -## help -**Syntax:** `help [command]` -**Other aliases:** `commands` - -Shows help about a command or the bot. - -- If no command is specified, it will list all available commands -- If a command is specified, it will show further info, and its subcommands if applicable. -- If a category is specified, it will show only commands in that *categor*. - -Various symbols are used to represent a command's signature and/or show further info. -**<argument>** -This means the argument is __**required**__. - -**[argument]** -This means the argument is __**optional**__. - -**[A|B]** -This means the it can be __**either A or B**__. - -**[argument...]** -This means you can have __**multiple arguments**__. - -πŸ”Έ -This means the command has subcommands. -Check the command's help to see them. - -??? Summary "Examples" - **/help** - ![image](../assets/images/commands/help_1.png) - **/help guild** - ![image](../assets/images/commands/help_2.png) - **/help guild info** - ![image](../assets/images/commands/help_3.png) - ----- - ## events **Other aliases:** `event` @@ -339,21 +283,4 @@ You can also specify the number of sides of the die, using the format `TdS` wher ![image](../assets/images/commands/roll_1.png) ---- - -## serverinfo - -Shows the server's information. - -??? Summary "Examples" - **/serverinfo** - ![image](../assets/images/commands/serverinfo.png) - ----- - -## uptime - -Shows how long the bot has been running. - -??? Summary "Examples" - **/uptime** - ![image](../assets/images/commands/uptime.png) + \ No newline at end of file diff --git a/docs/commands/info.md b/docs/commands/info.md new file mode 100644 index 0000000..7920461 --- /dev/null +++ b/docs/commands/info.md @@ -0,0 +1,135 @@ +# Info commands + +!!! info + Parameters are enclosed with `< >`. + Optional parameters are enclosed in brackets `[]`. + +## about +Shows basic information about the bot. + +??? Summary "Examples" + **/about** + ![image](../assets/images/commands/about.png) + +---- + +## botinfo +Shows advanced information about the bot. + +??? Summary "Examples" + **/botinfo** + ![image](../assets/images/commands/botinfo.png) + +--- + +## commands +**Other aliases:** `commandlist` + +Shows a simple list of all commands. +This displays all the commands you can use, with no description or subcommand information. +Note that different commands might show up in server channels and in private messages. + +For more details, use `help`. + +??? Summary "Examples" + **/commands** + ![image](../assets/images/commands/commands.png) + +--- + +## emojiinfo +**Syntax:** `help [emoji]` + +Shows information about an emoji, or shows all emojis. + +If the command is sued with no arguments, all the server emojis are shown. + +If an emoji, its ID or name is provided, it will show more information about it. + +Only emojis in the current servers can be checked. + +??? Summary "Examples" + **/emojiinfo** + ![image](../assets/images/commands/emojiinfo_1.png) + **/emojiinfo :energyDamage:** + ![image](../assets/images/commands/emojiinfo_2.png) + +--- + +## help +**Syntax:** `help [command]` + +Shows help about a command or the bot. + +- If no command is specified, it will list all available commands +- If a command is specified, it will show further info, and its subcommands if applicable. +- If a category is specified, it will show only commands in that *categor*. + +Various symbols are used to represent a command's signature and/or show further info. +**<argument>** +This means the argument is __**required**__. + +**[argument]** +This means the argument is __**optional**__. + +**[A|B]** +This means the it can be __**either A or B**__. + +**[argument...]** +This means you can have __**multiple arguments**__. + +πŸ”Έ +This means the command has subcommands. +Check the command's help to see them. + +??? Summary "Examples" + **/help** + ![image](../assets/images/commands/help_1.png) + **/help guild** + ![image](../assets/images/commands/help_2.png) + **/help guild info** + ![image](../assets/images/commands/help_3.png) + +---- + +## serverinfo +**Syntax:** `sererinfo [server]` + +Shows the server's information. + +The bot owner can additionally check the information of a specific server where the bot is. + +??? Summary "Examples" + **/serverinfo** + ![image](../assets/images/commands/serverinfo.png) + +---- + +## uptime + +Shows how long the bot has been running. + +??? Summary "Examples" + **/uptime** + ![image](../assets/images/commands/uptime.png) + +--- + +## userinfo +**Syntax:** `userinfo [user]` +**Other aliases:** `memberinfo` + +Shows a user's information. + +If no user is provided, it shows your own information. + +About user statuses: + +- Server Owner: Owner of the server +- Server Admin: User with `Administrator` permission +- Server Moderator: User with `Manage Server` permissions. +- Channel Moderator: User with `Manage Channels` permissions in at least one channel. + +??? Summary "Examples" + **/userinfo** + ![image](../assets/images/commands/userinfo.png) \ No newline at end of file diff --git a/docs/faq.md b/docs/faq.md index e097949..aac0e43 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,46 +1,37 @@ # FAQ -!!! note - In order to run and manage NabBot, basic knowledge of Python is expected. +### `im` command is not working +In order for `im` to work, you need to first configure a world for yor server. This can be done using the command `settings world`. -## Can you help me or make a cavebot/macro? -**No.** +For the moment, only one world can be configured per server. -## How do I invite NabBot to my server? -Unfortunately, this is only a self-hosted bot. You must run the bot by yourself. - -## I can't run NabBot, python is not found. -If you get an error like this: `β€œpython” is not recognized as an internal or external command, operable program or batch file.`, it can be due to one of these reasons: -* Python is not installed. To install visit [Python.org](https://www.python.org/) -* The python folder is not in the `PATH` enviroment variable. This is prompted when installing Python. +### How do I invite NabBot to my server? +For an always up to date invite link, go to [Discord Bots](https://discordbots.org/bot/178966653982212096) and click **Invite**. +Also, if you like NabBot, don't forget to vote for us! -## \_\_\_\_\_\_\_\_\_\_ doesn't look well or doesn't work on mobile +### \_\_\_\_\_\_\_\_\_\_ doesn't look well or doesn't work on mobile The Android and iOS versions of Discord are really behind the web or standalone client, so NabBot is focused on desktop users. If we limit ourselves to the mobile versions we would be slowing down the development of NabBot. -## How can I make NabBot work for this OT server? -NabBot was made to work specifically for Tibia, it depends completely on the layout of Tibia.com so even if the URLs were changed to an OT's website, the code used to parse information would have to be changed. - -However, if you're an OT owner, an API could be easily developed to let NabBot extract information more efficiently and even show a lot of extra information that is not available in Tibia.com - -## My world is getting merged, what do I do? -NabBot can't detect world merges, so you have to run the `/merge` command to change all content of the old world to the new one: +## Self Hosting -``` -/merge Fidera Gladera -``` +!!! note + In order to run and manage NabBot, basic knowledge of Python is expected. -This will tell the bot that **Fidera** has merged into **Gladera**, changing all references of Fidera to Gladera. -Note that this change be irreversible, so this must be used under your own risk. +### I can't run NabBot, python is not found. +If you get an error like this: `β€œpython” is not recognized as an internal or external command, operable program or batch file.`, it can be due to one of these reasons: +* Python is not installed. To install visit [Python.org](https://www.python.org/) +* The python folder is not in the `PATH` enviroment variable. This is prompted when installing Python. -It's recommended to do this right after the server save that will merge the worlds. +### My world is getting merged, what do I do? +NabBot can't detect world merges, so you have to run the `/merge` command to change all content of the old world to the new one: -## Someone got a namechange, what now? +### Someone got a namechange, what now? Name changes are automatically updated by NabBot, so nothing needs to be done here, the character will be updated automatically the first time he is checked. Namelocks are more complicated. -## What do I do with namelocked characters? +### What do I do with namelocked characters? Because of the way namelocks work, all references lost to the old name are completely gone on tibia.com, so it's nearly impossible to detect this change, resulting in people just registering their new character and ending with duplicates. @@ -55,3 +46,22 @@ Otherwise, it will just rename Oldname to new name. Since there's no way to check this, this should be done with care. The only thing that NabBot can verify is that their vocations match. + + +``` +/merge Fidera Gladera +``` + +This will tell the bot that **Fidera** has merged into **Gladera**, changing all references of Fidera to Gladera. +Note that this change be irreversible, so this must be used under your own risk. + +It's recommended to do this right after the server save that will merge the worlds. + +## Miscellaneous +### Can you help me or make a cavebot/macro? +**No.** + +### How can I make NabBot work for this OT server? +NabBot was made to work specifically for Tibia, it depends completely on the layout of Tibia.com so even if the URLs were changed to an OT's website, the code used to parse information would have to be changed. + +However, if you're an OT owner, an API could be easily developed to let NabBot extract information more efficiently and even show a lot of extra information that is not available in Tibia.com diff --git a/mkdocs.yml b/mkdocs.yml index 04e9c46..bebb8cc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -58,6 +58,7 @@ pages: - Overview: commands/index.md - Admin commands: commands/admin.md - General commands: commands/general.md + - Info commands: commands/info.md - Loot commands: commands/loot.md - Mod commands: commands/mod.md - Owner commands: commands/owner.md diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 0000000..a9dae54 --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,4 @@ +mkdocs +mkdocs-material +pymdown-extensions +git+https://github.com/jdittrich/figureAltCaption.git \ No newline at end of file From 5cfd73f72bd467e51a4ede38f19225a42ab93c65 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Mon, 30 Jul 2018 20:42:10 -0700 Subject: [PATCH 24/26] Updated changelog and fixed bug in GitLab pipeline --- .gitlab-ci.yml | 2 +- CHANGELOG.md | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b3cbe1f..2962778 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,7 +2,7 @@ image: python:3.6 pages: before_script: - - python -m pip r requirements-docs.txt + - python -m pip -r requirements-docs.txt script: - cp CHANGELOG.md docs/changelog.md - python -m mkdocs build -d public diff --git a/CHANGELOG.md b/CHANGELOG.md index bd069c0..9e963e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # Changelog ## Version 1.4.1 (Unreleased) - Small changes to `/userinfo` +- Fixed bug in `/monster` with creatures without a bestiary class defined. +- Added loading message to `/im`. +- Bot owner can now use `/serverinfo` to see other server's info. +- `/servers` now has pagination and sorting. +- NabBot now responds to a mention with its command prefixes. +- New `/emojiinfo` command +- Created new cog `Info`, moved information commands from `General` +- Level up and death messages now use lambdas for filtering. +- Server settings can now be changed with `Manage Server` permissions instead of `Administrator` permission. +- NabBot's initial message is now sent on a server channel instead of PMing the owner. ## Version 1.4.0 (2018-07-24) - `/loot` has been rewritten: From a2f7907a68a7018afc269e1ee9f1872690be0d32 Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Mon, 30 Jul 2018 20:44:40 -0700 Subject: [PATCH 25/26] Fixed GitLab pipeline - No really, I did it this time --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2962778..1075392 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,7 +2,7 @@ image: python:3.6 pages: before_script: - - python -m pip -r requirements-docs.txt + - python -m pip install -U -r requirements-docs.txt script: - cp CHANGELOG.md docs/changelog.md - python -m mkdocs build -d public From e53018ba6af4757f13339d1ef55a3577a3637f8d Mon Sep 17 00:00:00 2001 From: Allan Galarza Date: Mon, 30 Jul 2018 23:59:25 -0700 Subject: [PATCH 26/26] Bumped to 1.5.0 --- CHANGELOG.md | 2 +- cogs/settings.py | 2 +- docs/assets/images/commands/servers.png | Bin 24998 -> 37498 bytes docs/assets/images/commands/settings.png | Bin 26401 -> 35452 bytes docs/commands/owner.md | 10 ++++++++++ docs/commands/settings.md | 2 ++ nabbot.py | 2 +- 7 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e963e7..0b36dcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ # Changelog -## Version 1.4.1 (Unreleased) +## Version 1.5.0 (2018-07-31) - Small changes to `/userinfo` - Fixed bug in `/monster` with creatures without a bestiary class defined. - Added loading message to `/im`. diff --git a/cogs/settings.py b/cogs/settings.py index 1079d2a..e7b6e17 100644 --- a/cogs/settings.py +++ b/cogs/settings.py @@ -35,7 +35,7 @@ async def convert(self, ctx, argument): class Settings: """Commands related to server customization. - Settings can only be modified by Server Moderators (users with Manage Server permission).""" + Settings can only be modified by Server Moderators (users with `Manage Server` permission).""" def __init__(self, bot: NabBot): self.bot = bot diff --git a/docs/assets/images/commands/servers.png b/docs/assets/images/commands/servers.png index 2f063697217b09568ded44de17e9430570dc7f44..c77e5cbf6b29dc4fd5f44105a2c8ac1336b09b42 100644 GIT binary patch literal 37498 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%0A_|kY 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 VxBPxi-J1t|tj%#}t4+P){|PpFu-pIu literal 24998 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 pWX*xefuB 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. Optional parameters are enclosed in brackets `[]`. diff --git a/nabbot.py b/nabbot.py index c57db25..2460e11 100644 --- a/nabbot.py +++ b/nabbot.py @@ -43,7 +43,7 @@ def __init__(self): # A list version is created from the dictionary self.tracked_worlds = {} self.tracked_worlds_list = [] - self.__version__ = "1.4.1a" + self.__version__ = "1.5.0" self.__min_discord__ = 1480 async def on_ready(self):