diff --git a/CHANGELOG.md b/CHANGELOG.md index bc64052..686107d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,21 @@ - πŸ› Fixed bug - ❌ Removed feature -### Version 2.2.0 (2019-03-07) +## Version 2.3.0 (2019-04-19) +- βœ” New subcommand `/unregistered guild`, checks which members of a guild are not registered in the server. +- βœ” New owner command `/logs` to upload log files. +- βœ” New subcommand `/news ticker`, displays recent news ticker messages. +- βœ” New ticker messages are now announced along with news articles and featured articles. +- πŸ”§ `/quote` now shows a link to the original message. +- πŸ”§ Added auto sharding. +- πŸ”§ No longer using a development version of `discord.py`, now using version v1.0.0 +- πŸ› Fixed error in `/event subscribe`. +- πŸ› Fixed bug not allowing to check characters with `.` in their names. +- πŸ› Fixed bug that duplicates certain server-log messages. +- πŸ› Fixed with time strings (`2d`, `1d4h`, etc) not working with spaces around them. +- πŸ› Updated TibiaWiki database. + +## Version 2.2.0 (2019-03-07) - βœ” Added option to disable custom messages for deaths and level ups. `/settings simpleannouncements` - βœ” New `/purge` owner command, cleans settings for servers where the bot is no longer in. - βœ” Added option to set how long ago was killed, to reduce that from the cooldown timer. e.g. `/boss set Lloyd,Tschas,1h30m`. diff --git a/LICENSE b/LICENSE index adc1677..f149dc3 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2018 Allan Galarza + Copyright 2019 Allan Galarza Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index df86dc8..dd0c3fb 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ You can also host your own instance of NabBot. ### Requirements - Python 3.6 and up - Python modules: - - [discord.py (rewrite branch)](https://github.com/Rapptz/discord.py/tree/rewrite) + - discord.py - psutil - pillow - BeautifulSoup @@ -49,4 +49,4 @@ If you like NabBot, you can donate to this project. NabBot and the developers wi -*[Tibia](http://tibia.com) is made by [CipSoft](https://www.cipsoft.com/), all game related images are copyrighted by [CipSoft GmbH](https://www.cipsoft.com/).* \ No newline at end of file +*[Tibia](http://tibia.com) is made by [CipSoft](https://www.cipsoft.com/), all game related images are copyrighted by [CipSoft GmbH](https://www.cipsoft.com/).* diff --git a/cogs/core.py b/cogs/core.py index bc2579f..0b87763 100644 --- a/cogs/core.py +++ b/cogs/core.py @@ -39,7 +39,7 @@ async def game_update(self): # Entries starting with "l:" are prefixed with "Listening to " presence_list = [ # Playing _____ - "Half-Life 3", "Tibia on Steam", "DOTA 3", "Human Simulator 2018", "Russian roulette", + "Half-Life 3", "Tibia on Steam", "DOTA 3", "Human Simulator 2019", "Russian roulette", "with my toy humans", "with fireπŸ”₯", "God", "innocent", "the part", "hard to get", "with my human minions", "Singularity", "Portal 3", "Dank Souls", "you", "01101110", "dumb", "with GLaDOS πŸ’™", "with myself", "with your heart", "League of Dota", "my cards right", diff --git a/cogs/general.py b/cogs/general.py index 2a9ee6e..ce26fe0 100644 --- a/cogs/general.py +++ b/cogs/general.py @@ -1,6 +1,6 @@ import logging import random -from typing import List +from typing import List, Optional import discord from discord.ext import commands @@ -90,7 +90,7 @@ async def quote(self, ctx: NabCtx, message_id: int): Note that the bot won't attempt to search in channels you can't read. Additionally, messages in NSFW channels can't be quoted in regular channels.""" channels: List[discord.TextChannel] = ctx.guild.text_channels - message: discord.Message = None + message: Optional[discord.Message] = None with ctx.typing(): for channel in channels: bot_perm = ctx.bot_permissions @@ -100,7 +100,7 @@ async def quote(self, ctx: NabCtx, message_id: int): auth_perm.read_message_history and auth_perm.read_messages): continue try: - message = await channel.get_message(message_id) + message = await channel.fetch_message(message_id) except discord.HTTPException: continue if message is not None: @@ -114,13 +114,13 @@ async def quote(self, ctx: NabCtx, message_id: int): if message.channel.nsfw and not ctx.channel.nsfw: await ctx.error("I can't quote messages from NSFW channels in regular channels.") return - embed = discord.Embed(description=message.content, timestamp=message.created_at) + embed = discord.Embed(description=f"{message.content}\n\n[Jump to original]({message.jump_url})", + timestamp=message.created_at) try: embed.colour = message.author.colour except AttributeError: pass - embed.set_author(name=message.author.display_name, icon_url=get_user_avatar(message.author), - url=message.jump_url) + embed.set_author(name=message.author.display_name, icon_url=get_user_avatar(message.author)) embed.set_footer(text=f"In #{message.channel.name}") if len(message.attachments) >= 1: attachment: discord.Attachment = message.attachments[0] diff --git a/cogs/info.py b/cogs/info.py index 1c808d7..f9be549 100644 --- a/cogs/info.py +++ b/cogs/info.py @@ -21,7 +21,7 @@ class Info(commands.Cog, utils.CogUtils): - """Commands that disploy general information.""" + """Commands that display general information.""" def __init__(self, bot: NabBot): self.bot = bot @@ -60,16 +60,18 @@ async def about(self, ctx: NabCtx): @checks.can_embed() @commands.command(name="botinfo") - async def bot_info(self, ctx: NabCtx): + async def _bot_info(self, ctx: NabCtx): """Shows advanced information about the bot.""" async with ctx.pool.acquire() as conn: char_count = await conn.fetchval('SELECT COUNT(*) FROM "character" WHERE user_id != 0') deaths_count = await conn.fetchval('SELECT COUNT(*) FROM character_death') levels_count = await conn.fetchval('SELECT COUNT(*) FROM character_levelup') - used_ram = psutil.Process().memory_full_info().uss / 1024 ** 2 + bot_ram = psutil.Process().memory_full_info().uss / 1024 ** 2 + bot_percentage_ram = psutil.Process().memory_percent() + used_ram = psutil.virtual_memory().used / 1024 ** 2 + percentage_ram = psutil.virtual_memory().percent total_ram = psutil.virtual_memory().total / 1024 ** 2 - percentage_ram = psutil.Process().memory_percent() def ram(value): if value >= 1024: @@ -89,13 +91,14 @@ def ram(value): 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" + f"πŸ“‰ RAM: **{ram(bot_ram)} ({bot_percentage_ram:.2f}%)**\n" \ + f"πŸ“ˆ Total 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"πŸ‘Ύ Servers: **{len(self.bot.guilds):,}** (**{self.bot.shard_count}** shards)\n" \ f"πŸ’¬ Channels: **{len(list(self.bot.get_all_channels())):,}**\n" \ f"πŸ‘¨ Users: **{len(self.bot.users):,}** \n" \ f"πŸ‘€ Characters: **{char_count:,}**\n" \ @@ -275,6 +278,7 @@ async def emoji_info(self, ctx: NabCtx, *, emoji: discord.Emoji = None): embed.add_field(name=name, value=value.replace("\n", "")) await ctx.send(embed=embed) + # TODO: Implement this command the proper discord.py way @checks.can_embed() @commands.command(name='help') async def _help(self, ctx, *, command: str = None): diff --git a/cogs/loot.py b/cogs/loot.py index a7b85da..b33c43a 100644 --- a/cogs/loot.py +++ b/cogs/loot.py @@ -223,10 +223,10 @@ async def loot(self, ctx: NabCtx): # Send on ask_channel or PM if await ctx.is_long(): - await ctx.send(short_message, embed=embed, file=discord.File(loot_image_overlay, "results.png")) + await ctx.send(short_message, embed=embed, file=discord.File(io.BytesIO(loot_image_overlay), "results.png")) else: try: - await ctx.author.send(file=discord.File(loot_image_overlay, "results.png"), embed=embed) + await ctx.author.send(file=discord.File(io.BytesIO(loot_image_overlay), "results.png"), embed=embed) except discord.Forbidden: await ctx.error(f"{ctx.author.mention}, I tried pming you to send you the results, " f"but you don't allow private messages from this server.\n" @@ -239,7 +239,6 @@ async def loot_legend(self, ctx: NabCtx): """Shows the meaning of the overlayed icons.""" with open("./images/legend.png", "r+b") as f: await ctx.send(file=discord.File(f)) - f.close() @checks.owner_only() @loot.command(name="show") @@ -265,7 +264,7 @@ async def loot_show(self, ctx, *, item: str): if len(fields) > 5: embed.set_footer(text="Too many frames to display all information.") embed.set_image(url="attachment://results.png") - await ctx.send(embed=embed, file=discord.File(result, "results.png")) + await ctx.send(embed=embed, file=discord.File(io.BytesIO(result), "results.png")) @classmethod def load_image(cls, image_bytes: bytes) -> Image.Image: diff --git a/cogs/mod.py b/cogs/mod.py index c2be0cd..c16beec 100644 --- a/cogs/mod.py +++ b/cogs/mod.py @@ -7,6 +7,7 @@ from cogs import utils from cogs.utils import converter +from cogs.utils.tibia import get_guild, get_voc_emoji, get_voc_abb from nabbot import NabBot from .utils import checks, config, safe_delete_message from .utils.context import NabCtx @@ -183,14 +184,10 @@ async def unignore(self, ctx: NabCtx, *entries: converter.ChannelOrMember): @checks.channel_mod_only() @checks.tracking_world_only() - @commands.command() + @commands.group(invoke_without_command=True, case_insensitive=True) async def unregistered(self, ctx: NabCtx): """Shows a list of users with no registered characters.""" entries = [] - if ctx.world is None: - await ctx.send("This server is not tracking any worlds.") - return - results = await ctx.pool.fetch('SELECT user_id FROM "character" WHERE world = $1 GROUP BY user_id', ctx.world) # Flatten list users = [i["user_id"] for i in results] @@ -210,6 +207,45 @@ async def unregistered(self, ctx: NabCtx): await pages.paginate() except CannotPaginate as e: await ctx.send(e) + + @checks.channel_mod_only() + @checks.tracking_world_only() + @unregistered.command(name="guild") + async def unregistered_guild(self, ctx: NabCtx, *, name: str): + """Shows a list of unregistered guild members. + + Unregistered guild members can be either characters not registered to NabBot or + registered to users not in the server.""" + guild = await get_guild(name) + if guild is None: + return await ctx.error("There's no guild with that name.") + if guild.world != ctx.world: + return await ctx.error(f"**{guild.name}** is not in **{ctx.world}**") + + names = [m.name for m in guild.members] + registered = await ctx.pool.fetch("""SELECT name FROM "character" T0 + INNER JOIN user_server T1 ON T0.user_id = T1.user_id + WHERE name = any($1) AND server_id = $2""", names, ctx.guild.id) + registered_names = [m['name'] for m in registered] + + entries = [] + for member in guild.members: + if member.name in registered_names: + continue + emoji = get_voc_emoji(member.vocation.value) + voc_abb = get_voc_abb(member.vocation.value) + entries.append(f'{member.rank} β€” **{member.name}** (Lvl {member.level} {voc_abb} {emoji})') + if len(entries) == 0: + await ctx.send("There are no unregistered users.") + return + + pages = Pages(ctx, entries=entries, per_page=10) + pages.embed.set_author(name=f"Unregistered members from {guild.name}", icon_url=guild.logo_url) + try: + await pages.paginate() + except CannotPaginate as e: + await ctx.send(e) + # endregion @classmethod diff --git a/cogs/owner.py b/cogs/owner.py index 9d65950..5225eba 100644 --- a/cogs/owner.py +++ b/cogs/owner.py @@ -1,4 +1,5 @@ import inspect +import os import platform import textwrap import traceback @@ -24,7 +25,6 @@ log = logging.getLogger("nabbot") req_pattern = re.compile(r"([\w.]+)([><=]+)([\d.]+),([><=]+)([\d.]+)") -dpy_commit = re.compile(r"a(\d+)\+g([\w]+)") class Owner(commands.Cog, CogUtils): @@ -70,7 +70,7 @@ def check(m): @checks.owner_only() @commands.command() async def announcement(self, ctx: NabCtx, *, message): - """Sends an announcement to all servers with a sererlog.""" + """Sends an announcement to all servers with a serverlog.""" embed = discord.Embed(title="πŸ“£ Owner Announcement", colour=discord.Colour.blurple(), timestamp=dt.datetime.now()) embed.set_author(name="Support Server", url="https://discord.gg/NmDvhpY", icon_url=self.bot.user.avatar_url) @@ -256,6 +256,48 @@ async def load_cog(self, ctx: NabCtx, cog: str): except Exception as e: await ctx.send('{}: {}'.format(type(e).__name__, e)) + @commands.command(name="logs") + @checks.owner_only() + async def logs(self, ctx: NabCtx, log_name: str = None): + base_dir = "logs" + if log_name is None: + def file_size(size): + if size < 1024: + return f"{size:,} B" + size /= 1024 + if size < 1024: + return f"{size:,.2f} kB" + size /= 1024 + if size < 1024: + return f"{size:,.2f} mB" + + entries = [] + for log_file in os.listdir(base_dir): + path = os.path.join(base_dir, log_file) + if os.path.isfile(path): + entries.append(f"{log_file} (*{file_size(os.path.getsize(path))}*)") + entries[1:] = sorted(entries[1:], reverse=True) + pages = Pages(ctx, entries=entries, per_page=10) + pages.embed.title = f"Log files" + try: + await pages.paginate() + except CannotPaginate as e: + await ctx.error(e) + return + + if log_name and ctx.guild: + return await ctx.error("For security reasons, I can only upload logs on private channels.") + if ".." in log_name: + return await ctx.error("You're not allowed to get files from outside the log folder.") + try: + with open(os.path.join("logs", log_name), "rb") as f: + await ctx.send("Here's your log file", file=discord.File(f, log_name)) + except FileNotFoundError: + return await ctx.error("There's no log file with that name.") + except discord.HTTPException: + return await ctx.error("Error uploading file. It is currently not possible to read the current log file.") + + @commands.command(usage=" ") @checks.owner_only() async def merge(self, ctx: NabCtx, old_world: str, new_world: str): @@ -665,19 +707,7 @@ def comp(operator, object1, object2): elif operator == "<=": return object1 <= object2 - discordpy_version = pkg_resources.get_distribution("discord.py").version - m = dpy_commit.search(discordpy_version) - dpy = f"v{discordpy_version}" - if m: - revision, commit = m.groups() - is_valid = int(revision) >= self.bot.__min_discord__ - discordpy_url = f"https://github.com/Rapptz/discord.py/commit/{commit}" - dpy = f"{ctx.tick(is_valid)}[v{discordpy_version}]({discordpy_url})" - if not is_valid: - dpy += f"\n`{self.bot.__min_discord__ - int(revision)} commits behind`" - embed = discord.Embed(title="NabBot", description="v"+self.bot.__version__) - embed.add_field(name="discord.py", value=dpy) embed.set_footer(text=f"Python v{platform.python_version()} on {platform.platform()}", icon_url="https://www.python.org/static/apple-touch-icon-precomposed.png") diff --git a/cogs/serverlog.py b/cogs/serverlog.py index 362b9a7..ac4ff5f 100644 --- a/cogs/serverlog.py +++ b/cogs/serverlog.py @@ -344,13 +344,10 @@ async def on_member_remove(self, member: discord.Member): @commands.Cog.listener() async def on_member_update(self, before: discord.Member, after: discord.Member): """Called every time a member is updated""" - embed = discord.Embed(description=f"{after.mention}: ", colour=COLOUR_MEMBER_UPDATE) - embed.set_author(name=f"{after.name}#{after.discriminator} (ID: {after.id})", icon_url=get_user_avatar(after)) - changes = True - if f"{before.name}#{before.discriminator}" != f"{after.name}#{after.discriminator}": - embed.description += "Name changed from **{0.name}#{0.discriminator}** to **{1.name}#{1.discriminator}**." \ - .format(before, after) - elif before.nick != after.nick: + if before.nick != after.nick: + embed = discord.Embed(description=f"{after.mention}: ", colour=COLOUR_MEMBER_UPDATE) + embed.set_author(name=f"{after.name}#{after.discriminator} (ID: {after.id})", + icon_url=get_user_avatar(after)) if before.nick is None: embed.description += f"Nickname set to **{after.nick}**" elif after.nick is None: @@ -361,9 +358,6 @@ async def on_member_update(self, before: discord.Member, after: discord.Member): if entry and entry.user.id != after.id: icon_url = get_user_avatar(entry.user) embed.set_footer(text=f"{entry.user.name}#{entry.user.discriminator}", icon_url=icon_url) - else: - changes = False - if changes: await self.bot.send_log_message(after.guild, embed=embed) @commands.Cog.listener() @@ -377,7 +371,6 @@ async def on_member_unban(self, guild: discord.Guild, user: discord.User): embed.set_footer(text="{0.name}#{0.discriminator}".format(entry.user), icon_url=get_user_avatar(entry.user)) await self.bot.send_log_message(guild, embed=embed) - # endregion @staticmethod @@ -396,7 +389,7 @@ async def get_audit_entry(guild: discord.Guild, action: discord.AuditLogAction, return now = dt.datetime.utcnow() after = now - dt.timedelta(0, 5) - async for entry in guild.audit_logs(limit=10, reverse=False, action=action, after=after): + async for entry in guild.audit_logs(limit=10, oldest_first=False, action=action, after=after): if abs((entry.created_at - now)) >= dt.timedelta(seconds=5): break if target is not None and entry.target.id == target.id: diff --git a/cogs/tibia.py b/cogs/tibia.py index 64de39c..aaf9eef 100644 --- a/cogs/tibia.py +++ b/cogs/tibia.py @@ -1,6 +1,7 @@ import asyncio import calendar import datetime as dt +import io import logging import random import re @@ -28,7 +29,8 @@ from .utils.tibia import HIGHSCORES_FORMAT, HIGHSCORE_CATEGORIES, NabChar, TIBIACOM_ICON, TIBIA_URL, get_character, \ get_guild, get_highscores, get_house, get_house_id, get_level_by_experience, get_map_area, get_news_article, \ get_rashid_city, get_recent_news, get_share_range, get_tibia_time_zone, get_voc_abb, get_voc_abb_and_emoji, \ - get_voc_emoji, get_world, get_world_bosses, get_world_list, normalize_vocation, tibia_worlds + get_voc_emoji, get_world, get_world_bosses, get_world_list, normalize_vocation, tibia_worlds, \ + get_recent_news_tickers log = logging.getLogger("nabbot") @@ -44,10 +46,12 @@ class Tibia(commands.Cog, CogUtils): def __init__(self, bot: NabBot): self.bot = bot self.news_announcements_task = self.bot.loop.create_task(self.scan_news()) + self.news_ticker_task = self.bot.loop.create_task(self.scan_tickers()) def cog_unload(self): log.info(f"{self.tag} Unloading cog") self.news_announcements_task.cancel() + self.news_ticker_task.cancel() # region Events @@ -57,11 +61,11 @@ async def scan_news(self): log.info(f"{tag} Task started") while not self.bot.is_closed(): try: + log.debug(f"{tag} Checking recent news") recent_news = await get_recent_news() if recent_news is None: await asyncio.sleep(30) continue - log.debug(f"{tag} Checking recent news") last_article = recent_news[0]["id"] last_id = await get_global_property(self.bot.pool, "last_article") await set_global_property(self.bot.pool, "last_article", last_article) @@ -106,6 +110,61 @@ async def scan_news(self): except Exception as e: log.exception(f"{tag} Exception: {e}") + async def scan_tickers(self): + tag = f"{self.tag}[scan_tickers]" + await self.bot.wait_until_ready() + log.info(f"{tag} Task started") + while not self.bot.is_closed(): + try: + log.debug(f"{tag} Checking recent news tickers") + recent_news = await get_recent_news_tickers() + if recent_news is None: + await asyncio.sleep(30) + continue + last_article = recent_news[0]["id"] + last_id = await get_global_property(self.bot.pool, "last_ticker") + await set_global_property(self.bot.pool, "last_ticker", last_article) + # Do not announce anything if this is the first time the task is executed. + if last_id is None: + break + new_articles = [] + for article in recent_news: + # Do not post articles older than a week (in case bot was offline) + if int(article["id"]) == last_id or (dt.date.today() - article["date"]).days > 7: + break + fetched_article = await get_news_article(int(article["id"])) + if fetched_article is not None: + new_articles.insert(0, fetched_article) + for article in new_articles: + log.info(f"{tag} New news ticker: {article['id']} - {article['title']}") + for guild in self.bot.guilds: + news_channel_id = await get_server_property(self.bot.pool, guild.id, "news_channel", default=0) + if news_channel_id == 0: + continue + channel = self.bot.get_channel_or_top(guild, news_channel_id) + try: + await channel.send("New ticker message posted on Tibia.com", + embed=self.get_article_embed(article, 1000)) + except discord.Forbidden: + log.warning(f"{tag} Missing permissions | Server: {guild.id}") + except discord.HTTPException: + log.warning(f"{tag} Malformed message | Server: {guild.id}") + except AttributeError: + log.warning(f"{tag} No channel found | Server: {guild.id}") + await asyncio.sleep(60 * 60 * 2) + except (IndexError, KeyError): + log.warning(f"{tag} Error getting recent news") + await asyncio.sleep(60*30) + continue + except errors.NetworkError: + await asyncio.sleep(30) + continue + except asyncio.CancelledError: + # Task was cancelled, so this is fine + break + except Exception as e: + log.exception(f"{tag} Exception: {e}") + # endregion # region Commands @@ -625,7 +684,7 @@ async def house(self, ctx: NabCtx, *, name: str): pass # Attach image only if the bot has permissions if ctx.bot_permissions.attach_files: - mapimage = get_map_area(wiki_house.x, wiki_house.y, wiki_house.z) + mapimage = io.BytesIO(get_map_area(wiki_house.x, wiki_house.y, wiki_house.z)) embed = self.get_house_embed(ctx, wiki_house, house) embed.set_image(url="attachment://thumbnail.png") await ctx.send(file=discord.File(mapimage, "thumbnail.png"), embed=embed) @@ -727,7 +786,7 @@ async def levels_user(self, ctx: NabCtx, *, name: str): await ctx.send(e) @checks.can_embed() - @commands.command(usage="[id]") + @commands.group(usage="[id]", invoke_without_command=True, case_insensitive=True) async def news(self, ctx: NabCtx, news_id: int = None): """Shows the latest news articles from Tibia.com. @@ -735,9 +794,9 @@ async def news(self, ctx: NabCtx, news_id: int = None): if news_id is None: recent_news = await get_recent_news() if recent_news is None: - await ctx.error("Something went wrong getting recent news.") + return await ctx.error("Something went wrong getting recent news.") embed = self.get_tibia_embed("Recent news", "https://www.tibia.com/news/?subtopic=latestnews") - embed.set_footer(text="To see a specific article, use the command /news ") + embed.set_footer(text=f"To see a specific article, use the command {ctx.clean_prefix}news ") news_format = "{emoji} `{id}`\t[{news}]({tibiaurl})" type_emojis = { "Featured Article": "πŸ“‘", @@ -758,6 +817,19 @@ async def news(self, ctx: NabCtx, news_id: int = None): embed = self.get_article_embed(article, limit) await ctx.send(embed=embed) + @news.command(name="ticker", aliases=["newsticker"]) + async def news_ticker(self, ctx: NabCtx): + """Shows the latest news tickers from Tibia.com.""" + recent_tickers = await get_recent_news_tickers() + if recent_tickers is None: + return await ctx.error("Something went wrong getting recent news tickers.") + embed = self.get_tibia_embed("Recent news tickers", "https://www.tibia.com/news/?subtopic=latestnews") + embed.set_footer(text=f"To see a specific article, use the command {ctx.clean_prefix}news ") + news_format = "πŸ“ `{id}`\t[{news}]({tibiaurl})" + limit = 20 if await ctx.is_long() else 10 + embed.description = "\n".join([news_format.format(**n) for n in recent_tickers[:limit]]) + return await ctx.send(embed=embed) + @checks.can_embed() @commands.command(name="searchworld", aliases=["whereworld", "findworld"], usage="[,world]") async def search_world(self, ctx: NabCtx, *, params): diff --git a/cogs/tibiawiki.py b/cogs/tibiawiki.py index ffeeb94..7968462 100644 --- a/cogs/tibiawiki.py +++ b/cogs/tibiawiki.py @@ -1,4 +1,5 @@ import datetime as dt +import io import logging import random import re @@ -28,6 +29,20 @@ WIKI_CHARMS_ARTICLE = "Cyclopedia#List_of_Charms" WIKI_ICON = "https://vignette.wikia.nocookie.net/tibia/images/b/bc/Wiki.png/revision/latest?path-prefix=en" +DIFFICULTIES = { + "Harmless": config.difficulty_off_emoji * 4, + "Trivial": config.difficulty_on_emoji + config.difficulty_off_emoji * 3, + "Easy": config.difficulty_on_emoji * 2 + config.difficulty_off_emoji * 2, + "Medium": config.difficulty_on_emoji * 3 + config.difficulty_off_emoji, + "Hard": config.difficulty_on_emoji * 4 +} +OCCURRENCES = { + "Common": config.occurrence_on_emoji * 1 + config.occurrence_off_emoji * 3, + "Uncommon": config.occurrence_on_emoji * 2 + config.occurrence_off_emoji * 2, + "Rare": config.occurrence_on_emoji * 3 + config.occurrence_off_emoji * 1, + "Very Rare": config.occurrence_on_emoji * 4, +} + class TibiaWiki(commands.Cog, utils.CogUtils): """Commands that show information about Tibia, provided by TibiaWiki. @@ -307,12 +322,13 @@ async def npc(self, ctx: NabCtx, *, name: str): if ctx.bot_permissions.attach_files: files = [] if npc.image is not None: + thumbnail = io.BytesIO(npc.image) filename = re.sub(r"[^A-Za-z0-9]", "", npc.name) + ".gif" embed.set_thumbnail(url=f"attachment://{filename}") - files.append(discord.File(npc.image, filename)) + files.append(discord.File(thumbnail, filename)) if None not in [npc.x, npc.y, npc.z]: map_filename = re.sub(r"[^A-Za-z0-9]", "", npc.name) + "-map.png" - map_image = get_map_area(npc.x, npc.y, npc.z) + map_image = io.BytesIO(get_map_area(npc.x, npc.y, npc.z)) embed.set_image(url=f"attachment://{map_filename}") embed.add_field(name="Location", value=f"[Mapper link]({self.get_mapper_link(npc.x, npc.y, npc.z)})", inline=False) @@ -336,12 +352,13 @@ async def rashid(self, ctx: NabCtx): if ctx.bot_permissions.attach_files: files = [] if npc.image is not None: + thumbnail = io.BytesIO(npc.image) filename = re.sub(r"[^A-Za-z0-9]", "", npc.name) + ".gif" embed.set_thumbnail(url=f"attachment://{filename}") - files.append(discord.File(npc.image, filename)) + files.append(discord.File(thumbnail, filename)) if None not in [rashid.x, rashid.y, rashid.z]: map_filename = re.sub(r"[^A-Za-z0-9]", "", npc.name) + "-map.png" - map_image = get_map_area(rashid.x, rashid.y, rashid.z) + map_image = io.BytesIO(get_map_area(rashid.x, rashid.y, rashid.z)) embed.set_image(url=f"attachment://{map_filename}") embed.add_field(name="Location", value=f"[Mapper link]" f"({self.get_mapper_link(rashid.x,rashid.y,rashid.z)})", @@ -448,12 +465,13 @@ def get_charms_embed(cls, ctx: NabCtx): @classmethod async def send_embed_with_image(cls, entity, ctx, embed, apply_color=False, extension="gif"): if ctx.bot_permissions.attach_files and entity.image: + thumbnail = io.BytesIO(entity.image) filename = f"thumbnail.{extension}" embed.set_thumbnail(url=f"attachment://{filename}") if apply_color: main_color = await ctx.execute_async(average_color, entity.image) embed.color = discord.Color.from_rgb(*main_color) - await ctx.send(file=discord.File(entity.image, f"{filename}"), embed=embed) + await ctx.send(file=discord.File(thumbnail, f"{filename}"), embed=embed) else: await ctx.send(embed=embed) @@ -636,7 +654,7 @@ async def get_item_embed(self, ctx: NabCtx, item: models.Item, long): # region Item Embed Submethods @classmethod - async def get_item_embed_parse_properties(cls, embed, item): + async def get_item_embed_parse_properties(cls, embed, item: models.Item): properties = f"Weight: {item.weight} oz" for attribute in item.attributes: # type: models.ItemAttribute value = attribute.value @@ -830,69 +848,36 @@ def get_monster_embed_parse_loot(cls, loot: List[models.CreatureDrop]): return split_message(loot_string, FIELD_VALUE_LIMIT - 20) @classmethod - def get_monster_embed_bestiary_info(cls, embed, monster): + def get_monster_embed_bestiary_info(cls, embed, monster: models.Creature): if monster.bestiary_class: - difficulties = { - "Harmless": config.difficulty_off_emoji * 4, - "Trivial": config.difficulty_on_emoji + config.difficulty_off_emoji * 3, - "Easy": config.difficulty_on_emoji * 2 + config.difficulty_off_emoji * 2, - "Medium": config.difficulty_on_emoji * 3 + config.difficulty_off_emoji, - "Hard": config.difficulty_on_emoji * 4 - } - occurrences = { - "Common": config.occurrence_on_emoji * 1 + config.occurrence_off_emoji * 3, - "Uncommon": config.occurrence_on_emoji * 2 + config.occurrence_off_emoji * 2, - "Rare": config.occurrence_on_emoji * 3 + config.occurrence_off_emoji * 1, - "Very Rare": config.occurrence_on_emoji * 4, - } - kills = { - "Harmless": 25, - "Trivial": 250, - "Easy": 500, - "Medium": 1000, - "Hard": 2500 - } - points = { - "Harmless": 1, - "Trivial": 5, - "Easy": 15, - "Medium": 25, - "Hard": 50 - } bestiary_info = monster.bestiary_class if monster.bestiary_level: - difficulty = difficulties.get(monster.bestiary_level, f"({monster.bestiary_level})") - required_kills = kills[monster.bestiary_level] - given_points = points[monster.bestiary_level] + difficulty = DIFFICULTIES.get(monster.bestiary_level, f"({monster.bestiary_level})") bestiary_info += f"\n{difficulty}" if monster.bestiary_occurrence is not None: - occurrence = occurrences.get(monster.bestiary_occurrence, f"") - if monster.bestiary_occurrence == 'Very Rare': - 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}" + bestiary_info += f"\n{OCCURRENCES.get(monster.bestiary_occurrence, monster.bestiary_occurrence)}" + bestiary_info += f"\n{monster.bestiary_kills:,} kills | {monster.charm_points}{config.charms_emoji}" embed.add_field(name="Bestiary Class", value=bestiary_info) @classmethod - def get_monster_embed_elemental_modifiers(cls, embed, monster): + def get_monster_embed_elemental_modifiers(cls, embed, monster: models.Creature): # Iterate through elemental types - elemental_modifiers = {} - elements = ["physical", "holy", "death", "fire", "ice", "energy", "earth"] - for element in elements: - value = getattr(monster, f"modifier_{element}", None) - if value is None or value == 100: - continue - elemental_modifiers[element] = value - 100 - elemental_modifiers = dict(sorted(elemental_modifiers.items(), key=lambda x: x[1])) - if elemental_modifiers: + if monster: content = "" - for element, value in elemental_modifiers.items(): - if config.use_elemental_emojis: - content += f"\n{config.elemental_emojis[element]} {value:+}%" - else: - content += f"\n{value:+}% {element.title()}" - embed.add_field(name="Elemental modifiers", value=content) + for element, value in monster.elemental_modifiers.items(): + # TODO: Find icon for drown damage + try: + if value is None or value == 100: + continue + value -= 100 + if config.use_elemental_emojis: + content += f"\n{config.elemental_emojis[element]} {value:+}%" + else: + content += f"\n{value:+}% {element.title()}" + except KeyError: + pass + if content: + embed.add_field(name="Elemental modifiers", value=content) @classmethod def get_monster_embed_attributes(cls, embed, monster, ctx): diff --git a/cogs/timers.py b/cogs/timers.py index a03910e..e90790b 100644 --- a/cogs/timers.py +++ b/cogs/timers.py @@ -1,5 +1,6 @@ import asyncio import datetime as dt +import io import logging from enum import Enum from typing import List, Optional @@ -235,27 +236,24 @@ async def check_events(self): log.exception(f"{tag} {e}") async def clean_events(self): - """Cleans up past event notifications""" + """Cleans up past event notifications. + """ tag = f"{self.tag}[clean_events]" try: await self.bot.wait_until_ready() log.debug(f"{tag} Started") async with self.bot.pool.acquire() as conn: - res = await conn.execute("UPDATE event SET reminder = 1 " - "WHERE (start-($1::interval))-($2::interval) < now() AND reminder < 1", - FIRST_NOTIFICATION, TIME_MARGIN) - log.debug(res) - res = await conn.execute("UPDATE event SET reminder = 2 " - "WHERE (start-($1::interval))-($2::interval) < now() AND reminder < 2", - SECOND_NOTIFICATION, TIME_MARGIN) - log.debug(res) - res = await conn.execute("UPDATE event SET reminder = 3 " - "WHERE (start-($1::interval))-($2::interval) < now() AND reminder < 3", - THIRD_NOTIFICATION, TIME_MARGIN) - log.debug(res) - res = await conn.execute("UPDATE event SET reminder = 4 " - "WHERE (start-($1::interval)) < now() AND reminder < 4", TIME_MARGIN) - log.debug(res) + await conn.execute("UPDATE event SET reminder = 1 " + "WHERE (start-($1::interval))-($2::interval) < now() AND reminder < 1", + FIRST_NOTIFICATION, TIME_MARGIN) + await conn.execute("UPDATE event SET reminder = 2 " + "WHERE (start-($1::interval))-($2::interval) < now() AND reminder < 2", + SECOND_NOTIFICATION, TIME_MARGIN) + await conn.execute("UPDATE event SET reminder = 3 " + "WHERE (start-($1::interval))-($2::interval) < now() AND reminder < 3", + THIRD_NOTIFICATION, TIME_MARGIN) + await conn.execute("UPDATE event SET reminder = 4 " + "WHERE (start-($1::interval)) < now() AND reminder < 4", TIME_MARGIN) self.events_announce_task = self.bot.loop.create_task(self.check_events()) except asyncio.CancelledError: pass @@ -320,9 +318,10 @@ async def on_boss_timer_complete(self, timer: 'Timer'): monster = tibiawikisql.models.Creature.get_by_field(wiki_db, "name", timer.name) try: if monster: + thumbnail = io.BytesIO(monster.image) filename = f"thumbnail.gif" embed.set_thumbnail(url=f"attachment://{filename}") - await author.send(file=discord.File(monster.image, f"{filename}"), embed=embed) + await author.send(file=discord.File(thumbnail, f"{filename}"), embed=embed) else: await author.send(embed=embed) except discord.Forbidden: @@ -1227,7 +1226,7 @@ async def event_subscribe(self, ctx, event_id: int): await ctx.send("Ok then.") return - await event.add_subscriber(ctx.pool, ctx.author) + await event.add_subscriber(ctx.pool, ctx.author.id) await ctx.success("You have subscribed successfully to this event. " "I'll let you know when it's happening.") diff --git a/cogs/tracking.py b/cogs/tracking.py index 0e3d792..00c2c38 100644 --- a/cogs/tracking.py +++ b/cogs/tracking.py @@ -564,7 +564,8 @@ async def _watchlist_update_content(self, watchlist: Watchlist, channel: discord await self._watchlist_update_message(self.bot.pool, watchlist, channel, embed) await self._watchlist_update_name(watchlist, channel) except discord.HTTPException: - log.exception(f"{self.tag}[_watchlist_update_content] {watchlist}") + # log.exception(f"{self.tag}[_watchlist_update_content] {watchlist}") + pass @staticmethod async def _watchlist_update_name(watchlist: Watchlist, channel: discord.TextChannel): @@ -585,7 +586,7 @@ async def _watchlist_update_message(conn, watchlist, channel, embed): # 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: - message = await channel.get_message(watchlist.message_id) + message = await channel.fetch_message(watchlist.message_id) except discord.HTTPException: message = None if message is None: diff --git a/cogs/utils/config.py b/cogs/utils/config.py index bddcb49..388a775 100644 --- a/cogs/utils/config.py +++ b/cogs/utils/config.py @@ -126,7 +126,7 @@ def parse(self): print("\tconfig.yml not found, copying from template...") shutil.copyfile(TEMPLATE_PATH, CONFIG_PATH) with open(CONFIG_PATH, "r", encoding="utf-8") as f: - _config = yaml.load(f) + _config = yaml.safe_load(f) if _config is None: _config = {} self._assign_keys(_config) diff --git a/cogs/utils/converter.py b/cogs/utils/converter.py index 72915f6..e7ba204 100644 --- a/cogs/utils/converter.py +++ b/cogs/utils/converter.py @@ -69,10 +69,10 @@ class BadStamina(commands.BadArgument): class TimeString: - def __init__(self, argument): + def __init__(self, argument: str): compiled = re.compile(r"(?:(?P\d+)d)?(?:(?P\d+)h)?(?:(?P\d+)m)?(?:(?P\d+)s)?") - self.original = argument - match = compiled.match(argument) + self.original = argument.strip() + match = compiled.match(self.original) if match is None or not match.group(0): raise BadTime("That's not a valid time, try something like this: `1d7h` or `4h20m`") diff --git a/cogs/utils/pages.py b/cogs/utils/pages.py index 07e5c72..c6d6d64 100644 --- a/cogs/utils/pages.py +++ b/cogs/utils/pages.py @@ -306,6 +306,19 @@ def _command_signature(cmd): return ' '.join(result) +def get_command_signature(command): + parent = command.full_parent_name + if len(command.aliases) > 0: + aliases = '|'.join(command.aliases) + fmt = f'[{command.name}|{aliases}]' + if parent: + fmt = f'{parent} {fmt}' + alias = fmt + else: + alias = command.name if not parent else f'{parent} {command.name}' + return f'{alias} {command.signature}' + + class HelpPaginator(Pages): def __init__(self, ctx, entries, *, per_page=4): super().__init__(ctx, entries=entries, per_page=per_page) @@ -340,7 +353,7 @@ async def from_command(cls, ctx, command): entries = [cmd for cmd in entries if (await _can_run(cmd, ctx)) and not cmd.hidden] self = cls(ctx, entries) - self.title = command.signature + self.title = get_command_signature(command) if command.description: self.description = f'{command.description}\n\n{command.help}' diff --git a/cogs/utils/tibia.py b/cogs/utils/tibia.py index 3f5dd2d..76fef1f 100644 --- a/cogs/utils/tibia.py +++ b/cogs/utils/tibia.py @@ -42,7 +42,7 @@ NO_VOCATION = ["no vocation", "no voc", "novoc", "nv", "n v", "none", "no", "n", "noob", "noobΓ±ie", "rook", "rookie", Vocation.NONE] -invalid_name = re.compile(r"[^\sA-Za-zΓ€-Γ–Γ˜-ΓΆΓΈ-ΓΏ'\-]") +invalid_name = re.compile(r"[^\sA-Za-zΓ€-Γ–Γ˜-ΓΆΓΈ-ΓΏ'\-.]") """Regex used to validate names to avoid doing unnecessary fetches""" boss_pattern = re.compile(r'(?:)?\s*([^<]+)\s*\s*' @@ -358,7 +358,7 @@ async def get_news_article(article_id: int, *, tries=5) -> Optional[Dict[str, Un content = await resp.text(encoding='ISO-8859-1') except (aiohttp.ClientError, asyncio.TimeoutError, tibiapy.TibiapyException): await asyncio.sleep(config.network_retry_delay) - return await get_recent_news(tries=tries - 1) + return await get_news_article(tries=tries - 1) content_json = json.loads(content) try: @@ -376,10 +376,8 @@ async def get_news_article(article_id: int, *, tries=5) -> Optional[Dict[str, Un async def get_recent_news(*, tries=5): if tries == 0: raise errors.NetworkError(f"get_recent_news()") - try: - url = f"https://api.tibiadata.com/v2/latestnews.json" - except UnicodeEncodeError: - return None + + url = f"https://api.tibiadata.com/v2/latestnews.json" # Fetch website try: news = CACHE_NEWS["recent"] @@ -401,10 +399,41 @@ async def get_recent_news(*, tries=5): return None for article in newslist["data"]: article["date"] = parse_tibiadata_time(article["date"]).date() + article["news"] = article["news"].replace("\u00a0", " ") CACHE_NEWS["recent"] = newslist["data"] return newslist["data"] +async def get_recent_news_tickers(*, tries=5): + if tries == 0: + raise errors.NetworkError(f"get_recent_newstickers()") + url = f"https://api.tibiadata.com/v2/newstickers.json" + # Fetch website + try: + news = CACHE_NEWS["recent_tickers"] + return news + except KeyError: + pass + try: + async with aiohttp.ClientSession() as session: + async with session.get(url) as resp: + content = await resp.text() + except (aiohttp.ClientError, asyncio.TimeoutError, tibiapy.TibiapyException): + await asyncio.sleep(config.network_retry_delay) + return await get_recent_news_tickers(tries=tries - 1) + + content_json = json.loads(content) + try: + newslist = content_json["newslist"] + except KeyError: + return None + for article in newslist["data"]: + article["date"] = parse_tibiadata_time(article["date"]).date() + article["news"] = article["news"].replace("\u00a0", " ") + CACHE_NEWS["recent_tickers"] = newslist["data"] + return newslist["data"] + + async def get_world(name, *, tries=5) -> Optional[World]: name = name.strip().title() if tries == 0: diff --git a/data/tibia_worlds.json b/data/tibia_worlds.json index ba03124..c0d801a 100644 --- a/data/tibia_worlds.json +++ b/data/tibia_worlds.json @@ -1 +1 @@ -["Antica", "Assombra", "Astera", "Belobra", "Bona", "Calmera", "Carnera", "Celebra", "Celesta", "Cosera", "Damora", "Descubra", "Duna", "Epoca", "Estela", "Faluna", "Ferobra", "Firmera", "Funera", "Furia", "Garnera", "Gentebra", "Gladera", "Harmonia", "Helera", "Honbra", "Impera", "Inabra", "Jonera", "Kalibra", "Kenora", "Lobera", "Luminera", "Lutabra", "Macabra", "Menera", "Monza", "Nefera", "Noctera", "Nossobra", "Olera", "Ombra", "Pacera", "Peloria", "Premia", "Pyra", "Quelibra", "Quintera", "Refugia", "Relania", "Relembra", "Secura", "Serdebra", "Solidera", "Talera", "Tortura", "Venebra", "Vita", "Vunira", "Wintera", "Zuna", "Zunera"] \ No newline at end of file +["Antica", "Assombra", "Astera", "Belluma", "Belobra", "Bona", "Calmera", "Carnera", "Celebra", "Celesta", "Cosera", "Damora", "Descubra", "Dibra", "Duna", "Epoca", "Estela", "Faluna", "Ferobra", "Firmera", "Funera", "Furia", "Garnera", "Gentebra", "Gladera", "Harmonia", "Helera", "Honbra", "Impera", "Inabra", "Jonera", "Kalibra", "Kenora", "Lobera", "Luminera", "Lutabra", "Macabra", "Menera", "Monza", "Nefera", "Noctera", "Nossobra", "Olera", "Ombra", "Pacera", "Peloria", "Premia", "Pyra", "Quelibra", "Quintera", "Refugia", "Relania", "Relembra", "Secura", "Serdebra", "Solidera", "Talera", "Torpera", "Tortura", "Venebra", "Vita", "Vunira", "Wintera", "Zuna", "Zunera"] \ No newline at end of file diff --git a/data/tibiawiki.db b/data/tibiawiki.db index e942669..b80010f 100644 Binary files a/data/tibiawiki.db and b/data/tibiawiki.db differ diff --git a/docs/assets/images/commands/mod/unregistered_guild.png b/docs/assets/images/commands/mod/unregistered_guild.png new file mode 100644 index 0000000..9e6be91 Binary files /dev/null and b/docs/assets/images/commands/mod/unregistered_guild.png differ diff --git a/docs/assets/images/commands/tibia/news_ticker.png b/docs/assets/images/commands/tibia/news_ticker.png new file mode 100644 index 0000000..a5cc8bf Binary files /dev/null and b/docs/assets/images/commands/tibia/news_ticker.png differ diff --git a/docs/commands/admin.md b/docs/commands/admin.md index 6645932..b9e2e8f 100644 --- a/docs/commands/admin.md +++ b/docs/commands/admin.md @@ -90,12 +90,12 @@ If this is disabled, Announcements won't be made, but there will still be tracki ---- ### settings minlevel -**Syntax:** `settings minlevel [channel]` +**Syntax:** `settings minlevel [level]` **Other aliases:** `settings announcelevel` -Changes the channel where levelup and deaths are announced. +Changes the minimum level from which NabBot starts to announce level ups and deaths. -Level ups and deaths under the minimum level are still and can be seen by checking the character directly. +Level ups and deaths under the minimum level are still tracked and can be seen by checking the character directly. ---- diff --git a/docs/commands/info.md b/docs/commands/info.md index 69ba152..f568f5a 100644 --- a/docs/commands/info.md +++ b/docs/commands/info.md @@ -118,7 +118,7 @@ Check the command's help to see them. ---- ## serverinfo -**Syntax:** `sererinfo [server]` +**Syntax:** `serverinfo [server]` Shows the server's information. @@ -157,4 +157,4 @@ About user statuses: ??? Summary "Examples" **/userinfo** - ![image](../assets/images/commands/info/userinfo.png) \ No newline at end of file + ![image](../assets/images/commands/info/userinfo.png) diff --git a/docs/commands/mod.md b/docs/commands/mod.md index 5507805..0f2b196 100644 --- a/docs/commands/mod.md +++ b/docs/commands/mod.md @@ -75,3 +75,14 @@ Shows a list of users with no registered characters. ![image](../assets/images/commands/mod/unregistered.png) ---- + +### unregistered guild +Shows a list of unregistered guild members. + +Unregistered guild members can be either characters not registered to NabBot or registered to users not in the server. + +??? Summary "Example" + **/unregistered guild Redd Alliance** + ![image](../assets/images/commands/mod/unregistered_guild.png) + +---- diff --git a/docs/commands/tibia.md b/docs/commands/tibia.md index 73d0aa3..ec6b04a 100644 --- a/docs/commands/tibia.md +++ b/docs/commands/tibia.md @@ -213,6 +213,17 @@ If no id is supplied, a list of recent articles is shown, otherwise, a snippet o ---- +### news ticker +**Other aliases:** `news newsticker` + +Shows the latest news tickers from Tibia.com. + +??? Summary "Examples" + **/news ticker** + ![image](../assets/images/commands/tibia/news_ticker.png) + +---- + ## searchworld **Syntax:** `searchworld [,world]` or `searchworld [,world]` or `searchworld ,[,world]` **Other aliases:** `whereworld`, `findworld` @@ -413,4 +424,4 @@ You can pass a list of parameters separated by commas to change the sorting or f **/worlds northamerica,online,descending** ![image](../assets/images/commands/tibia/worlds_2.png) **/worlds southamerica,openpvp,online,descending** - ![image](../assets/images/commands/tibia/worlds_3.png) \ No newline at end of file + ![image](../assets/images/commands/tibia/worlds_3.png) diff --git a/docs/install.md b/docs/install.md index 1cc1609..df27559 100644 --- a/docs/install.md +++ b/docs/install.md @@ -5,8 +5,8 @@ If you just invited NabBot to your server, you don't need to read this. ## Installing requirements -In order to run NabBot, you need to install three things: -[git](https://git-scm.com/), [Python 3.6+](https://www.python.org/) and [PostgreSQL 10+](https://www.postgresql.org/) +In order to run NabBot, you need to install two things: +[Python 3.6+](https://www.python.org/) and [PostgreSQL 10+](https://www.postgresql.org/) When installing on Windows, make sure that you select the option to add Python to `PATH`. @@ -106,4 +106,4 @@ The bot will give longer responses here and it will delete any message that is n Additionally, you can create a channel named **#server-log**. Whenever a user registers characters, they will be shown here, along with their levels and guilds. Other changes are shown here, such as users leaving, getting banned, changing display names and more. -Further customization can be done on a per-server basis by using the command [settings](commands/admin.md#settings). \ No newline at end of file +Further customization can be done on a per-server basis by using the command [settings](commands/admin.md#settings). diff --git a/launcher.py b/launcher.py index 1a6ead4..b72efae 100644 --- a/launcher.py +++ b/launcher.py @@ -53,7 +53,7 @@ def get_uri(): return uri else: with open(file_name) as f: - return f.read() + return f.read().strip() except KeyboardInterrupt: exit() diff --git a/mkdocs.yml b/mkdocs.yml index c0f3ca6..d19099c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -42,8 +42,8 @@ extra: link: 'https://github.com/NabDev' - type: 'gitlab' link: 'https://gitlab.com/NabBot' - - type: 'discord' - link: 'https://discord.me/NabBot' + - type: 'globe' + link: 'https://nabbot.xyz' repo_url: https://github.com/NabDev/NabBot edit_uri: "" diff --git a/nabbot.py b/nabbot.py index a372492..46d2d09 100644 --- a/nabbot.py +++ b/nabbot.py @@ -49,9 +49,9 @@ async def _prefix_callable(bot, msg): return base -class NabBot(commands.Bot): +class NabBot(commands.AutoShardedBot): def __init__(self): - super().__init__(command_prefix=_prefix_callable, case_insensitive=True, + super().__init__(command_prefix=_prefix_callable, case_insensitive=True, fetch_offline_members=True, description="Discord bot with functions for the MMORPG Tibia.") # Remove default help command to implement custom one self.remove_command("help") @@ -67,8 +67,7 @@ def __init__(self): self.tracked_worlds = {} self.tracked_worlds_list = [] - self.__version__ = "2.2.0" - self.__min_discord__ = 1700 + self.__version__ = "2.3.0" async def on_ready(self): """Called when the bot is ready.""" @@ -78,6 +77,7 @@ async def on_ready(self): print(f"Version {self.__version__}") print('------') # Populating members's guild list + self.users_servers.clear() for guild in self.guilds: for member in guild.members: self.users_servers[member.id].append(guild.id) @@ -338,7 +338,7 @@ def get_token(): return token else: with open("token.txt") as f: - return f.read() + return f.read().strip() if __name__ == "__main__": diff --git a/requirements.txt b/requirements.txt index 603e844..835e3c1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,12 @@ -git+https://github.com/Rapptz/discord.py@rewrite aiohttp>=3.3.0,<3.4.0 asyncpg>=0.17.0,<1.0 beautifulsoup4>=4.6.0,<5.0 cachetools>=2.0.1,<4.0 click>=7.0,<8.0 +discord.py>=1.0,<2.0 pillow>=4.1,<5.6 psutil>=5.2,<6.0 PyYAML>=3.12,<4.0 pytz>=2018.5,<3000.0 -tibiawikisql>=2.0,<3.0 -tibia.py>=1.1.3,<2.0 \ No newline at end of file +tibiawikisql>=2.1.1,<3.0 +tibia.py>=1.1.3,<2.0