def check_ghosts(): yield from bot.wait_until_ready() while not bot.is_closed: for server in bot.servers: for member in server.members: c = userDatabase.cursor() now = datetime.utcnow() try: c.execute("SELECT * FROM user_servers WHERE id = ? AND server = ?", (member.id, member.server.id,)) result = c.fetchone() if result is not None: last_message = datetime.strptime(result["last_message"], "%Y-%m-%d %H:%M:%S.%f") name = result["name"] diff = (now - last_message).total_seconds() / 60.0 if (diff/(24*60) > 7.0): #yield from bot.send_message(member.server, "Simulando remoção de usuários: Usuário {0} será removido pois ficou {1} dia(s) sem escrever nada.".format(name, int(diff/(24*60)))) #yield from bot.send_message(member, "Você foi kickado do servidor {0} por ficar {1} dia(s) sem escrever nada. Saí daqui seu ghost!".format(member.server.name, int(diff/(24*60)))) for owners in owner_ids: yield from bot.send_message(member.server.get_member(owners), "O usuário {0} foi kickado (simulação apenas) do servidor {1} por ficar {2} dia(s) sem escrever nada.".format(member.name, member.server.name, int(diff/(24*60)))) #yield from bot.kick(member) else: # Não há registro deste usuário escrever algo mas ele está no servidor c.execute("""INSERT INTO user_servers (id, server, last_message, name) values (?, ?, ?, ?);""", (member.id, member.server.id, now, member.name)) finally: c.close() userDatabase.commit() yield from asyncio.sleep(60*60) # Verifica a cada uma hora (60 minutos)
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 with closing(userDatabase.cursor()) as c: c.execute( "SELECT user_id FROM chars WHERE world LIKE ? GROUP BY user_id", (ctx.world, )) result = c.fetchall() if len(result) <= 0: await ctx.send("There are no unregistered users.") return users = [i["user_id"] for i in result] for member in ctx.guild.members: # type: discord.Member # Skip bots if member.bot: continue if member.id not in users: entries.append( f"@**{member.display_name}** \u2014 Joined: **{member.joined_at.date()}**" ) if len(entries) == 0: await ctx.send("There are no unregistered users.") return pages = Pages(ctx, entries=entries, per_page=10) pages.embed.title = "Unregistered members" try: await pages.paginate() except CannotPaginate as e: await ctx.send(e)
async def on_member_join(self, member: discord.Member): """ Called when a member joins a guild (server) the bot is in.""" log.info("{0.display_name} (ID: {0.id}) joined {0.guild.name}".format( member)) # Updating member list if member.id in self.members: self.members[member.id].append(member.guild.id) else: self.members[member.id] = [member.guild.id] # No welcome message for lite servers and servers not tracking worlds if member.guild.id in config.lite_servers or tracked_worlds.get( member.guild.id) is None: return server_welcome = get_server_property("welcome", member.guild.id, "") pm = (config.welcome_pm + "\n" + server_welcome).format( user=member, server=member.guild, bot=self.user, owner=member.guild.owner) embed = discord.Embed(description="{0.mention} joined.".format(member)) icon_url = get_user_avatar(member) embed.colour = discord.Colour.green() embed.set_author(name="{0.name}#{0.discriminator}".format(member), icon_url=icon_url) embed.timestamp = dt.datetime.utcnow() # Check if user already has characters registered and announce them on log_channel # This could be because he rejoined the server or is in another server tracking the same worlds world = tracked_worlds.get(member.guild.id) if world is not None: c = userDatabase.cursor() try: c.execute( "SELECT name, vocation, ABS(level) as level, guild " "FROM chars WHERE user_id = ? and world = ?", ( member.id, world, )) results = c.fetchall() if len(results) > 0: pm += "\nYou already have these characters in {0} registered to you: {1}"\ .format(world, join_list([r["name"] for r in results], ", ", " and ")) characters = [ "\u2023 {name} - Level {level} {voc} - **{guild}**". format(**c, voc=get_voc_abb_and_emoji(c["vocation"])) for c in results ] embed.add_field(name="Registered characters", value="\n".join(characters)) finally: c.close() await self.send_log_message(member.guild, embed=embed) await member.send(pm)
def check(self, ctx): """Check which users are currently not registered.""" if not ctx.message.channel.is_private: return True author = ctx.message.author if author.id in mod_ids + owner_ids: author_servers = get_user_servers(self.bot, author.id) else: author_servers = get_user_admin_servers(self.bot, author.id) embed = discord.Embed(description="Members with unregistered users.") yield from self.bot.send_typing(ctx.message.channel) c = userDatabase.cursor() try: for server in author_servers: world = tracked_worlds.get(server.id, None) if world is None: continue c.execute( "SELECT user_id FROM chars WHERE world LIKE ? GROUP BY user_id", (world, )) result = c.fetchall() if len(result) <= 0: embed.add_field( name=server.name, value="There are no registered characters.", inline=False) continue users = [str(i["user_id"]) for i in result] empty_members = list() for member in server.members: if member.id == self.bot.user.id: continue if member.id not in users: empty_members.append("**@" + member.display_name + "**") if len(empty_members) == 0: embed.add_field(name=server.name, value="There are no unregistered users.", inline=False) continue field_value = "\n{0}".format("\n".join(empty_members)) split_value = split_message(field_value, FIELD_VALUE_LIMIT) for empty_member in split_value: if empty_member == split_value[0]: name = server.name else: name = "\u200F" embed.add_field(name=name, value=empty_member, inline=False) yield from self.bot.say(embed=embed) finally: c.close()
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)
def update_ghost(member): c = userDatabase.cursor() now = datetime.utcnow() try: c.execute("SELECT * FROM user_servers WHERE id = ? AND server = ?", (member.id, member.server.id,)) result = c.fetchone() if result is not None: c.execute("""UPDATE user_servers SET last_message = ?, name = ? WHERE id = ? AND server = ?;""", (now, member.id, member.server.id, member.name)) else: c.execute("""INSERT INTO user_servers (id, server, last_message, name) values (?, ?, ?, ?);""", (member.id, member.server.id, now, member.name)) finally: c.close() userDatabase.commit()
def remove_char(self, ctx, *, name): """Removes a registered character. The syntax is: /stalk removechar name""" if not ctx.message.channel.is_private: return True # This could be used to remove deleted chars so we don't need to check anything # Except if the char exists in the database... yield from self.bot.send_typing(ctx.message.channel) c = userDatabase.cursor() try: c.execute( "SELECT name, user_id, world, ABS(last_level) as level, vocation " "FROM chars WHERE name LIKE ?", (name, )) result = c.fetchone() if result is None: yield from self.bot.say( "There's no character with that name registered.") return user = get_member(self.bot, result["user_id"]) username = "******" if user is None else user.display_name c.execute("DELETE FROM chars WHERE name LIKE ?", (name, )) yield from self.bot.say( "**{0}** was removed successfully from **@{1}**.".format( result["name"], username)) if user is not None: for server in get_user_servers(self.bot, user.id): world = tracked_worlds.get(server.id, None) if world != result["world"]: continue log_msg = "{0.mention} removed **{1}** ({2} {3}) from {4.mention}.".\ format(ctx.message.author, result["name"], result["level"], result["vocation"], user) yield from send_log_message(self.bot, server, log_msg) return finally: c.close() userDatabase.commit()
def refresh_names(self, ctx): """Checks and updates user names on the database.""" if not ctx.message.channel.is_private: return True c = userDatabase.cursor() try: c.execute("SELECT id FROM users") result = c.fetchall() if len(result) <= 0: yield from self.bot.say("There are no registered users.") return update_users = list() for user in result: update_users.append( ("unknown" if get_member(self.bot, user[0]) is None else get_member(self.bot, user[0]).display_name, user["id"])) c.executemany("UPDATE users SET name = ? WHERE id LIKE ?", update_users) yield from self.bot.say("Usernames updated successfully.") finally: c.close() userDatabase.commit()
def reload_ignored(self): """Refresh the world list from the database This is used to avoid reading the database everytime the world list is needed. A global variable holding the world list is loaded on startup and refreshed only when worlds are modified""" c = userDatabase.cursor() ignored_dict_temp = {} # type: Dict[int, List[int]] try: c.execute("SELECT server_id, channel_id FROM ignored_channels") result = c.fetchall() # type: Dict if len(result) > 0: for row in result: if not ignored_dict_temp.get(row["server_id"]): ignored_dict_temp[row["server_id"]] = [] ignored_dict_temp[row["server_id"]].append( row["channel_id"]) self.ignored.clear() self.ignored.update(ignored_dict_temp) finally: c.close()
async def unregistered(self, ctx): """Check which users are currently not registered.""" world = tracked_worlds.get(ctx.guild.id, None) entries = [] if world is None: await ctx.send("This server is not tracking any worlds.") return with closing(userDatabase.cursor()) as c: c.execute( "SELECT user_id FROM chars WHERE world LIKE ? GROUP BY user_id", (world, )) result = c.fetchall() if len(result) <= 0: await ctx.send("There are no registered characters.") return users = [i["user_id"] for i in result] for member in ctx.guild.members: # type: discord.Member if member.id == ctx.me.id: continue if member.id not in users: entries.append( f"@**{member.display_name}** \u2014 Joined on: **{member.joined_at.date()}**" ) if len(entries) == 0: await ctx.send("There are no unregistered users.") return pages = Paginator(self.bot, message=ctx.message, entries=entries, title="Unregistered members", per_page=10) try: await pages.paginate() except CannotPaginate as e: await ctx.send(e)
def reload_worlds(self): """Refresh the world list from the database This is used to avoid reading the database every time the world list is needed. A global variable holding the world list is loaded on startup and refreshed only when worlds are modified""" c = userDatabase.cursor() tibia_servers_dict_temp = {} try: c.execute( "SELECT server_id, value FROM server_properties WHERE name = 'world' ORDER BY value ASC" ) result: Dict = c.fetchall() del self.tracked_worlds_list[:] if len(result) > 0: for row in result: if row["value"] not in self.tracked_worlds_list: self.tracked_worlds_list.append(row["value"]) tibia_servers_dict_temp[int( row["server_id"])] = row["value"] self.tracked_worlds.clear() self.tracked_worlds.update(tibia_servers_dict_temp) finally: c.close()
def stalk_namelock(self, ctx, *, params): """Register the name of a new character that was namelocked. Characters that get namelocked can't be searched by their old name, so they must be reassigned manually. If the character got a name change (from the store), searching the old name redirects to the new name, so this are usually reassigned automatically. The syntax is: /stalk namelock oldname,newname""" if not ctx.message.channel.is_private: return True params = params.split(",") if len(params) != 2: yield from self.bot.say( "The correct syntax is: `/stalk namelock oldname,newname") return old_name = params[0] new_name = params[1] yield from self.bot.send_typing(ctx.message.channel) c = userDatabase.cursor() try: c.execute("SELECT * FROM chars WHERE name LIKE ? LIMIT 1", (old_name, )) old_char_db = c.fetchone() # If character wasn't registered, there's nothing to do. if old_char_db is None: yield from self.bot.say( "I don't have a character registered with the name: **{0}**" .format(old_name)) return # Search old name to see if there's a result old_char = yield from get_character(old_name) if old_char == ERROR_NETWORK: yield from self.bot.say( "I'm having problem with 'the internet' as you humans say, try again." ) return # Check if returns a result if type(old_char) is dict: if old_name.lower() == old_char["name"].lower(): yield from self.bot.say( "The character **{0}** wasn't namelocked.".format( old_char["name"])) else: yield from self.bot.say( "The character **{0}** was renamed to **{1}**.".format( old_name, old_char["name"])) # Renaming is actually done in get_character(), no need to do anything. return # Check if new name exists new_char = yield from get_character(new_name) if new_char == ERROR_NETWORK: yield from self.bot.say( "I'm having problem with 'the internet' as you humans say, try again." ) return if new_char == ERROR_DOESNTEXIST: yield from self.bot.say( "The character **{0}** doesn't exists.".format(new_name)) return # Check if vocations are similar if not (old_char_db["vocation"].lower() in new_char["vocation"].lower() or new_char["vocation"].lower() in old_char_db["vocation"].lower()): yield from self.bot.say( "**{0}** was a *{1}* and **{2}** is a *{3}*. I think you're making a mistake." .format(old_char_db["name"], old_char_db["vocation"], new_char["name"], new_char["vocation"])) return confirm_message = "Are you sure **{0}** ({1} {2}) is **{3}** ({4} {5}) now? `yes/no`" yield from self.bot.say( confirm_message.format(old_char_db["name"], abs(old_char_db["last_level"]), old_char_db["vocation"], new_char["name"], new_char["level"], new_char["vocation"])) reply = yield from self.bot.wait_for_message( author=ctx.message.author, channel=ctx.message.channel, timeout=50.0) if reply is None: yield from self.bot.say( "No answer? I guess you changed your mind.") return elif reply.content.lower() not in ["yes", "y"]: yield from self.bot.say("No then? Alright.") return # Check if new name was already registered c.execute("SELECT * FROM chars WHERE name LIKE ?", (new_char["name"], )) new_char_db = c.fetchone() if new_char_db is None: c.execute( "UPDATE chars SET name = ?, vocation = ?, last_level = ? WHERE id = ?", ( new_char["name"], new_char["vocation"], new_char["level"], old_char_db["id"], )) else: # Replace new char with old char id and delete old char, reassign deaths and levelups c.execute( "DELETE FROM chars WHERE id = ?", (old_char_db["id"]), ) c.execute("UPDATE chars SET id = ? WHERE id = ?", ( old_char_db["id"], new_char_db["id"], )) c.execute("UPDATE char_deaths SET id = ? WHERE id = ?", ( old_char_db["id"], new_char_db["id"], )) c.execute("UPDATE char_levelups SET id = ? WHERE id = ?", ( old_char_db["id"], new_char_db["id"], )) yield from self.bot.say("Character renamed successfully.") finally: c.close() userDatabase.commit()
async def on_character_change(self, user_id: int): try: with closing(userDatabase.cursor()) as c: guilds_raw = c.execute( "SELECT guild FROM chars WHERE user_id = ?", (user_id, )).fetchall() rules_raw = c.execute( "SELECT * FROM auto_roles ORDER BY server_id").fetchall() # Flatten list of guilds guilds = set(g['guild'] for g in guilds_raw) # Flatten rules rules = {} for rule in rules_raw: server_id = rule["server_id"] if server_id not in rules: rules[rule["server_id"]] = [] rules[server_id].append((rule["role_id"], rule["guild"])) for server, rules in rules.items(): guild: discord.Guild = self.bot.get_guild(server) if guild is None: continue member: discord.Member = guild.get_member(user_id) if member is None: continue all_roles = set() to_add = set() for role_id, tibia_guild in rules: role: discord.Role = discord.utils.get(guild.roles, id=role_id) if role is None: continue all_roles.add(role) if (tibia_guild == "*" and guilds) or tibia_guild in guilds: to_add.add(role) to_remove = all_roles - to_add try: before_roles = set(member.roles) await member.remove_roles(*to_remove, reason="Automatic roles") await member.add_roles(*to_add, reason="Automatic roles") # A small delay is needed so member.roles is updated with the added possible added roles. await asyncio.sleep(0.15) after_roles = set(member.roles) new_roles = after_roles - before_roles removed_roles = before_roles - after_roles if new_roles or removed_roles: embed = discord.Embed( colour=discord.Colour.dark_blue(), title="Autorole changes") embed.set_author( name="{0.name}#{0.discriminator} (ID: {0.id})". format(member), icon_url=get_user_avatar(member)) if new_roles: embed.add_field(name="Added roles", value=", ".join( r.mention for r in new_roles)) if removed_roles: embed.add_field(name="Removed roles", value=", ".join( r.mention for r in removed_roles)) await self.bot.send_log_message(guild, embed=embed) except discord.HTTPException: pass except Exception: log.exception("Event: character_change")
async def on_member_join(self, member: discord.Member): """ Called when a member joins a guild (server) the bot is in.""" log.info("{0.display_name} (ID: {0.id}) joined {0.guild.name}".format( member)) # Updating member list if member.id in self.members: self.members[member.id].append(member.guild.id) else: self.members[member.id] = [member.guild.id] embed = discord.Embed(description="{0.mention} joined.".format(member), color=discord.Color.green()) embed.set_author( name="{0.name}#{0.discriminator} (ID: {0.id})".format(member), icon_url=get_user_avatar(member)) previously_registered = "" # If server is not tracking worlds, we don't check the database if member.guild.id in config.lite_servers or self.tracked_worlds.get( member.guild.id) is None: await self.send_log_message(member.guild, embed=embed) else: # Check if user already has characters registered and announce them on log_channel # This could be because he rejoined the server or is in another server tracking the same worlds world = self.tracked_worlds.get(member.guild.id) previously_registered = "" if world is not None: c = userDatabase.cursor() try: c.execute( "SELECT name, vocation, ABS(level) as level, guild " "FROM chars WHERE user_id = ? and world = ?", ( member.id, world, )) results = c.fetchall() if len(results) > 0: previously_registered = "\n\nYou already have these characters in {0} registered to you: *{1}*"\ .format(world, join_list([r["name"] for r in results], ", ", " and ")) characters = [ "\u2023 {name} - Level {level} {voc} - **{guild}**" .format(**c, voc=get_voc_abb_and_emoji(c["vocation"])) for c in results ] embed.add_field(name="Registered characters", value="\n".join(characters)) finally: c.close() self.dispatch("character_change", member.id) await self.send_log_message(member.guild, embed=embed) welcome_message = get_server_property(member.guild.id, "welcome") welcome_channel_id = get_server_property(member.guild.id, "welcome_channel", is_int=True) if welcome_message is None: return message = welcome_message.format(user=member, server=member.guild, bot=self, owner=member.guild.owner) message += previously_registered channel = member.guild.get_channel(welcome_channel_id) # If channel is not found, send via pm as fallback if channel is None: channel = member try: await channel.send(message) except discord.Forbidden: try: # If bot has no permissions to send the message on that channel, send on private message # If the channel was already a private message, don't try it again if welcome_channel_id is None: return await member.send(message) except discord.Forbidden: pass
def get_character(name, tries=5): """Returns a dictionary with a player's info The dictionary contains the following keys: name, deleted, level, vocation, world, residence, married, gender, guild, last,login, chars*. *chars is list that contains other characters in the same account (if not hidden). Each list element is dictionary with the keys: name, world. May return ERROR_DOESNTEXIST or ERROR_NETWORK accordingly.""" try: url = url_character + urllib.parse.quote(name.encode('iso-8859-1')) except UnicodeEncodeError: return ERROR_DOESNTEXIST char = dict() # Fetch website try: page = yield from aiohttp.get(url) content = yield from page.text(encoding='ISO-8859-1') except Exception: if tries == 0: log.error( "getPlayer: Couldn't fetch {0}, network error.".format(name)) return ERROR_NETWORK else: tries -= 1 yield from asyncio.sleep(network_retry_delay) ret = yield from get_character(name, tries) return ret # Trimming content to reduce load try: startIndex = content.index('<div class="BoxContent"') endIndex = content.index("<B>Search Character</B>") content = content[startIndex:endIndex] except ValueError: # Website fetch was incomplete, due to a network error if tries == 0: log.error( "getPlayer: Couldn't fetch {0}, network error.".format(name)) return ERROR_NETWORK else: tries -= 1 yield from asyncio.sleep(network_retry_delay) ret = yield from get_character(name, tries) return ret # Check if player exists if "Name:</td><td>" not in content: return ERROR_DOESNTEXIST # TODO: Is there a way to reduce this part? # Name m = re.search(r'Name:</td><td>([^<,]+)', content) if m: char['name'] = m.group(1).strip() # Deleted m = re.search(r', will be deleted at ([^<]+)', content) if m: char['deleted'] = True # Vocation m = re.search(r'Vocation:</td><td>([^<]+)', content) if m: char['vocation'] = m.group(1) # Level m = re.search(r'Level:</td><td>(\d+)', content) if m: char['level'] = int(m.group(1)) # Use database levels for online characters for onchar in global_online_list: if onchar.split("_", 1)[1] == char['name']: c = userDatabase.cursor() c.execute("SELECT last_level FROM chars WHERE name LIKE ?", (char['name'], )) result = c.fetchone() if result: char['level'] = abs(result["last_level"]) c.close() break # World m = re.search(r'World:</td><td>([^<]+)', content) if m: char['world'] = m.group(1) # Residence (City) m = re.search(r'Residence:</td><td>([^<]+)', content) if m: char['residence'] = m.group(1) # Marriage m = re.search(r'Married To:</td><td>?.+name=([^"]+)', content) if m: char['married'] = urllib.parse.unquote_plus(m.group(1), encoding='ISO-8859-1') # Sex m = re.search(r'Sex:</td><td>([^<]+)', content) if m: if m.group(1) == 'male': char['gender'] = 'male' else: char['gender'] = 'female' # Guild rank m = re.search(r'Membership:</td><td>([^<]+)\sof the', content) if m: char['rank'] = m.group(1) # Guild membership m = re.search(r'GuildName=.*?([^&]+).+', content) if m: char['guild'] = urllib.parse.unquote_plus(m.group(1)) # House m = re.search( r'House:</td><td> <a href=\"https://secure\.tibia\.com/community/\?subtopic=houses.+houseid=(\d+)' r'&character=(?:[^&]+)&action=characters\" >([^<]+)</a> \(([^(]+)\) is paid until ' r'([A-z]+).*?;(\d+).*?;(\d+)', content) if m: char["house_id"] = m.group(1) char["house"] = m.group(2) char["house_town"] = m.group(3) # Last login m = re.search(r'Last Login:</td><td>([^<]+)', content) if m: lastLogin = m.group(1).replace(" ", " ").replace(",", "") if "never" in lastLogin: char['last_login'] = None else: char['last_login'] = lastLogin # Discord owner c = userDatabase.cursor() c.execute("SELECT user_id FROM chars WHERE name LIKE ?", (char["name"], )) result = c.fetchone() char["owner_id"] = None if result is None else result["user_id"] # Update name, vocation and world for chars in database if necessary c = userDatabase.cursor() c.execute("SELECT vocation, name, id, world FROM chars WHERE name LIKE ?", (name, )) result = c.fetchone() if result: if result["vocation"] != char['vocation']: c.execute("UPDATE chars SET vocation = ? WHERE id = ?", ( char['vocation'], result["id"], )) log.info( "{0}'s vocation was set to {1} from {2} during get_character()" .format(char['name'], char['vocation'], result["vocation"])) if result["name"] != char["name"]: c.execute("UPDATE chars SET name = ? WHERE id = ?", ( char['name'], result["id"], )) log.info("{0} was renamed to {1} during get_character()".format( result["name"], char['name'])) if result["world"] != char["world"]: c.execute("UPDATE chars SET world = ? WHERE id = ?", ( char['world'], result["id"], )) log.info( "{0}'s world was set to {1} from {2} during get_character()". format(char['name'], char['world'], result["world"])) #Skills from highscores c = userDatabase.cursor() for category in highscores_categories: c.execute( "SELECT " + category + "," + category + "_rank FROM chars WHERE name LIKE ?", (name, )) result = c.fetchone() if result: if result[category] is not None and result[category + '_rank'] is not None: char[category] = result[category] char[category + '_rank'] = result[category + '_rank'] char["deaths"] = [] regex_deaths = r'valign="top" >([^<]+)</td><td>(.+?)</td></tr>' pattern = re.compile(regex_deaths, re.MULTILINE + re.S) matches = re.findall(pattern, content) for m in matches: death_time = m[0].replace(' ', ' ').replace(",", "") death_level = "" death_killer = "" death_by_player = False if m[1].find("Died") != -1: regex_deathinfo_monster = r'Level (\d+) by ([^.]+)' pattern = re.compile(regex_deathinfo_monster, re.MULTILINE + re.S) m_deathinfo_monster = re.search(pattern, m[1]) if m_deathinfo_monster: death_level = m_deathinfo_monster.group(1) death_killer = m_deathinfo_monster.group(2) else: regex_deathinfo_player = r'Level (\d+) by .+?name=([^"]+)' pattern = re.compile(regex_deathinfo_player, re.MULTILINE + re.S) m_deathinfo_player = re.search(pattern, m[1]) if m_deathinfo_player: death_level = m_deathinfo_player.group(1) death_killer = urllib.parse.unquote_plus( m_deathinfo_player.group(2)) death_by_player = True try: char["deaths"].append({ 'time': death_time, 'level': int(death_level), 'killer': death_killer, 'byPlayer': death_by_player }) except ValueError: # Some pvp deaths have no level, so they are raising a ValueError, they will be ignored for now. continue # Other chars # note that an empty char list means the character is hidden # otherwise you'd have at least the same char in the list char['chars'] = [] try: # See if there is a character list startIndex = content.index("<B>Characters</B>") content = content[startIndex:] # Find characters regex_chars = r'<TD WIDTH=10%><NOBR>([^<]+)[^?]+.+?VALUE=\"([^\"]+)' pattern = re.compile(regex_chars, re.MULTILINE + re.S) m = re.findall(pattern, content) if m: for (world, name) in m: name = urllib.parse.unquote_plus(name) char['chars'].append({'name': name, 'world': world}) except Exception: pass return char
async def get_character(name, tries=5) -> Optional[Character]: """Fetches a character from TibiaData, parses and returns a Character object The character object contains all the information available on Tibia.com Information from the user's database is also added, like owner and highscores. If the character can't be fetch due to a network error, an NetworkError exception is raised If the character doesn 't exist, None is returned. """ if tries == 0: log.error( "get_character: Couldn't fetch {0}, network error.".format(name)) raise NetworkError() try: url = f"https://api.tibiadata.com/v2/characters/{urllib.parse.quote(name, safe='')}.json" except UnicodeEncodeError: return None # Fetch website try: async with aiohttp.ClientSession() as session: async with session.get(url) as resp: content = await resp.text(encoding='ISO-8859-1') except Exception: await asyncio.sleep(config.network_retry_delay) return await get_character(name, tries - 1) content_json = json.loads(content) character = Character.parse_from_tibiadata(content_json) if character is None: return None if character.house is not None: with closing(tibiaDatabase.cursor()) as c: c.execute("SELECT id FROM houses WHERE name LIKE ?", (character.house["name"].strip(), )) result = c.fetchone() if result: character.house["houseid"] = result["id"] # Database operations c = userDatabase.cursor() # Skills from highscores c.execute("SELECT category, rank, value FROM highscores WHERE name LIKE ?", (character.name, )) results = c.fetchall() if len(results) > 0: character.highscores = results # Discord owner c.execute( "SELECT user_id, vocation, name, id, world, guild FROM chars WHERE name LIKE ?", (name, )) result = c.fetchone() if result is None: # Untracked character return character character.owner = result["user_id"] if result["vocation"] != character.vocation: with userDatabase as conn: conn.execute("UPDATE chars SET vocation = ? WHERE id = ?", ( character.vocation, result["id"], )) log.info( "{0}'s vocation was set to {1} from {2} during get_character()" .format(character.name, character.vocation, result["vocation"])) if result["name"] != character.name: with userDatabase as conn: conn.execute("UPDATE chars SET name = ? WHERE id = ?", ( character.name, result["id"], )) log.info("{0} was renamed to {1} during get_character()".format( result["name"], character.name)) if result["world"] != character.world: with userDatabase as conn: conn.execute("UPDATE chars SET world = ? WHERE id = ?", ( character.world, result["id"], )) log.info( "{0}'s world was set to {1} from {2} during get_character()". format(character.name, character.world, result["world"])) if character.guild is not None and result["guild"] != character.guild[ "name"]: with userDatabase as conn: conn.execute("UPDATE chars SET guild = ? WHERE id = ?", ( character.guild["name"], result["id"], )) log.info( "{0}'s guild was set to {1} from {2} during get_character()". format(character.name, character.guild["name"], result["guild"])) return character
async def get_character(name, tries=5, *, bot: commands.Bot = None) -> Optional[Character]: """Fetches a character from TibiaData, parses and returns a Character object The character object contains all the information available on Tibia.com Information from the user's database is also added, like owner and highscores. If the character can't be fetch due to a network error, an NetworkError exception is raised If the character doesn 't exist, None is returned. """ if tries == 0: log.error( "get_character: Couldn't fetch {0}, network error.".format(name)) raise NetworkError() try: url = f"https://api.tibiadata.com/v2/characters/{urllib.parse.quote(name.strip(), safe='')}.json" except UnicodeEncodeError: return None # Fetch website try: character = CACHE_CHARACTERS[name.lower()] except KeyError: req_log.info(f"get_character({name})") try: async with aiohttp.ClientSession() as session: async with session.get(url) as resp: content = await resp.text(encoding='ISO-8859-1') except Exception: await asyncio.sleep(config.network_retry_delay) return await get_character(name, tries - 1) content_json = json.loads(content) character = Character.parse_from_tibiadata(content_json) CACHE_CHARACTERS[name.lower()] = character if character is None: return None if character.house is not None: with closing(tibiaDatabase.cursor()) as c: c.execute("SELECT id FROM houses WHERE name LIKE ?", (character.house["name"].strip(), )) result = c.fetchone() if result: character.house["houseid"] = result["id"] # If the character exists in the online list use data from there where possible for c in global_online_list: if c == character: character.level = c.level character.vocation = c.vocation break # Database operations c = userDatabase.cursor() # Skills from highscores c.execute("SELECT category, rank, value FROM highscores WHERE name LIKE ?", (character.name, )) results = c.fetchall() if len(results) > 0: character.highscores = results # Check if this user was recently renamed, and update old reference to this for old_name in character.former_names: c.execute("SELECT id FROM chars WHERE name LIKE ? LIMIT 1", (old_name, )) result = c.fetchone() if result: with userDatabase as conn: conn.execute("UPDATE chars SET name = ? WHERE id = ?", (character.name, result["id"])) log.info( "{0} was renamed to {1} during get_character()".format( old_name, character.name)) # Discord owner c.execute( "SELECT user_id, vocation, name, id, world, guild FROM chars WHERE name LIKE ? OR name LIKE ?", (name, character.name)) result = c.fetchone() if result is None: # Untracked character return character character.owner = result["user_id"] if result["vocation"] != character.vocation: with userDatabase as conn: conn.execute("UPDATE chars SET vocation = ? WHERE id = ?", ( character.vocation, result["id"], )) log.info( "{0}'s vocation was set to {1} from {2} during get_character()" .format(character.name, character.vocation, result["vocation"])) # This condition PROBABLY can't be met again if result["name"] != character.name: with userDatabase as conn: conn.execute("UPDATE chars SET name = ? WHERE id = ?", ( character.name, result["id"], )) log.info("{0} was renamed to {1} during get_character()".format( result["name"], character.name)) if result["world"] != character.world: with userDatabase as conn: conn.execute("UPDATE chars SET world = ? WHERE id = ?", ( character.world, result["id"], )) log.info( "{0}'s world was set to {1} from {2} during get_character()". format(character.name, character.world, result["world"])) if character.guild is not None and result["guild"] != character.guild[ "name"]: with userDatabase as conn: conn.execute("UPDATE chars SET guild = ? WHERE id = ?", ( character.guild["name"], result["id"], )) log.info( "{0}'s guild was set to {1} from {2} during get_character()". format(character.name, character.guild["name"], result["guild"])) if bot is not None: bot.dispatch("character_change", character.owner) if character.guild is None and result["guild"] is not None: with userDatabase as conn: conn.execute("UPDATE chars SET guild = ? WHERE id = ?", ( None, result["id"], )) log.info( "{0}'s guild was set to {1} from {2} during get_character()". format(character.name, None, result["guild"])) if bot is not None: bot.dispatch("character_change", character.owner) return character
def add_char(self, ctx, *, params): """Registers a tibia character to a discord user. The syntax is: /stalk addchar user,character""" if not ctx.message.channel.is_private: return True params = params.split(",") if len(params) != 2: yield from self.bot.say( "The correct syntax is: ``/stalk addchar username,character``") return author = ctx.message.author if author.id in mod_ids + owner_ids: author_servers = get_user_servers(self.bot, author.id) else: author_servers = get_user_admin_servers(self.bot, author.id) author_worlds = get_user_worlds(self.bot, author.id) # Only search in the servers the command author is user = get_member_by_name(self.bot, params[0], server_list=author_servers) user_servers = get_user_servers(self.bot, user.id) user_worlds = get_user_worlds(self.bot, author.id) common_worlds = list(set(author_worlds) & set(user_worlds)) yield from self.bot.send_typing(ctx.message.channel) char = yield from get_character(params[1]) if user is None: yield from self.bot.say( "I don't see any user named **{0}** in the servers you manage." .format(params[0])) return if type(char) is not dict: if char == ERROR_NETWORK: yield from self.bot.say( "I couldn't fetch the character, please try again.") elif char == ERROR_DOESNTEXIST: yield from self.bot.say("That character doesn't exists.") return if char["world"] not in common_worlds: yield from self.bot.say( "**{name}** ({world}) is not in a world you can manage.". format(**char)) return if char.get("deleted", False): yield from self.bot.say( "**{name}** ({world}) is scheduled for deletion and can't be added." .format(**char)) return c = userDatabase.cursor() try: c.execute("SELECT id, name, user_id FROM chars WHERE name LIKE ?", (char['name'], )) result = c.fetchone() # Char is already in database if result is not None: # Update name if it was changed if char['name'] != params[1]: c.execute("UPDATE chars SET name = ? WHERE id = ?", ( char['name'], result["id"], )) yield from self.bot.say( "This character's name was changed from **{0}** to **{1}**" .format(params[1], char['name'])) # Registered to a different user if result["user_id"] != user.id: current_user = get_member(self.bot, result["user_id"]) # User no longer in server if current_user is None: c.execute("UPDATE chars SET user_id = ? WHERE id = ?", ( user.id, result["id"], )) yield from self.bot.say( "This character was registered to a user no longer in server. " "It was assigned to this user successfully.") # Log on relevant servers for server in user_servers: world = tracked_worlds.get(server.id, None) if world == char["world"]: log_msg = "{0.mention} registered **{1}** ({2} {3}) to {4.mention}." yield from send_log_message( self.bot, server, log_msg.format(author, char["name"], char["level"], char["vocation"], user)) else: yield from self.bot.say( "This character is already registered to **@{0}**". format(current_user.display_name)) return # Registered to current user yield from self.bot.say( "This character is already registered to this user.") return c.execute( "INSERT INTO chars (name,last_level,vocation,user_id, world) VALUES (?,?,?,?,?)", (char["name"], char["level"] * -1, char["vocation"], user.id, char["world"])) # Check if user is already registered c.execute("SELECT id from users WHERE id = ?", (user.id, )) result = c.fetchone() if result is None: c.execute("INSERT INTO users(id,name) VALUES (?,?)", ( user.id, user.display_name, )) yield from self.bot.say( "**{0}** was registered successfully to this user.".format( char['name'])) # Log on relevant servers for server in user_servers: world = tracked_worlds.get(server.id, None) if world == char["world"]: char["guild"] = char.get("guild", "No guild") log_msg = "{0.mention} registered **{1}** ({2} {3}, {4}) to {5.mention}." yield from send_log_message( self.bot, server, log_msg.format(author, char["name"], char["level"], char["vocation"], char["guild"], user)) return finally: c.close() userDatabase.commit()
def add_account(self, ctx, *, params): """Register a character and all other visible characters to a discord user. If a character is hidden, only that character will be added. Characters in other worlds are skipped. The syntax is the following: /stalk addacc user,char""" if not ctx.message.channel.is_private: return True params = params.split(",") if len(params) != 2: yield from self.bot.say( "The correct syntax is: ``/stalk addacc username,character``") return author = ctx.message.author if author.id in mod_ids + owner_ids: author_servers = get_user_servers(self.bot, author.id) else: author_servers = get_user_admin_servers(self.bot, author.id) author_worlds = get_user_worlds(self.bot, author.id) user = get_member_by_name(self.bot, params[0], server_list=author_servers) user_servers = get_user_servers(self.bot, user.id) user_worlds = get_user_worlds(self.bot, user.id) common_worlds = list(set(author_worlds) & set(user_worlds)) yield from self.bot.send_typing(ctx.message.channel) character = yield from get_character(params[1]) if user is None: yield from self.bot.say( "I don't see any user named **{0}** in the servers you manage." .format(params[0])) return if type(character) is not dict: if character == ERROR_NETWORK: yield from self.bot.say( "I couldn't fetch the character, please try again.") elif character == ERROR_DOESNTEXIST: yield from self.bot.say("That character doesn't exists.") return c = userDatabase.cursor() try: chars = character['chars'] # If the char is hidden,we still add the searched character if len(chars) == 0: yield from self.bot.say("Character is hidden.") chars = [character] skipped = list() added = list() added_tuples = list() reassigned_tuples = list() existent = list() error = list() for char in chars: # Character not in followed server(s), skip. if char['world'] not in common_worlds: skipped.append([char["name"], char["world"]]) continue name = char["name"] # If char is the same we already looked up, no need to look him up again if character["name"] == char["name"]: char = character else: char = yield from get_character(char["name"]) if type(char) is not dict: error.append(name) continue # Skip characters scheduled for deletion if char.get("deleted", False): skipped.append([char["name"], char["world"]]) continue c.execute( "SELECT id, name,user_id FROM chars WHERE name LIKE ?", (char['name'], )) result = c.fetchone() # Char is already in database if result is not None: # Registered to different user if str(result["user_id"]) != user.id: current_user = get_member(self.bot, result["user_id"]) # Char is registered to user no longer in server if current_user is None: added.append(char) reassigned_tuples.append(( user.id, result["id"], )) continue else: yield from self.bot.say( "{0} is already assigned to {1}. We can't add any other of these " "characters.".format( char["name"], current_user.display_name)) return # Registered to current user existent.append(char) continue added.append(char) added_tuples.append(( char["name"], char["level"] * -1, char["vocation"], user.id, char["world"], )) c.execute("SELECT id from users WHERE id = ?", (user.id, )) result = c.fetchone() if result is None: c.execute("INSERT INTO users(id,name) VALUES (?,?)", ( user.id, user.display_name, )) c.executemany( "INSERT INTO chars(name,last_level,vocation,user_id, world) VALUES (?,?,?,?,?)", added_tuples) c.executemany("UPDATE chars SET user_id = ? WHERE id = ?", reassigned_tuples) reply = "" log_reply = dict().fromkeys([server.id for server in user_servers], "") if added: reply += "\nThe following characters were registered or reassigned successfully:" for char in added: char["guild"] = char.get("guild", "No guild") reply += "\n\t**{name}** ({level} {vocation}) - **{guild}**".format( **char) # Announce on server log of each server for server in user_servers: # Only announce on worlds where the character's world is tracked if tracked_worlds.get(server.id, None) == char["world"]: log_reply[ server. id] += "\n\t{name} - {level} {vocation} - **{guild}**".format( **char) if existent: reply += "\nThe following characters were already registered to this user:"******"guild"] = char.get("guild", "No guild") reply += "\n\t**{name}** ({level} {vocation}) - **{guild}**".format( **char) if skipped: reply += "\nThe following characters were skipped (not in tracked worlds or scheduled deletion):" for char, world in skipped: reply += "\n\t{0} ({1})".format(char, world) if error: reply += "\nThe following characters couldn't be fetched: " reply += ", ".join(error) yield from self.bot.say(reply) for server_id, message in log_reply.items(): if message: message = "{0.mention} registered the following characters to {1.mention}: {2}".format( author, user, message) yield from send_log_message(self.bot, self.bot.get_server(server_id), message) return finally: c.close() userDatabase.commit()
def remove_user(self, ctx, *, name): """Removes a discord user from the database The syntax is: /stalk remove name""" if not ctx.message.channel.is_private: return True c = userDatabase.cursor() yield from self.bot.send_typing(ctx.message.channel) # Searching users in server user = get_member_by_name(self.bot, name) # Searching users in database try: c.execute("SELECT id, name from users WHERE name LIKE ?", (name, )) result = c.fetchone() # Users in database and not in servers if result is not None and get_member(self.bot, result['id']) is None: yield from self.bot.say( "**@{0}** was no longer in server and was removed successfully." .format(result["name"])) delete_id = result["id"] # User in servers and in database elif user is not None and result is not None: yield from self.bot.say( "**{0}** was removed successfully.".format( user.display_name)) delete_id = user.id # User in server but not in database elif user is not None and result is None: yield from self.bot.say("**{0}** is not registered.".format( user.display_name)) return # User not in server or database else: yield from self.bot.say( "I don't see any user named **{0}**.".format(name)) return c.execute("DELETE FROM users WHERE id = ?", (delete_id, )) c.execute("SELECT name FROM chars WHERE user_id = ?", (delete_id, )) result = c.fetchall() if len(result) >= 1: chars = ["**" + i["name"] + "**" for i in result] reply = "The following characters were registered to the user:\n\t" reply += "\n\t".join(chars) reply += "\nDo you want to delete them? ``(yes/no)``" yield from self.bot.say(reply) answer = yield from self.bot.wait_for_message( author=ctx.message.author, channel=ctx.message.channel, timeout=30.0) if answer is None: yield from self.bot.say( "I will take your silence as a no...") elif answer.content.lower() in ["yes", "y"]: c.execute("DELETE FROM chars WHERE user_id = ?", (delete_id, )) yield from self.bot.say("Characters deleted successfully.") else: yield from self.bot.say("Ok, we are done then.") return finally: c.close() userDatabase.commit()
def purge(self, ctx): """Performs a database cleanup Removes characters that have been deleted and users with no characters or no longer in server.""" if not ctx.message.channel.is_private: return True c = userDatabase.cursor() try: c.execute("SELECT id FROM users") result = c.fetchall() if result is None: yield from self.bot.say("There are no users registered.") return delete_users = list() yield from self.bot.say("Initiating purge...") # Deleting users no longer in server for row in result: user = get_member(self.bot, row["id"]) if user is None: delete_users.append((row["id"], )) if len(delete_users) > 0: c.executemany("DELETE FROM users WHERE id = ?", delete_users) yield from self.bot.say( "{0} user(s) no longer in the server were removed.".format( c.rowcount)) # Deleting chars with non-existent user c.execute( "SELECT name FROM chars WHERE user_id NOT IN (SELECT id FROM users)" ) result = c.fetchall() if len(result) >= 1: chars = ["**" + i["name"] + "**" for i in result] reply = "{0} char(s) were assigned to a non-existent user and were deleted:\n\t".format( len(result)) reply += "\n\t".join(chars) yield from self.bot.say(reply) c.execute( "DELETE FROM chars WHERE user_id NOT IN (SELECT id FROM users)" ) # Removing deleted chars c.execute("SELECT name,last_level,vocation FROM chars") result = c.fetchall() if result is None: return delete_chars = list() rename_chars = list() # revoc_chars = list() for row in result: char = yield from get_character(row["name"]) if char == ERROR_NETWORK: yield from self.bot.say( "Couldn't fetch **{0}**, skipping...".format( row["name"])) continue # Char was deleted if char == ERROR_DOESNTEXIST: delete_chars.append((row["name"], )) yield from self.bot.say( "**{0}** doesn't exists, deleting...".format( row["name"])) continue # Char was renamed if char['name'] != row["name"]: rename_chars.append(( char['name'], row["name"], )) yield from self.bot.say( "**{0}** was renamed to **{1}**, updating...".format( row["name"], char['name'])) # No need to check if user exists cause those were removed already if len(delete_chars) > 0: c.executemany("DELETE FROM chars WHERE name LIKE ?", delete_chars) yield from self.bot.say("{0} char(s) were removed.".format( c.rowcount)) if len(rename_chars) > 0: c.executemany("UPDATE chars SET name = ? WHERE name LIKE ?", rename_chars) yield from self.bot.say("{0} char(s) were renamed.".format( c.rowcount)) # Remove users with no chars c.execute( "SELECT id FROM users WHERE id NOT IN (SELECT user_id FROM chars)" ) result = c.fetchall() if len(result) >= 1: c.execute( "DELETE FROM users WHERE id NOT IN (SELECT user_id FROM chars)" ) yield from self.bot.say( "{0} user(s) with no characters were removed.".format( c.rowcount)) # Remove level ups of removed characters c.execute( "DELETE FROM char_levelups WHERE char_id NOT IN (SELECT id FROM chars)" ) if c.rowcount > 0: yield from self.bot.say( "{0} level up registries from removed characters were deleted." .format(c.rowcount)) c.execute( "DELETE FROM char_deaths WHERE char_id NOT IN (SELECT id FROM chars)" ) # Remove deaths of removed characters if c.rowcount > 0: yield from self.bot.say( "{0} death registries from removed characters were deleted." .format(c.rowcount)) yield from self.bot.say("Purge done.") return finally: userDatabase.commit() c.close()