summaryrefslogtreecommitdiff
path: root/code
diff options
context:
space:
mode:
authorstolenvw <stolenvw@hotmail.com>2021-04-10 15:28:44 -0400
committerstolenvw <stolenvw@hotmail.com>2021-04-10 15:28:44 -0400
commit7c58f5a97abf9ac5e7353940b0ddc50a585d9066 (patch)
tree7c58c1b3a856822495f95701f2417b0ec8c9ab2b /code
parent10003220489fe8493021e35f7243a4f63525f376 (diff)
Major rewrite, Added optional commands for displaying V+ config settings
Diffstat (limited to 'code')
-rw-r--r--code/botcmds/helpcmds.py134
-rw-r--r--code/botcmds/maincmd.py258
-rw-r--r--code/config.py4
-rw-r--r--code/plusbot.py397
-rw-r--r--code/utils/botsql.py58
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))