class DiscordGSM(): def __init__(self, bot): print('\n----------------') print('Github: \thttps://github.com/DiscordGSM/DiscordGSM') print('Discord:\thttps://discord.gg/Cg4Au9T') print('----------------\n') self.bot = bot self.servers = Servers() self.server_list = self.servers.get() self.messages = [] self.message_error_count = self.current_display_server = 0 def start(self): self.print_to_console(f'Starting DiscordGSM v{VERSION}...') self.query_servers.start() def cancel(self): self.query_servers.cancel() self.print_servers.cancel() self.presense_load.cancel() async def on_ready(self): # set username and avatar icon_file_name = 'images/discordgsm' + ('DGSM_TOKEN' in os.environ and '-heroku' or '') + '.png' with open(icon_file_name, 'rb') as file: try: await bot.user.edit(username='******', avatar=file.read()) except: pass # print info to console print('\n----------------') print(f'Logged in as:\t{bot.user.name}') print(f'Robot ID:\t{bot.user.id}') app_info = await bot.application_info() print(f'Owner ID:\t{app_info.owner.id} ({app_info.owner.name})') print('----------------\n') self.print_presense_hint() self.presense_load.start() await self.set_channels_permissions() self.print_to_console( f'Query server and send discord embed every {REFRESH_RATE} seconds...' ) await self.refresh_discord_embed() self.print_servers.start() # query the servers @tasks.loop(seconds=REFRESH_RATE) async def query_servers(self): server_count = self.servers.query() self.print_to_console(f'{server_count} servers queried') # pre-query servers before ready @query_servers.before_loop async def before_query_servers(self): self.print_to_console('Pre-Query servers...') server_count = self.servers.query() self.print_to_console(f'{server_count} servers queried') await self.bot.wait_until_ready() await self.on_ready() # send messages to discord @tasks.loop(seconds=REFRESH_RATE) async def print_servers(self): if self.message_error_count < 20: updated_count = 0 for i in range(len(self.server_list)): try: await self.messages[i].edit( content=('frontMessage' in self.server_list[i] and self.server_list[i]['frontMessage'].strip()) and self.server_list[i]['frontMessage'] or None, embed=self.get_embed(self.server_list[i])) updated_count += 1 except: self.message_error_count += 1 self.print_to_console( f'ERROR: message {i} fail to edit, message deleted or no permission. Server: {self.server_list[i]["addr"]}:{self.server_list[i]["port"]}' ) self.print_to_console(f'{updated_count} messages updated') else: self.message_error_count = 0 self.print_to_console(f'Message ERROR reached, refreshing...') await self.refresh_discord_embed() # refresh discord presense @tasks.loop(minutes=PRESENCE_RATE) async def presense_load(self): # 1 = display number of servers, 2 = display total players/total maxplayers, 3 = display each server one by one every 10 minutes if len(self.server_list) == 0: activity_text = f'Command: {DGSM_PREFIX}dgsm' if PRESENCE_TYPE <= 1: activity_text = f'{len(self.server_list)} game servers' elif PRESENCE_TYPE == 2: total_activeplayers = total_maxplayers = 0 for server in self.server_list: server_cache = ServerCache(server['addr'], server['port']) data = server_cache.get_data() if data and server_cache.get_status() == 'Online': total_activeplayers += int(data['players']) total_maxplayers += int(data['maxplayers']) activity_text = f'{total_activeplayers}/{total_maxplayers} active players' if total_maxplayers > 0 else '0 players' elif PRESENCE_TYPE >= 3: if self.current_display_server >= len(self.server_list): self.current_display_server = 0 server_cache = ServerCache( self.server_list[self.current_display_server]['addr'], self.server_list[self.current_display_server]['port']) data = server_cache.get_data() if data and server_cache.get_status() == 'Online': activity_text = f'{data["players"]}/{data["maxplayers"]} on {data["name"]}' if int( data["maxplayers"]) > 0 else '0 players' else: activity_text = None self.current_display_server += 1 if activity_text != None: await bot.change_presence(status=discord.Status.online, activity=discord.Activity( name=activity_text, type=3)) self.print_to_console( f'Discord presence updated | {activity_text}') # set channels permissions before sending new messages async def set_channels_permissions(self): channels = [server['channel'] for server in self.server_list] channels = list(set(channels)) # remove duplicated channels for channel in channels: try: await bot.get_channel(channel).set_permissions( bot.user, read_messages=True, send_messages=True, reason='Display servers embed') self.print_to_console( f'Channel: {channel} | Permissions: read_messages, send_messages | Permissions set successfully' ) except: self.print_to_console( f'Channel: {channel} | Permissions: read_messages, send_messages | ERROR: Permissions fail to set' ) # remove old discord embed and send new discord embed async def refresh_discord_embed(self): # refresh servers.json cache self.servers = Servers() self.server_list = self.servers.get() # remove old discord embed channels = [server['channel'] for server in self.server_list] channels = list(set(channels)) # remove duplicated channels for channel in channels: await bot.get_channel(channel).purge( check=lambda m: m.author == bot.user) # send new discord embed self.messages = [ await bot.get_channel(s['channel']).send( content=('frontMessage' in s and s['frontMessage'].strip()) and s['frontMessage'] or None, embed=self.get_embed(s)) for s in self.server_list ] def print_to_console(self, value): print(datetime.now().strftime('%Y-%m-%d %H:%M:%S: ') + value) # 1 = display number of servers, 2 = display total players/total maxplayers, 3 = display each server one by one every 10 minutes def print_presense_hint(self): if PRESENCE_TYPE <= 1: hints = 'number of servers' elif PRESENCE_TYPE == 2: hints = 'total players/total maxplayers' elif PRESENCE_TYPE >= 3: hints = f'each server one by one every {PRESENCE_RATE} minutes' self.print_to_console( f'Presence update type: {PRESENCE_TYPE} | Display {hints}') # get game server discord embed def get_embed(self, server): # load server cache server_cache = ServerCache(server['addr'], server['port']) # load server data data = server_cache.get_data() if data: # load server status Online/Offline status = server_cache.get_status() emoji = (status == 'Online') and ':green_circle:' or ':red_circle:' if status == 'Online': if int(data['maxplayers']) <= int(data['players']): color = discord.Color.from_rgb(240, 71, 71) # red elif int(data['maxplayers']) <= int(data['players']) * 2: color = discord.Color.from_rgb(250, 166, 26) # yellew else: color = discord.Color.from_rgb(67, 181, 129) # green try: if 'color' in server: h = server['color'].lstrip('#') rgb = tuple(int(h[i:i + 2], 16) for i in (0, 2, 4)) color = discord.Color.from_rgb( rgb[0], rgb[1], rgb[2]) except Exception as e: self.print_to_console(e) else: color = discord.Color.from_rgb(32, 34, 37) # dark title = (data['password'] and ':lock: ' or '') + f'`{data["name"]}`' custom = ('custom' in server) and server['custom'] or None if custom and custom.strip(): embed = discord.Embed(title=title, description=custom, color=color) elif server['type'] == 'SourceQuery' and not custom: embed = discord.Embed( title=title, description= f'Connect: steam://connect/{data["addr"]}:{server["port"]}', color=color) else: embed = discord.Embed(title=title, color=color) embed.add_field(name=FIELD_STATUS, value=f'{emoji} **{status}**', inline=True) embed.add_field(name=f'{FIELD_ADDRESS}:{FIELD_PORT}', value=f'`{data["addr"]}:{data["port"]}`', inline=True) flag_emoji = ('country' in server) and ( ':flag_' + server['country'].lower() + f': {server["country"]}') or ':united_nations: Unknown' embed.add_field(name=FIELD_COUNTRY, value=flag_emoji, inline=True) embed.add_field(name=FIELD_GAME, value=data['game'], inline=True) embed.add_field(name=FIELD_CURRENTMAP, value=data['map'], inline=True) if status == 'Online': value = str(data['players']) # example: 20/32 if int(data['bots']) > 0: value += f' ({data["bots"]})' # example: 20 (2)/32 else: value = '0' # example: 0/32 embed.add_field(name=FIELD_PLAYERS, value=f'{value}/{data["maxplayers"]}', inline=True) if 'image_url' in server: image_url = str(server['image_url']) else: image_url = ( CUSTOM_IMAGE_URL and CUSTOM_IMAGE_URL.strip() ) and CUSTOM_IMAGE_URL or f'https://github.com/DiscordGSM/Map-Thumbnails/raw/master/{urllib.parse.quote(data["game"])}' image_url += f'/{urllib.parse.quote(data["map"])}.jpg' embed.set_thumbnail(url=image_url) else: # server fail to query color = discord.Color.from_rgb(240, 71, 71) # red embed = discord.Embed( title='ERROR', description=f'{FIELD_STATUS}: :warning: **Fail to query**', color=color) embed.add_field(name=f'{FIELD_ADDRESS}:{FIELD_PORT}', value=f'{server["addr"]}:{server["port"]}', inline=True) embed.set_footer( text= f'DiscordGSM v{VERSION} | 📺Game Server Monitor | Last update: ' + datetime.now().strftime('%a, %Y-%m-%d %I:%M:%S%p'), icon_url= 'https://github.com/DiscordGSM/DiscordGSM/raw/master/images/discordgsm.png' ) return embed def get_server_list(self): return self.server_list
class DiscordGSM(): def __init__(self, client): self.client = client self.servers = Servers() self.server_list = self.servers.get() self.message_error_count = self.current_display_server = 0 def start(self): self.print_to_console(f'Starting DiscordGSM v.{VERSION}') self.update_messages.start() def cancel(self): self.update_messages.cancel() self.presence_load.cancel() async def on_ready(self): # set username and avatar | not very nice for self-hosted users. # icon_file_name = "images/discordgsm" + ("DGSM_TOKEN" in os.environ and "-heroku" or "") + ".png" # with open(icon_file_name, "rb") as file: # try: # await client.user.edit(username="******", avatar=file.read()) # except Exception as e: # pass # print info to console print("\n----------------") print(f'Logged in as:\t{client.user.name}') print(f'Client ID:\t{client.user.id}') app_info = await client.application_info() print(f'Owner ID:\t{app_info.owner.id} ({app_info.owner.name})') print(f'Invite Link: \t{invite_link}') print("----------------") print( f'Querying {self.servers.get_distinct_server_count()} servers and updating {len(self.server_list)} messages every {REFRESH_RATE} minutes.' ) print("----------------\n") self.presence_load.start() @tasks.loop(minutes=REFRESH_RATE) async def update_messages(self): await self.query_servers() updated_count = 0 for server in self.server_list: if self.message_error_count > ERROR_THRESHOLD: self.message_error_count = 0 self.print_to_console( f'ERROR: update_messages error threshold({ERROR_THRESHOLD}) reached. Reposting messages.' ) await self.repost_messages() break try: message = await self.try_get_message_to_update(server) if not message: self.message_error_count += 1 self.print_to_console( f'\n {self.message_error_count} error(s) in update_messages().' ) continue await message.edit(embed=self.get_embed(server)) updated_count += 1 except Exception as e: self.message_error_count += 1 self.print_to_console( f'ERROR: Failed to edit message for server: {self.get_server_info(server)}. \n {self.message_error_count} error(s) in update_messages(). Missing permissions?\n{e}' ) finally: await asyncio.sleep(SEND_DELAY) self.print_to_console(f'{updated_count} messages updated.') # pre-query servers before ready @update_messages.before_loop async def before_update_messages(self): await self.query_servers() await client.wait_until_ready() await self.on_ready() # remove old discord embed and send new discord embed async def repost_messages(self): self.servers = Servers() self.server_list = self.servers.get() repost_count = 0 # remove old discord embed channels = [server["channel"] for server in self.server_list] channels = list(set(channels)) # remove duplicated channels for channel in channels: try: await client.get_channel(channel).purge( check=lambda m: m.author == client.user) except Exception as e: self.print_to_console( f'ERROR: Unable to delete bot messages.\n{e}') finally: await asyncio.sleep(SEND_DELAY) # send new discord embed for server in self.server_list: try: message = await client.get_channel( server["channel"]).send(embed=self.get_embed(server)) server["message_id"] = message.id repost_count += 1 except Exception as e: self.message_error_count += 1 self.print_to_console( f'ERROR: Failed to send message for server: {self.get_server_info(server)}. Missing permissions ?\n{e}' ) finally: self.servers.update_server_file(self.server_list) await asyncio.sleep(SEND_DELAY) self.print_to_console(f'{repost_count} messages reposted.') # 1 = display number of servers, 2 = display total players/total maxplayers, 3 = display each server one by one every 10 minutes def print_presense_hint(self): if PRESENCE_TYPE <= 1: hints = "number of servers" elif PRESENCE_TYPE == 2: hints = "total players/total maxplayers" elif PRESENCE_TYPE >= 3: hints = f'each server one by one every {PRESENCE_RATE} minutes' self.print_to_console( f'Presence update type: {PRESENCE_TYPE} | Display {hints}') # refresh discord presence @tasks.loop(minutes=PRESENCE_RATE) async def presence_load(self): # 1 = display number of servers, 2 = display total players/total maxplayers, 3 = display each server one by one every 10 minutes if len(self.server_list) == 0: activity_text = f'Command: {PREFIX}dgsm' if PRESENCE_TYPE <= 1: activity_text = f'{self.servers.get_distinct_server_count()} game servers' elif PRESENCE_TYPE == 2: total_activeplayers = total_maxplayers = 0 for server in self.server_list: server_cache = ServerCache(server["address"], server["port"]) data = server_cache.get_data() if data and server_cache.get_status() == "Online": total_activeplayers += int(data["players"]) total_maxplayers += int(data["maxplayers"]) activity_text = f'{total_activeplayers}/{total_maxplayers} active players' if total_maxplayers > 0 else "0 players" elif PRESENCE_TYPE >= 3: if self.current_display_server >= len(self.server_list): self.current_display_server = 0 server_cache = ServerCache( self.server_list[self.current_display_server]["address"], self.server_list[self.current_display_server]["port"]) data = server_cache.get_data() if data and server_cache.get_status() == "Online": activity_text = f'{data["players"]}/{data["maxplayers"]} on {data["name"]}' if int( data["maxplayers"]) > 0 else "0 players" else: activity_text = None self.current_display_server += 1 if activity_text != None: try: await client.change_presence(status=discord.Status.online, activity=discord.Activity( name=activity_text, type=3)) self.print_to_console( f'Discord presence updated | {activity_text}') except Exception as e: self.print_to_console( f'ERROR: Unable to update presence.\n{e}') def print_to_console(self, value): print(datetime.now().strftime("%Y-%m-%d %H:%M:%S: ") + value) async def try_get_message_to_update(self, server): try: message = await client.get_channel( server["channel"]).fetch_message(server["message_id"]) if not message: self.print_to_console( f'ERROR: Failed to fetch message for server: {self.get_server_info(server)}.' ) return None return message except Exception as e: self.print_to_console( f'ERROR: Failed to fetch message for server: {self.get_server_info(server)}. \n{e}' ) return None finally: await asyncio.sleep(SEND_DELAY) async def query_servers(self): try: self.server_list = self.servers.refresh() self.servers.query() except Exception as e: self.print_to_console(f'Error Querying servers: \n{e}') self.print_to_console( f'{self.servers.get_distinct_server_count()} servers queried.') def get_value(self, dataset, field, default=None): if type(dataset) != dict or field not in dataset or dataset[ field] is None or dataset[field] == "": return default return dataset[field] def get_server_info(self, server): return self.get_value(server, "comment", f'{server["address"]}:{server["port"]}') def get_server_list(self): return self.server_list def get_embed(self, server): server_cache = ServerCache(server["address"], server["port"]) data = server_cache.get_data() cache_status = server_cache.get_status() # Evaluate fields try: lock = (server["locked"] if type(self.get_value(server, "locked")) == bool else data["password"] if type( self.get_value(data, "password")) == bool else False) title = self.get_value(server, "title") or self.get_value( data, "game") or self.get_value(server, "game") title = f':lock: {title}' if lock else f':unlock: {title}' description = self.get_value(server, "custom") status = (f':green_circle: **{FIELD_ONLINE}**' if cache_status == "Online" else f':red_circle: **{FIELD_OFFLINE}**' if cache_status == "Offline" and data is not False else f':yellow_circle: **{FIELD_UNKNOWN}**') hostname = self.get_value(server, "hostname") or self.get_value( data, "name") or SPACER players_string = self.determinePlayerString( server, data, cache_status) port = self.get_value(data, "port") address = self.get_value( server, "public_address" ) or self.get_value( data, "address" ) and port and f'{self.get_value(data, "address")}:{port}' or SPACER password = self.get_value(server, "password") country = self.get_value(server, "country") map = None if self.get_value( server, "map") == False else self.get_value( server, "map") or self.get_value(data, "map") image_url = self.get_value(server, "image_url") steam_id = self.get_value(server, "steam_id") direct_join = self.get_value(server, "direct_join") color = self.determineColor(server, data, cache_status) except Exception as e: self.print_to_console(f'Error Evaluate fields.\n{e}') # Build embed try: embed = (discord.Embed( title=title, description=description, color=color) if description else discord.Embed(title=title, color=color)) embed.add_field(name=FIELD_STATUS, value=status, inline=True) embed.add_field(name=FIELD_NAME, value=hostname, inline=True) embed.add_field(name=SPACER, value=SPACER, inline=True) embed.add_field(name=FIELD_PLAYERS, value=players_string, inline=True) embed.add_field(name=FIELD_ADDRESS, value=f'`{address}`', inline=True) if password is None: embed.add_field(name=SPACER, value=SPACER, inline=True) else: embed.add_field(name=FIELD_PASSWORD, value=f'`{password}`', inline=True) if country: embed.add_field(name=FIELD_COUNTRY, value=f':flag_{country.lower()}:', inline=True) if map and not country: embed.add_field(name=SPACER, value=SPACER, inline=True) if map: embed.add_field(name=FIELD_CURRENTMAP, value=map, inline=True) if map or country: embed.add_field(name=SPACER, value=SPACER, inline=True) if steam_id: if direct_join: if password: embed.add_field( name=FIELD_JOIN, value=f'steam://connect/{address}/{password}', inline=False) else: try: embed.add_field(name=FIELD_JOIN, value=f'steam://connect/{address}', inline=False) except Exception as e: self.print_to_console( f'Error Building embed 2.\n{e}') else: embed.add_field(name=FIELD_LAUNCH, value=f'steam://rungameid/{steam_id}', inline=False) if image_url: embed.set_thumbnail(url=image_url) embed.set_footer( text= f'DiscordGSM v.{VERSION} | Game Server Monitor | {FIELD_LASTUPDATE}: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}{SPACER}', icon_url=CUSTOM_IMAGE_URL) except Exception as e: self.print_to_console(f'Error Building embed.\n{e}') return embed def determineColor(self, server, data, cache_status): players = self.get_value(data, "players", "?") maxplayers = self.get_value(data, "maxplayers") or self.get_value( server, "maxplayers") or "?" if cache_status == "Online" and players != "?" and maxplayers != "??": if players >= maxplayers: color = discord.Color.from_rgb(240, 71, 71) # red elif players >= maxplayers / 2: color = discord.Color.from_rgb(250, 166, 26) # yellow else: color = discord.Color.from_rgb(67, 181, 129) # green else: color = discord.Color.from_rgb(0, 0, 0) # black # color is defined try: if "color" in server: h = server["color"].lstrip("#") rgb = tuple(int(h[i:i + 2], 16) for i in (0, 2, 4)) color = discord.Color.from_rgb(rgb[0], rgb[1], rgb[2]) except: pass return color def determinePlayerString(self, server, data, cache_status): players = self.get_value(data, "players", "?") maxplayers = self.get_value(data, "maxplayers") or self.get_value( server, "maxplayers") or "?" bots = self.get_value(data, "bots") if cache_status == "Offline": players = 0 bots = None if data == False: players = "?" bots = None return f'{players}({bots})/{maxplayers}' if bots is not None and bots > 0 else f'{players}/{maxplayers}'