diff options
Diffstat (limited to 'code')
-rw-r--r-- | code/botcmds/helpcmds.py | 134 | ||||
-rw-r--r-- | code/botcmds/maincmd.py | 258 | ||||
-rw-r--r-- | code/config.py | 4 | ||||
-rw-r--r-- | code/plusbot.py | 397 | ||||
-rw-r--r-- | code/utils/botsql.py | 58 |
5 files changed, 547 insertions, 304 deletions
diff --git a/code/botcmds/helpcmds.py b/code/botcmds/helpcmds.py new file mode 100644 index 0000000..10bc239 --- /dev/null +++ b/code/botcmds/helpcmds.py @@ -0,0 +1,134 @@ +import discord, config +from discord.ext import commands +from discord.errors import Forbidden + +"""This custom help command is a perfect replacement for the default one on any Discord Bot written in Discord.py! + +Original concept by Jared Newsom (AKA Jared M.F.) +[Deleted] https://gist.github.com/StudioMFTechnologies/ad41bfd32b2379ccffe90b0e34128b8b +Rewritten and optimized by github.com/nonchris +https://gist.github.com/nonchris/1c7060a14a9d94e7929aa2ef14c41bc2 +Edited by stolenvw for Stolenvw ValPlusBot +""" + + +async def send_embed(ctx, embed): + """ + Function that handles the sending of embeds + -> Takes context and embed to send + - tries to send embed in channel + - tries to send normal message when that fails + - tries to send embed private with information abot missing permissions + If this all fails: https://youtu.be/dQw4w9WgXcQ + """ + try: + await ctx.send(embed=embed) + except Forbidden: + try: + await ctx.send("Hey, seems like I can't send embeds. Please check my permissions :)") + except Forbidden: + await ctx.author.send( + f"Hey, seems like I can't send any message in {ctx.channel.name} on {ctx.guild.name}\n" + f"May you inform the server team about this issue? :slight_smile: ", embed=embed) + + +class Help(commands.Cog): + """ + Sends this help message + """ + + def __init__(self, bot): + self.bot = bot + self.bot.remove_command("help") + + @commands.command() + # @commands.bot_has_permissions(add_reactions=True,embed_links=True) + async def help(self, ctx, *input): + """Shows all modules of that bot""" + + # !SET THOSE VARIABLES TO MAKE THE COG FUNCTIONAL! + prefix = config.BOT_PREFIX + version = "v1.0.0" + + # checks if cog parameter was given + # if not: sending all modules and commands not associated with a cog + if not input: + # starting to build embed + emb = discord.Embed(title='Valheim Plus Discord Bot', url="https://github.com/stolenvw/ValheimPlus-Discord_Bot", color=discord.Color.blue(), + description=f'Use `{prefix}help <module>` to gain more information about that module ' + f'\n') + + # iterating trough cogs, gathering descriptions + cogs_desc = '' + for cog in self.bot.cogs: + if cog != "BotSQL" and cog != "Admin": + cogs_desc += f'`{cog}` {self.bot.cogs[cog].__doc__}\n' + + # adding 'list' of cogs to embed + emb.add_field(name='Modules', value=cogs_desc, inline=False) + + # integrating trough uncategorized commands + commands_desc = '' + for command in self.bot.walk_commands(): + # if cog not in a cog + # listing command if cog name is None and command isn't hidden + if not command.cog_name and not command.hidden: + commands_desc += f'{command.name} - {command.help}\n' + + # adding those commands to embed + if commands_desc: + emb.add_field(name='Not belonging to a module', value=commands_desc, inline=False) + + emb.set_footer(text=f"Stolenvw ValPlusBot {version}") + + # block called when one cog-name is given + # trying to find matching cog and it's commands + elif len(input) == 1: + + # iterating trough cogs + for cog in self.bot.cogs: + # check if cog is the matching one + if cog.lower() == input[0].lower(): + + # making title - getting description from doc-string below class + emb = discord.Embed(title=f'{cog} - Commands', description=self.bot.cogs[cog].__doc__, + color=discord.Color.green()) + + # getting commands from cog + for command in self.bot.get_cog(cog).get_commands(): + # if cog is not hidden + if not command.hidden: + if not command.usage: + emb.add_field(name=f"`{prefix}{command.name}`", value=command.help, inline=False) + else: + emb.add_field(name=f"`{prefix}{command.name} {command.usage}`", value=command.help, inline=False) + # found cog - breaking loop + break + + # if input not found + # yes, for-loops have an else statement, it's called when no 'break' was issued + else: + emb = discord.Embed(title="What's that?!", + description=f"I've never heard from a module called `{input[0]}` before", + color=discord.Color.orange()) + + # too many cogs requested - only one at a time allowed + elif len(input) > 1: + emb = discord.Embed(title="That's too much.", + description="Please request only one module at once", + color=discord.Color.orange()) + + else: + emb = discord.Embed(title="It's a magical place.", + description="I don't know how you got here. But I didn't see this coming at all.\n" + "Would you please be so kind to report that issue to me on github?\n" + "https://github.com/nonchris/discord-fury/issues\n" + "Thank you! ~Chris", + color=discord.Color.red()) + + # sending reply embed using our own function defined above + await send_embed(ctx, emb) + + +def setup(bot): + bot.add_cog(Help(bot)) diff --git a/code/botcmds/maincmd.py b/code/botcmds/maincmd.py new file mode 100644 index 0000000..e1e2000 --- /dev/null +++ b/code/botcmds/maincmd.py @@ -0,0 +1,258 @@ +import discord, typing, config, time, mysql.connector +from discord.ext import commands +from datetime import datetime, timedelta +from matplotlib import pyplot as plt +import matplotlib.dates as md +import matplotlib.ticker as ticker +import matplotlib.spines as ms +import pandas as pd + +class Main(commands.Cog): + """ + Main bot commands + """ + + def __init__(self, bot): + self.bot = bot + self.mydb = mysql.connector.connect( + host=config.SQL_HOST, + user=config.SQL_USER, + password=config.SQL_PASS, + database=config.SQL_DATABASE, + port=config.SQL_PORT, + ) + + @commands.command(name="deaths", + brief="Deaths leaderboard", + help="Shows a top 5 leaderboard of players with the most deaths. \n Available: 1-10 (default: 5)", + usage="<n>", + ) + async def leaderboards(self, ctx, arg: typing.Optional[str] = '5'): + ldrembed = discord.Embed(title=":skull_crossbones: __Death Leaderboards (top " + arg + ")__ :skull_crossbones:", color=0xFFC02C) + botsql = self.bot.get_cog('BotSQL') + mycursor = await botsql.get_cursor() + sql = """SELECT user, deaths FROM players WHERE deaths > 0 ORDER BY deaths DESC LIMIT %s""" % (arg) + mycursor.execute(sql) + Info = mycursor.fetchall() + row_count = mycursor.rowcount + l = 1 + for ind in Info: + grammarnazi = 'deaths' + leader = '' + pdname = ind[0] + pddeath = ind[1] + if pddeath == 1 : + grammarnazi = 'death' + if l == 1: + leader = ':crown:' + ldrembed.add_field(name="{} {}".format(pdname,leader), + value='{} {}'.format(pddeath,grammarnazi), + inline=False) + l += 1 + mycursor.close() + await ctx.send(embed=ldrembed) + + @commands.command(name="stats", + brief="Graph of connected players", + help="Plots a graph of connected players over the last X hours.\n Available args: 24, 12, w (default: 24)", + usage="<arg>", + ) + async def gen_plot(self, ctx, tmf: typing.Optional[str] = '24'): + user_range = 0 + if tmf.lower() in ['w', 'week', 'weeks']: + user_range = 168 - 1 + tlookup = int(time.time()) - 604800 + interval = 24 + date_format = '%m/%d' + timedo = 'week' + description = 'Players online in the past ' + timedo + ':' + elif tmf.lower() in ['12', '12hrs', '12h', '12hr']: + user_range = 12 - 0.15 + tlookup = int(time.time()) - 43200 + interval = 1 + date_format = '%H' + timedo = '12hrs' + description = 'Players online in the past ' + timedo + ':' + else: + user_range = 24 - 0.30 + tlookup = int(time.time()) - 86400 + interval = 2 + date_format = '%H' + timedo = '24hrs' + description = 'Players online in the past ' + timedo + ':' + + #Get data from mysql + botsql = self.bot.get_cog('BotSQL') + mycursor = await botsql.get_cursor() + mycursor.close() + sqls = """SELECT date, users FROM serverstats WHERE timestamp BETWEEN '%s' AND '%s'""" % (tlookup, int(time.time())) + df = pd.read_sql(sqls, self.mydb, parse_dates=['date']) + lastday = datetime.now() - timedelta(hours = user_range) + + # Plot formatting / styling matplotlib + plt.style.use('seaborn-pastel') + plt.minorticks_off() + fig, ax = plt.subplots() + ax.grid(b=True, alpha=0.2) + ax.set_xlim(lastday, datetime.now()) + # ax.set_ylim(0, 10) Not sure about this one yet + for axis in [ax.xaxis, ax.yaxis]: + axis.set_major_locator(ticker.MaxNLocator(integer=True)) + ax.xaxis.set_major_formatter(md.DateFormatter(date_format)) + ax.xaxis.set_major_locator(md.HourLocator(interval=interval)) + for spine in ax.spines.values(): + spine.set_visible(False) + for tick in ax.get_xticklabels(): + tick.set_color('gray') + for tick in ax.get_yticklabels(): + tick.set_color('gray') + + #Plot and rasterize figure + plt.gcf().set_size_inches([5.5,3.0]) + plt.plot(df['date'], df['users'], drawstyle='steps-post') + plt.tick_params(axis='both', which='both', bottom=False, left=False) + plt.margins(x=0,y=0,tight=True) + plt.tight_layout() + fig.savefig('img/temp.png', transparent=True, pad_inches=0) # Save and upload Plot + image = discord.File('img/temp.png', filename='temp.png') + plt.close() + embed = discord.Embed(title=config.SERVER_NAME, description=description, colour=12320855) + embed.set_image(url='attachment://temp.png') + await ctx.send(file=image, embed=embed) + + @commands.command(name="playerstats", + brief="Player stats", + help="Shows player stats on active monitored world.\n Arg= <Players Name>", + usage="<arg>", + ) + async def playstats(self, ctx, arg): + botsql = self.bot.get_cog('BotSQL') + mycursor = await botsql.get_cursor() + sql = """SELECT user, deaths, startdate, playtime FROM players WHERE user = '%s'""" % (arg) + mycursor.execute(sql) + Info = mycursor.fetchall() + row_count = mycursor.rowcount + if row_count == 1: + Info=Info[0] + plsembed = discord.Embed(title=":bar_chart: __Player Stats For " + Info[0] + "__ :bar_chart:", color=0x4A90E2) + plsembed.add_field(name="Server Join Date:", + value='{}'.format(Info[2]), + inline=True) + plsembed.add_field(name="Play Time:", + value=str(timedelta(seconds = Info[3])), + inline=True) + plsembed.add_field(name="Deaths:", + value=Info[1], + inline=True) + await ctx.send(embed=plsembed) + else: + await ctx.send(content=':no_entry_sign: **' + arg + '** Not Found') + mycursor.close() + + @commands.command(name="active", + brief="Active players", + help="Shows who is currently logged into the server and how long they have been on for.", + ) + async def actives(self, ctx): + botsql = self.bot.get_cog('BotSQL') + mycursor = await botsql.get_cursor() + sql = """SELECT user, jointime FROM players WHERE ingame = 1 ORDER BY jointime LIMIT 10""" + mycursor.execute(sql) + Info = mycursor.fetchall() + row_count = mycursor.rowcount + if row_count == 0: + await ctx.send(content=':globe_with_meridians: 0 Players Active') + else: + ldrembed = discord.Embed(title=":man_raising_hand: __Active Users__ :woman_raising_hand:", color=0x50E3C2) + EndTime = int(time.time()) + for ind in Info: + pname = ind[0] + onfor = "Online For:" + ponline = str(timedelta(seconds = EndTime - ind[1])) + ldrembed.add_field(name="{}".format(pname), + value='{} {}'.format(onfor,ponline), + inline=False) + await ctx.send(embed=ldrembed) + mycursor.close() + + @commands.command(name="version", + brief="Server Versions", + help="Shows current version of Valheim and Valheim Plus server is running.", + ) + async def versions(self, ctx): + botsql = self.bot.get_cog('BotSQL') + mycursor = await botsql.get_cursor() + sql = """SELECT serverversion, plusversion FROM exstats WHERE id = 1""" + mycursor.execute(sql) + Info = mycursor.fetchall() + row_count = mycursor.rowcount + if row_count == 1: + Info=Info[0] + sembed = discord.Embed(title="Server Versions", color=0x407500) + sembed.add_field(name="Valheim:", + value='{}'.format(Info[0]), + inline=True) + sembed.add_field(name="Valheim Plus:", + value='{}'.format(Info[1]), + inline=True) + await ctx.send(embed=sembed) + else: + await ctx.send(content=':no_entry_sign: Sorry no game version info found in the DB') + mycursor.close() + + @commands.command(name="setstatus", + brief="Server Versions", + help="Set status message of the bot. \n Available arg: playing, watching, listening", + usage='<arg> <"arg1">', + hidden=True, + ) + @commands.is_owner() + async def setstatus(self, ctx, arg: typing.Optional[str] = '0', arg1: typing.Optional[str] = '1'): + if arg == "playing": + await self.bot.change_presence(activity=discord.Game(arg1)) + # elif arg == "streaming": + # await bot.change_presence(activity=discord.Streaming(name=arg1, url=arg2)) + elif arg == "watching": + await self.bot.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name=arg1)) + elif arg == "listening": + await self.bot.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name=arg1)) + else: + await ctx.channel.send('Usage: `{}setstatus <playing|watching|listening> "<Some activity>"`'.format(self.bot.command_prefix)) + + @commands.command(name="savestats", + brief="Save stats", + help="Shows how many zods where saved and time it took to save them.", + hidden=True, + ) + @commands.is_owner() + async def savestats(self, ctx): + if config.EXSERVERINFO == True: + botsql = self.bot.get_cog('BotSQL') + mycursor = await botsql.get_cursor() + sql = """SELECT savezdos, savesec, worldsize, timestamp FROM exstats WHERE savesec is not null AND savezdos is not null ORDER BY timestamp DESC LIMIT 1""" + mycursor.execute(sql) + Info = mycursor.fetchall() + row_count = mycursor.rowcount + if row_count == 1: + Info=Info[0] + sembed = discord.Embed(title="World File Save Stats", color=0x407500, timestamp=datetime.utcfromtimestamp(Info[3])) + sembed.set_footer(text="Last saved") + sembed.add_field(name="Zdos Saved:", + value='{}'.format(Info[0]), + inline=True) + sembed.add_field(name="Saving Took:", + value='{}ms'.format(Info[1]), + inline=True) + if config.WORLDSIZE == True: + sembed.add_field(name="World Size:", + value='{}MB'.format(Info[2]), + inline=True) + await ctx.send(embed=sembed) + else: + await ctx.send(content=':no_entry_sign: No World File Save Stats Found') + mycursor.close() + else: + await ctx.send(content=':no_entry_sign: Extra Server Info is turned off, turn on to see save stats') + +def setup(bot): + bot.add_cog(Main(bot)) diff --git a/code/config.py b/code/config.py index ea7abdc..30e7f16 100644 --- a/code/config.py +++ b/code/config.py @@ -3,8 +3,12 @@ # IMPORTANT! You must have a log for event reports and death leaderboards # logs are achieved by using -logfile flag on launch, or by logging stdout # Windows Users use forward slashes +# Path to your log file file = '/var/log/valheim.log' +# Path to Valheim Plus config file (Only needed if using optional pluscmds) +vplusfile = '/home/user/valheim/BepInEx/config/valheim_plus.cfg' + BOT_TOKEN = "" # Change ? to command prefix you want to use diff --git a/code/plusbot.py b/code/plusbot.py index 708d1e7..e94da6e 100644 --- a/code/plusbot.py +++ b/code/plusbot.py @@ -1,21 +1,12 @@ import os, time, re, discord, asyncio, config, emoji, sys, colorama, typing, signal, errno, mysql.connector, a2s -from matplotlib import pyplot as plt from datetime import datetime, timedelta from colorama import Fore, Style, init from config import LOGCHAN_ID as lchanID from config import VCHANNEL_ID as chanID from config import BUGCHANNEL_ID as dbchanID -from config import SQL_HOST as MYhost -from config import SQL_PORT as MYport -from config import SQL_USER as MYuser -from config import SQL_PASS as MYpass -from config import SQL_DATABASE as MYbase from config import file from discord.ext import commands -import matplotlib.dates as md -import matplotlib.ticker as ticker -import matplotlib.spines as ms -import pandas as pd + ######################### Code below ########################## ##### Dont complain if you edit it and something dont work #### @@ -35,48 +26,11 @@ sversion = '^[0-9]{2}\/[0-9]{2}\/[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}: Valheim ve gdays = '^[0-9]{2}\/[0-9]{2}\/[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}: Time [\.0-9]+, day:([0-9]+)\s{1,}nextm:[\.0-9]+\s+skipspeed:[\.0-9]+$' -bot = commands.Bot(command_prefix=config.BOT_PREFIX, help_command=None) +bot = commands.Bot(command_prefix=config.BOT_PREFIX) server_name = config.SERVER_NAME sonline = 1 - -# Connect to MYSQL -async def mydbconnect(): - global mydb - mydb = mysql.connector.connect( - host=MYhost, - user=MYuser, - password=MYpass, - database=MYbase, - port=MYport, - ) - bugchan = bot.get_channel(dbchanID) - try: - if mydb.is_connected(): - db_Info = mydb.get_server_info() - print(Fore.GREEN + "Connected to MySQL database... MySQL Server version ", db_Info + Style.RESET_ALL) - if config.USEDEBUGCHAN == True: - buginfo = discord.Embed(title=":white_check_mark: **INFO** :white_check_mark:", description="Connected to MySQL database... MySQL Server version " + db_Info, color=0x7EFF00) - buginfo.set_author(name=server_name) - await bugchan.send(embed=buginfo) - except mysql.connector.Error as err: - print(Fore.RED + err + 'From MySQL database' + Style.RESET_ALL) - if config.USEDEBUGCHAN == True: - bugerror = discord.Embed(title=":sos: **ERROR** :sos:", description="{} From MySQL database".format(err), color=0xFF001E) - bugerror.set_author(name=server_name) - await bugchan.send(embed=bugerror) - -async def get_cursor(): - try: - mydb.ping(reconnect=True, attempts=3, delay=5) - except mysql.connector.Error as err: - await mydbconnect() - print(Fore.RED + "Connection to MySQL database went away... Reconnecting " + Style.RESET_ALL) - if config.USEDEBUGCHAN == True: - bugchan = bot.get_channel(dbchanID) - bugerror = discord.Embed(title=":sos: **ERROR** :sos:", description="Connection to MySQL database went away... Reconnecting", color=0xFF001E) - bugerror.set_author(name=server_name) - await bugchan.send(embed=bugerror) - return mydb.cursor() +startup_extensions = ["utils.botsql"] +cogs_dir = "botcmds" # Method for catching SIGINT, cleaner output for restarting bot def signal_handler(signal, frame): @@ -121,240 +75,65 @@ async def on_ready(): value="#{}".format(bot.get_channel(dbchanID)), inline=False) await bugchan.send(embed=buginfo) + if __name__ == "__main__": + icount = 0 + ecount = 0 + for extension in startup_extensions: + try: + bot.load_extension(extension) + print(Fore.GREEN + 'Loaded extension {}'.format(extension) + Style.RESET_ALL) + if config.USEDEBUGCHAN == True: + if icount == 1: + description = description + '\n' + '{}'.format(extension) + else: + description = '{}'.format(extension) + icount = 1 + except Exception as e: + exc = '{}: {}'.format(type(e).__name__, e) + print(Fore.RED + 'Failed to load extension {}\n{}'.format(extension, exc) + Style.RESET_ALL) + if config.USEDEBUGCHAN == True: + if ecount == 1: + erdescription = erdescription + '\n' + '{}'.format(extension) + else: + erdescription = '{}'.format(extension) + ecount = 1 + for extension in [f.replace('.py', '') for f in os.listdir(cogs_dir) if os.path.isfile(os.path.join(cogs_dir, f))]: + try: + bot.load_extension(cogs_dir + "." + extension) + print(Fore.GREEN + 'Loaded extension {}.{}'.format(cogs_dir, extension) + Style.RESET_ALL) + if config.USEDEBUGCHAN == True: + if icount == 1: + description = description + '\n' + '{}.{}'.format(cogs_dir, extension) + else: + description = '{}.{}'.format(cogs_dir, extension) + icount = 1 + except Exception as e: + exc = '{}: {}'.format(type(e).__name__, e) + print(Fore.RED + 'Failed to load extension {}\n{}'.format(extension, exc) + Style.RESET_ALL) + if config.USEDEBUGCHAN == True: + if ecount == 1: + erdescription = erdescription + '\n' + '{}.{}'.format(cogs_dir, extension) + else: + erdescription = '{}.{}'.format(cogs_dir, extension) + ecount = 1 + if icount == 1: + buginfo = discord.Embed(title=":white_check_mark: **INFO** :white_check_mark:", color=0x7EFF00) + buginfo.set_author(name=server_name) + buginfo.add_field(name="Loaded extensions:", + value="{}".format(description), + inline=False) + await bugchan.send(embed=buginfo) + if ecount == 1: + bugerror = discord.Embed(title=":sos: **ERROR** :sos:", color=0xFF001E) + bugerror.set_author(name=server_name) + bugerror.add_field(name="Failed to load extensions:", + value="{}".format(erdescription), + inline=False) + await bugchan.send(embed=bugerror) bot.loop.create_task(serveronline()) - await mydbconnect() - -@bot.command(name='help') -async def help_ctx(ctx): - help_embed = discord.Embed(description="[**Valheim Plus Discord Bot**](https://github.com/stolenvw/ValheimPlus-Discord_Bot)", color=0x33a163,) - help_embed.add_field(name="{}stats <n>".format(bot.command_prefix), - value="Plots a graph of connected players over the last X hours.\n Example: `{}stats 12` \n Available: 24, 12, w (*default: 24*)".format(bot.command_prefix), - inline=True) - help_embed.add_field(name="{}deaths <n>".format(bot.command_prefix), - value="Shows a top 5 leaderboard of players with the most deaths. \n Example:`{}deaths 3` \n Available: 1-10 (*default: 10*)".format(bot.command_prefix), - inline=True) - help_embed.add_field(name="{}playerstats <playername>".format(bot.command_prefix), - value="Shows player stats on active monitored world. \n Example: `{}playerstats bob`".format(bot.command_prefix), - inline=True) - help_embed.add_field(name="{}active".format(bot.command_prefix), - value="Shows who is currently logged into the server and how long they have been on for. \n Example: `{}active`".format(bot.command_prefix), - inline=True) - help_embed.add_field(name="{}version".format(bot.command_prefix), - value="Shows current version of Valheim and Valheim Plus server is running. \n Example: `{}version`".format(bot.command_prefix), - inline=True) - is_owner = await ctx.bot.is_owner(ctx.author) - if is_owner: - help_embed.add_field(name="**Owner**", - value="Owner only commands", - inline=False) - help_embed.add_field(name="{}setstatus <type> <message>".format(bot.command_prefix), - value='Set status message of the bot. \n Example: `{}setstatus playing "Valheim"` \n Available type: playing, watching, listening'.format(bot.command_prefix), - inline=True) - if config.EXSERVERINFO == True: - help_embed.add_field(name="{}savestats".format(bot.command_prefix), - value="Shows how many zods where saved and time it took to save them. \n Example: `{}savestats`".format(bot.command_prefix), - inline=True) - help_embed.set_footer(text="stolenvw ValPlusBot v0.10") - await ctx.send(embed=help_embed) - -@bot.command(name="deaths") -async def leaderboards(ctx, arg: typing.Optional[str] = '5'): - ldrembed = discord.Embed(title=":skull_crossbones: __Death Leaderboards (top " + arg + ")__ :skull_crossbones:", color=0xFFC02C) - mycursor = await get_cursor() - sql = """SELECT user, deaths FROM players WHERE deaths > 0 ORDER BY deaths DESC LIMIT %s""" % (arg) - mycursor.execute(sql) - Info = mycursor.fetchall() - row_count = mycursor.rowcount - l = 1 - for ind in Info: - grammarnazi = 'deaths' - leader = '' - pdname = ind[0] - pddeath = ind[1] - if pddeath == 1 : - grammarnazi = 'death' - if l == 1: - leader = ':crown:' - ldrembed.add_field(name="{} {}".format(pdname,leader), - value='{} {}'.format(pddeath,grammarnazi), - inline=False) - l += 1 - mycursor.close() - await ctx.send(embed=ldrembed) - -@bot.command(name="stats") -async def gen_plot(ctx, tmf: typing.Optional[str] = '24'): - user_range = 0 - if tmf.lower() in ['w', 'week', 'weeks']: - user_range = 168 - 1 - tlookup = int(time.time()) - 604800 - interval = 24 - date_format = '%m/%d' - timedo = 'week' - description = 'Players online in the past ' + timedo + ':' - elif tmf.lower() in ['12', '12hrs', '12h', '12hr']: - user_range = 12 - 0.15 - tlookup = int(time.time()) - 43200 - interval = 1 - date_format = '%H' - timedo = '12hrs' - description = 'Players online in the past ' + timedo + ':' - else: - user_range = 24 - 0.30 - tlookup = int(time.time()) - 86400 - interval = 2 - date_format = '%H' - timedo = '24hrs' - description = 'Players online in the past ' + timedo + ':' - - #Get data from mysql - mycursor = await get_cursor() - mycursor.close() - sqls = """SELECT date, users FROM serverstats WHERE timestamp BETWEEN '%s' AND '%s'""" % (tlookup, int(time.time())) - df = pd.read_sql(sqls, mydb, parse_dates=['date']) - lastday = datetime.now() - timedelta(hours = user_range) - - # Plot formatting / styling matplotlib - plt.style.use('seaborn-pastel') - plt.minorticks_off() - fig, ax = plt.subplots() - ax.grid(b=True, alpha=0.2) - ax.set_xlim(lastday, datetime.now()) - # ax.set_ylim(0, 10) Not sure about this one yet - for axis in [ax.xaxis, ax.yaxis]: - axis.set_major_locator(ticker.MaxNLocator(integer=True)) - ax.xaxis.set_major_formatter(md.DateFormatter(date_format)) - ax.xaxis.set_major_locator(md.HourLocator(interval=interval)) - for spine in ax.spines.values(): - spine.set_visible(False) - for tick in ax.get_xticklabels(): - tick.set_color('gray') - for tick in ax.get_yticklabels(): - tick.set_color('gray') + botsql = bot.get_cog('BotSQL') + await botsql.mydbconnect() - #Plot and rasterize figure - plt.gcf().set_size_inches([5.5,3.0]) - plt.plot(df['date'], df['users'], drawstyle='steps-post') - plt.tick_params(axis='both', which='both', bottom=False, left=False) - plt.margins(x=0,y=0,tight=True) - plt.tight_layout() - fig.savefig('img/temp.png', transparent=True, pad_inches=0) # Save and upload Plot - image = discord.File('img/temp.png', filename='temp.png') - plt.close() - embed = discord.Embed(title=server_name, description=description, colour=12320855) - embed.set_image(url='attachment://temp.png') - await ctx.send(file=image, embed=embed) - -@bot.command(name="playerstats") -async def playstats(ctx, arg): - mycursor = await get_cursor() - sql = """SELECT user, deaths, startdate, playtime FROM players WHERE user = '%s'""" % (arg) - mycursor.execute(sql) - Info = mycursor.fetchall() - row_count = mycursor.rowcount - if row_count == 1: - Info=Info[0] - plsembed = discord.Embed(title=":bar_chart: __Player Stats For " + Info[0] + "__ :bar_chart:", color=0x4A90E2) - plsembed.add_field(name="Server Join Date:", - value='{}'.format(Info[2]), - inline=True) - plsembed.add_field(name="Play Time:", - value=await convert(Info[3]), - inline=True) - plsembed.add_field(name="Deaths:", - value=Info[1], - inline=True) - await ctx.send(embed=plsembed) - else: - await ctx.send(content=':no_entry_sign: **' + arg + '** Not Found') - mycursor.close() - -@bot.command(name="active") -async def actives(ctx): - mycursor = await get_cursor() - sql = """SELECT user, jointime FROM players WHERE ingame = 1 ORDER BY jointime LIMIT 10""" - mycursor.execute(sql) - Info = mycursor.fetchall() - row_count = mycursor.rowcount - if row_count == 0: - await ctx.send(content=':globe_with_meridians: 0 Players Active') - else: - ldrembed = discord.Embed(title=":man_raising_hand: __Active Users__ :woman_raising_hand:", color=0x50E3C2) - EndTime = int(time.time()) - for ind in Info: - pname = ind[0] - onfor = "Online For:" - ponline = await convert(EndTime - ind[1]) - ldrembed.add_field(name="{}".format(pname), - value='{} {}'.format(onfor,ponline), - inline=False) - await ctx.send(embed=ldrembed) - mycursor.close() - -@bot.command(name="version") -async def versions(ctx): - mycursor = await get_cursor() - sql = """SELECT serverversion, plusversion FROM exstats WHERE id = 1""" - mycursor.execute(sql) - Info = mycursor.fetchall() - row_count = mycursor.rowcount - if row_count == 1: - Info=Info[0] - sembed = discord.Embed(title="Server Versions", color=0x407500) - sembed.add_field(name="Valheim:", - value='{}'.format(Info[0]), - inline=True) - sembed.add_field(name="Valheim Plus:", - value='{}'.format(Info[1]), - inline=True) - await ctx.send(embed=sembed) - else: - await ctx.send(content=':no_entry_sign: Sorry no game version info found in the DB') - mycursor.close() - -@bot.command(name="setstatus") -@commands.is_owner() -async def setstatus(ctx, arg: typing.Optional[str] = '0', arg1: typing.Optional[str] = '1'): - if arg == "playing": - await bot.change_presence(activity=discord.Game(arg1)) -# elif arg == "streaming": -# await bot.change_presence(activity=discord.Streaming(name=arg1, url=arg2)) - elif arg == "watching": - await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.watching, name=arg1)) - elif arg == "listening": - await bot.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name=arg1)) - else: - await ctx.channel.send('Usage: `{}setstatus <playing|watching|listening> "<Some activity>"`'.format(bot.command_prefix)) - -@bot.command(name="savestats") -@commands.is_owner() -async def savestats(ctx): - if config.EXSERVERINFO == True: - mycursor = await get_cursor() - sql = """SELECT savezdos, savesec, worldsize, timestamp FROM exstats WHERE savesec is not null AND savezdos is not null ORDER BY timestamp DESC LIMIT 1""" - mycursor.execute(sql) - Info = mycursor.fetchall() - row_count = mycursor.rowcount - if row_count == 1: - Info=Info[0] - sembed = discord.Embed(title="World File Save Stats", color=0x407500, timestamp=datetime.utcfromtimestamp(Info[3])) - sembed.set_footer(text="Last saved") - sembed.add_field(name="Zdos Saved:", - value='{}'.format(Info[0]), - inline=True) - sembed.add_field(name="Saving Took:", - value='{}ms'.format(Info[1]), - inline=True) - if config.WORLDSIZE == True: - sembed.add_field(name="World Size:", - value='{}MB'.format(Info[2]), - inline=True) - await ctx.send(embed=sembed) - else: - await ctx.send(content=':no_entry_sign: No World File Save Stats Found') - mycursor.close() - else: - await ctx.send(content=':no_entry_sign: Extra Server Info is turned off, turn on to see save stats') # Main loop for reading log file and outputing events async def mainloop(file): await bot.wait_until_ready() @@ -383,15 +162,17 @@ async def mainloop(file): else: if(re.search(pdeath, line)): pname = re.search(pdeath, line).group(1) - mycursor = await get_cursor() + botsql = bot.get_cog('BotSQL') + mycursor = await botsql.get_cursor() sql = """UPDATE players SET deaths = deaths + 1 WHERE user = '%s'""" % (pname) mycursor.execute(sql) - mydb.commit() + await botsql.botmydb() mycursor.close() await lchannel.send(':skull: **' + pname + '** just died!') if(re.search(pevent, line)): eventID = re.search(pevent, line).group(1) - mycursor = await get_cursor() + botsql = bot.get_cog('BotSQL') + mycursor = await botsql.get_cursor() sql = """SELECT type, smessage, image FROM events WHERE type = '%s' LIMIT 1""" % (eventID) mycursor.execute(sql) Info = mycursor.fetchall() @@ -405,7 +186,8 @@ async def mainloop(file): if(re.search(pjoin, line)): logJoin = re.search(pjoin, line).group(1) logID = re.search(pjoin, line).group(2) - mycursor = await get_cursor() + botsql = bot.get_cog('BotSQL') + mycursor = await botsql.get_cursor() sql = """SELECT id, ingame FROM players WHERE user = '%s'""" % (logJoin) mycursor.execute(sql) Info = mycursor.fetchall() @@ -416,29 +198,30 @@ async def mainloop(file): InGame = 1 sql = """INSERT INTO players (user, valid, startdate, jointime, ingame) VALUES ('%s', '%s', '%s', '%s', '%s')""" % (logJoin, logID, StartDate, JoinTime, InGame) mycursor.execute(sql) - mydb.commit() + await botsql.botmydb() await lchannel.send(':airplane_arriving: New player **' + logJoin + '** has joined the party!') else: Info=Info[0] if Info[1] == 1: sql = """UPDATE players SET valid = '%s' WHERE id = '%s'""" % (logID, Info[0]) mycursor.execute(sql) - mydb.commit() + await botsql.botmydb() else: JoinTime = int(time.time()) InGame = 1 sql = """UPDATE players SET valid = '%s', jointime = '%s', ingame = '%s' WHERE user = '%s'""" % (logID, JoinTime, InGame, logJoin) mycursor.execute(sql) - mydb.commit() + await botsql.botmydb() await lchannel.send(':airplane_arriving: **' + logJoin + '** has joined the party!') sql2 = """INSERT INTO serverstats (date, timestamp, users) VALUES ('%s', '%s', '%s')""" % (await timenow(), int(time.time()), await serverstatsupdate()) mycursor.execute(sql2) - mydb.commit() + await botsql.botmydb() mycursor.close() # Announcing when a player leaves the server, updates db with playtime if(re.search(pquit, line)): logquit = re.search(pquit, line).group(1) - mycursor = await get_cursor() + botsql = bot.get_cog('BotSQL') + mycursor = await botsql.get_cursor() sql = """SELECT id, user, jointime, playtime FROM players WHERE valid = '%s'""" % (logquit) mycursor.execute(sql) Info = mycursor.fetchall() @@ -451,16 +234,17 @@ async def mainloop(file): InGame = 0 sql = """UPDATE players SET playtime = '%s', ingame = '%s' WHERE id = '%s'""" % (Ptime, InGame, Info[0]) mycursor.execute(sql) - mydb.commit() + await botsql.botmydb() await lchannel.send(':airplane_departure: **' + Info[1] + '** has left the party! Online for: ' + ponline + '') sql2 = """INSERT INTO serverstats (date, timestamp, users) VALUES ('%s', '%s', '%s')""" % (await timenow(), int(time.time()), await serverstatsupdate()) mycursor.execute(sql2) - mydb.commit() + await botsql.botmydb() mycursor.close() # Annocing that a boss location was found if(re.search(pfind, line)): newitem = re.search(pfind, line).group(1) - mycursor = await get_cursor() + botsql = bot.get_cog('BotSQL') + mycursor = await botsql.get_cursor() sql = """SELECT type, smessage, image FROM events WHERE type = '%s' LIMIT 1""" % (newitem) mycursor.execute(sql) Info = mycursor.fetchall() @@ -475,15 +259,17 @@ async def mainloop(file): if config.EXSERVERINFO == True: if(re.search(ssaved1, line)): save1 = re.search(ssaved1, line).group(1) - mycursor = await get_cursor() + botsql = bot.get_cog('BotSQL') + mycursor = await botsql.get_cursor() sql = """INSERT INTO exstats (savezdos, timestamp) VALUES ('%s', '%s')""" % (save1, int(time.time())) mycursor.execute(sql) - mydb.commit() + await botsql.botmydb() mycursor.close() # Getting time it took for the save if(re.search(ssaved2, line)): save2 = re.search(ssaved2, line).group(1) - mycursor = await get_cursor() + botsql = bot.get_cog('BotSQL') + mycursor = await botsql.get_cursor() tlookup = int(time.time()) - 60 sql = """SELECT id FROM exstats WHERE savesec is null AND timestamp BETWEEN '%s' AND '%s' LIMIT 1""" % (tlookup, int(time.time())) mycursor.execute(sql) @@ -496,7 +282,7 @@ async def mainloop(file): else: sql = """UPDATE exstats SET savesec = '%s' WHERE id = '%s'""" % (save2, Info[0]) mycursor.execute(sql) - mydb.commit() + await botsql.botmydb() else: print(Fore.RED + 'ERROR: Could not find save zdos info' + Style.RESET_ALL) if config.USEDEBUGCHAN == True: @@ -508,7 +294,8 @@ async def mainloop(file): if(re.search(sversion, line)): serversion = re.search(sversion, line).group(1) vplversion = re.search(sversion, line).group(2) - mycursor = await get_cursor() + botsql = bot.get_cog('BotSQL') + mycursor = await botsql.get_cursor() sql = """SELECT id, serverversion, plusversion FROM exstats WHERE id = 1""" mycursor.execute(sql) Info = mycursor.fetchall() @@ -536,15 +323,16 @@ async def mainloop(file): if vcount == 1: sql = """UPDATE exstats SET %s WHERE id = '%s'""" % (sqlv, Info[0]) mycursor.execute(sql) - mydb.commit() + await botsql.botmydb() mycursor.close() # Getting and storing game day in db (only reported in log when doing a sleep) reports day of sleep not day after waking up if(re.search(gdays, line)): gamedays = re.search(gdays, line).group(1) - mycursor = await get_cursor() + botsql = bot.get_cog('BotSQL') + mycursor = await botsql.get_cursor() sql = """INSERT INTO exstats (gameday, timestamp) VALUES ('%s', '%s')""" % (gamedays, int(time.time())) mycursor.execute(sql) - mydb.commit() + await botsql.botmydb() await lchannel.send('**INFO:** Server reported in game day as: ' + gamedays + '') mycursor.close() await asyncio.sleep(0.2) @@ -591,10 +379,11 @@ async def serveronline(): await channel.edit(name=f"{emoji.emojize(':cross_mark:')} Server Offline") if sonline == 1: sonline = 0 - mycursor = await get_cursor() + botsql = self.bot.get_cog('BotSQL') + mycursor = await botsql.get_cursor() sql2 = """INSERT INTO serverstats (date, timestamp, users) VALUES ('%s', '%s', '%s')""" % (await timenow(), int(time.time()), sonline) mycursor.execute(sql2) - mydb.commit() + await botsql.botmydb() mycursor.close() print(Fore.RED + await timenow(), e, 'from A2S, retrying (60s)...' + Style.RESET_ALL) if config.USEDEBUGCHAN == True: diff --git a/code/utils/botsql.py b/code/utils/botsql.py new file mode 100644 index 0000000..5df39c6 --- /dev/null +++ b/code/utils/botsql.py @@ -0,0 +1,58 @@ +import mysql.connector, config, discord, asyncio +from discord.ext import commands +from colorama import Fore, Style, init +from config import SQL_HOST as MYhost +from config import SQL_PORT as MYport +from config import SQL_USER as MYuser +from config import SQL_PASS as MYpass +from config import SQL_DATABASE as MYbase + +# Connect to MYSQL + +class BotSQL(commands.Cog): + def __init__(self, bot): + self.bot = bot + + async def mydbconnect(self): + global mydb + mydb = mysql.connector.connect( + host=MYhost, + user=MYuser, + password=MYpass, + database=MYbase, + port=MYport, + ) + bugchan = self.bot.get_channel(config.BUGCHANNEL_ID) + try: + if mydb.is_connected(): + db_Info = mydb.get_server_info() + print(Fore.GREEN + "Connected to MySQL database... MySQL Server version ", db_Info + Style.RESET_ALL) + if config.USEDEBUGCHAN == True: + buginfo = discord.Embed(title=":white_check_mark: **INFO** :white_check_mark:", description="Connected to MySQL database... MySQL Server version " + db_Info, color=0x7EFF00) + buginfo.set_author(name=config.SERVER_NAME) + await bugchan.send(embed=buginfo) + except mysql.connector.Error as err: + print(Fore.RED + err + 'From MySQL database' + Style.RESET_ALL) + if config.USEDEBUGCHAN == True: + bugerror = discord.Embed(title=":sos: **ERROR** :sos:", description="{} From MySQL database".format(err), color=0xFF001E) + bugerror.set_author(name=config.SERVER_NAME) + await bugchan.send(embed=bugerror) + + async def get_cursor(self): + try: + mydb.ping(reconnect=True, attempts=3, delay=5) + except mysql.connector.Error as err: + await mydbconnect() + print(Fore.RED + "Connection to MySQL database went away... Reconnecting " + Style.RESET_ALL) + if config.USEDEBUGCHAN == True: + bugchan = bot.get_channel(dbchanID) + bugerror = discord.Embed(title=":sos: **ERROR** :sos:", description="Connection to MySQL database went away... Reconnecting", color=0xFF001E) + bugerror.set_author(name=config.SERVER_NAME) + await bugchan.send(embed=bugerror) + return mydb.cursor() + + async def botmydb(self): + mydb.commit() + +def setup(bot): + bot.add_cog(BotSQL(bot)) |