From 14e29dd15037c9368081bfb0a9ac5c689c289eb4 Mon Sep 17 00:00:00 2001 From: stolenvw Date: Fri, 2 Apr 2021 18:13:17 -0400 Subject: Valheim plus discord bot --- code/config.py | 46 ++++ code/img/Boar.png | Bin 0 -> 4381 bytes code/img/Bonemass.png | Bin 0 -> 6196 bytes code/img/Drake.png | Bin 0 -> 4540 bytes code/img/Draugr.png | Bin 0 -> 10559 bytes code/img/Eikthyr.png | Bin 0 -> 7332 bytes code/img/Fuling.png | Bin 0 -> 3126 bytes code/img/Greydwarf.png | Bin 0 -> 2888 bytes code/img/Moder.png | Bin 0 -> 5706 bytes code/img/Ooze.png | Bin 0 -> 8982 bytes code/img/The_Elder.png | Bin 0 -> 5801 bytes code/img/Yagluth.png | Bin 0 -> 7304 bytes code/img/skeleton.png | Bin 0 -> 4630 bytes code/img/surtling.png | Bin 0 -> 4645 bytes code/img/temp.png | Bin 0 -> 4399 bytes code/img/troll.png | Bin 0 -> 6122 bytes code/img/wolf.png | Bin 0 -> 5146 bytes code/plusbot.py | 608 +++++++++++++++++++++++++++++++++++++++++++++++++ example/example.png | Bin 0 -> 312031 bytes requirements.txt | 7 + table_info.sql | 80 +++++++ 21 files changed, 741 insertions(+) create mode 100644 code/config.py create mode 100644 code/img/Boar.png create mode 100644 code/img/Bonemass.png create mode 100644 code/img/Drake.png create mode 100644 code/img/Draugr.png create mode 100644 code/img/Eikthyr.png create mode 100644 code/img/Fuling.png create mode 100644 code/img/Greydwarf.png create mode 100644 code/img/Moder.png create mode 100644 code/img/Ooze.png create mode 100644 code/img/The_Elder.png create mode 100644 code/img/Yagluth.png create mode 100644 code/img/skeleton.png create mode 100644 code/img/surtling.png create mode 100644 code/img/temp.png create mode 100644 code/img/troll.png create mode 100644 code/img/wolf.png create mode 100644 code/plusbot.py create mode 100644 example/example.png create mode 100644 requirements.txt create mode 100644 table_info.sql diff --git a/code/config.py b/code/config.py new file mode 100644 index 0000000..ea7abdc --- /dev/null +++ b/code/config.py @@ -0,0 +1,46 @@ +# Still working on discord-side triggering + +# 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 +file = '/var/log/valheim.log' + +BOT_TOKEN = "" + +# Change ? to command prefix you want to use +BOT_PREFIX = "!" + +# Server ip and port for A2S port needs to be +1 of the port number used in -port +SERVER_ADDRESS = ("0.0.0.0",2457) + +# Shows up in embeds for stats report +SERVER_NAME = "Server Name" + +# LOGCHAIN - where the bot outputs death and random mob events +LOGCHAN_ID = 000000000000000 + +# use a locked VC channel to report player count, if not, set as False +USEVCSTATS = True + +# VCHANNEL - where the bot shows server ticker, must be voice channel +VCHANNEL_ID = 000000000000000000 + +# MYSQL server info +SQL_HOST = 'localhost' +SQL_PORT = '3306' +SQL_USER = 'username' +SQL_PASS = 'password' +SQL_DATABASE = 'database' + +# EXSERVERINFO - used to enable extra server data info gathering from logs ***Must add the extra server info table in mysql*** +EXSERVERINFO = True + +# World file location used for showing file size. +WORLDSIZE = True +worldfile = '/home/user/valheim/.config/unity3d/IronGate/Valheim/worlds/world.db.old' + +# Enable sending debug info to a channel +USEDEBUGCHAN = True + +# BUGCHANNEL - where the bot shows debug info +BUGCHANNEL_ID = 7293481670000121 diff --git a/code/img/Boar.png b/code/img/Boar.png new file mode 100644 index 0000000..338e337 Binary files /dev/null and b/code/img/Boar.png differ diff --git a/code/img/Bonemass.png b/code/img/Bonemass.png new file mode 100644 index 0000000..e2d62f6 Binary files /dev/null and b/code/img/Bonemass.png differ diff --git a/code/img/Drake.png b/code/img/Drake.png new file mode 100644 index 0000000..252037a Binary files /dev/null and b/code/img/Drake.png differ diff --git a/code/img/Draugr.png b/code/img/Draugr.png new file mode 100644 index 0000000..5464d01 Binary files /dev/null and b/code/img/Draugr.png differ diff --git a/code/img/Eikthyr.png b/code/img/Eikthyr.png new file mode 100644 index 0000000..86f4909 Binary files /dev/null and b/code/img/Eikthyr.png differ diff --git a/code/img/Fuling.png b/code/img/Fuling.png new file mode 100644 index 0000000..ea201d9 Binary files /dev/null and b/code/img/Fuling.png differ diff --git a/code/img/Greydwarf.png b/code/img/Greydwarf.png new file mode 100644 index 0000000..040a583 Binary files /dev/null and b/code/img/Greydwarf.png differ diff --git a/code/img/Moder.png b/code/img/Moder.png new file mode 100644 index 0000000..086db8a Binary files /dev/null and b/code/img/Moder.png differ diff --git a/code/img/Ooze.png b/code/img/Ooze.png new file mode 100644 index 0000000..4bdae51 Binary files /dev/null and b/code/img/Ooze.png differ diff --git a/code/img/The_Elder.png b/code/img/The_Elder.png new file mode 100644 index 0000000..6a722f7 Binary files /dev/null and b/code/img/The_Elder.png differ diff --git a/code/img/Yagluth.png b/code/img/Yagluth.png new file mode 100644 index 0000000..d36b290 Binary files /dev/null and b/code/img/Yagluth.png differ diff --git a/code/img/skeleton.png b/code/img/skeleton.png new file mode 100644 index 0000000..c6f44fa Binary files /dev/null and b/code/img/skeleton.png differ diff --git a/code/img/surtling.png b/code/img/surtling.png new file mode 100644 index 0000000..5b94211 Binary files /dev/null and b/code/img/surtling.png differ diff --git a/code/img/temp.png b/code/img/temp.png new file mode 100644 index 0000000..8c40edc Binary files /dev/null and b/code/img/temp.png differ diff --git a/code/img/troll.png b/code/img/troll.png new file mode 100644 index 0000000..ee2fdb8 Binary files /dev/null and b/code/img/troll.png differ diff --git a/code/img/wolf.png b/code/img/wolf.png new file mode 100644 index 0000000..9a5f612 Binary files /dev/null and b/code/img/wolf.png differ diff --git a/code/plusbot.py b/code/plusbot.py new file mode 100644 index 0000000..cf026b7 --- /dev/null +++ b/code/plusbot.py @@ -0,0 +1,608 @@ +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 #### + +#Color init +colorama.init() + +pdeath = '^\[Info\s+:\s+Unity Log\].*? Got character ZDOID from (\w+) : 0:0$' +pevent = '^\[Info\s+:\s+Unity Log\].*? Random event set:(\w+)$' +pjoin = '^\[Info\s+:\s+Unity Log\].*? Got character ZDOID from (\w+) : ([-0-9]*:[-0-9]*)$' +pquit = '^\[Info\s+:\s+Unity Log\].*? Destroying abandoned non persistent zdo ([-0-9]*:[0-9]*) owner [-0-9]*$' +pfind = '^\[Info\s+:\s+Unity Log\].*? Found location of type (\w+)$' +# Extra Server Info +ssaved1 = '.*? Saved ([0-9]+) zdos$' +ssaved2 = '.*? World saved \( ([0-9]+\.[0-9]+)ms \)$' +sversion = '^\[Info\s+:\s+Unity Log\].*? Valheim version:([\.0-9]+)@([\.0-9]+)$' +gdays = '^\[Info\s+:\s+Unity Log\].*? 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) +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 + Style.RESET_ALL) + if config.USEDEBUGCHAN == True: + bugerror = discord.Embed(title=":sos: **ERROR** :sos:", description=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() + +# Method for catching SIGINT, cleaner output for restarting bot +def signal_handler(signal, frame): + os._exit(0) + +signal.signal(signal.SIGINT, signal_handler) + +async def timenow(): + now = datetime.now() + gettime = now.strftime("%m/%d/%Y %H:%M:%S") + return gettime + +async def convert(n): + return str(timedelta(seconds = n)) + +@bot.event +async def on_ready(): + print(Fore.GREEN + f'Bot connected as {bot.user} :)' + Style.RESET_ALL) + print('Command prefix: %s' % config.BOT_PREFIX) + print('Log channel: #%s' % (bot.get_channel(lchanID))) + if config.USEVCSTATS == True: + print('VoIP channel: %d' % (chanID)) + if config.USEDEBUGCHAN == True: + print('Debug channel: #%s' % (bot.get_channel(dbchanID))) + bugchan = bot.get_channel(dbchanID) + buginfo = discord.Embed(title=":white_check_mark: **INFO** :white_check_mark:", color=0x7EFF00) + buginfo.set_author(name=server_name) + buginfo.add_field(name="Bot connected as:", + value="{}".format(bot.user), + inline=False) + buginfo.add_field(name="Command prefix:", + value="{}".format(config.BOT_PREFIX), + inline=False) + buginfo.add_field(name="Log channel:", + value="#{}".format(bot.get_channel(lchanID)), + inline=False) + if config.USEVCSTATS == True: + buginfo.add_field(name="VoIP channel:", + value="#{}".format(chanID), + inline=False) + buginfo.add_field(name="Debug channel", + value="#{}".format(bot.get_channel(dbchanID)), + inline=False) + await bugchan.send(embed=buginfo) + 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 ".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 ".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 ".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 ".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') + + #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 ""`'.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() + lchannel = bot.get_channel(lchanID) + bugchan = bot.get_channel(dbchanID) + print('Main loop: init') + if config.USEDEBUGCHAN == True: + buginfo = discord.Embed(title=":white_check_mark: **INFO** :white_check_mark:", description="Main Loop Started", color=0x7EFF00) + buginfo.set_author(name=server_name) + await bugchan.send(embed=buginfo) + try: + testfile = open(file) + testfile.close() + while not bot.is_closed(): + with open(file, encoding='utf-8', mode='r') as f: + f.seek(0,2) + while True: + try: + line = f.readline() + except UnicodeDecodeError: + print('Got an invalid utf-8 character! Skipping moving to next line') + if config.USEDEBUGCHAN == True: + bugerror = discord.Embed(title=":sos: **ERROR** :sos:", description="Got an invalid utf-8 character! Skipping.. moving to next line", color=0xFF001E) + bugerror.set_author(name=server_name) + await bugchan.send(embed=bugerror) + else: + if(re.search(pdeath, line)): + pname = re.search(pdeath, line).group(1) + mycursor = await get_cursor() + sql = """UPDATE players SET deaths = deaths + 1 WHERE user = '%s'""" % (pname) + mycursor.execute(sql) + mydb.commit() + 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() + sql = """SELECT type, smessage, image FROM events WHERE type = '%s' LIMIT 1""" % (eventID) + mycursor.execute(sql) + Info = mycursor.fetchall() + Info=Info[0] + image = discord.File('img/' + Info[2], filename=Info[2]) + embed = discord.Embed(title=Info[0], colour=discord.Colour(0xb6000e), description="*" + Info[1] + "*") + embed.set_thumbnail(url='attachment://' + Info[2]) + embed.set_author(name="📢 Random Mob Event") + await lchannel.send(file=image, embed=embed) + mycursor.close() + if(re.search(pjoin, line)): + logJoin = re.search(pjoin, line).group(1) + logID = re.search(pjoin, line).group(2) + mycursor = await get_cursor() + sql = """SELECT id, ingame FROM players WHERE user = '%s'""" % (logJoin) + mycursor.execute(sql) + Info = mycursor.fetchall() + row_count = mycursor.rowcount + if row_count == 0: + StartDate = await timenow() + JoinTime = int(time.time()) + 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 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() + 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 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() + 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() + sql = """SELECT id, user, jointime, playtime FROM players WHERE valid = '%s'""" % (logquit) + mycursor.execute(sql) + Info = mycursor.fetchall() + row_count = mycursor.rowcount + if row_count == 1: + Info=Info[0] + EndTime = int(time.time()) + Ptime = EndTime - Info[2] + Info[3] + ponline = await convert(EndTime - Info[2]) + InGame = 0 + sql = """UPDATE players SET playtime = '%s', ingame = '%s' WHERE id = '%s'""" % (Ptime, InGame, Info[0]) + mycursor.execute(sql) + mydb.commit() + 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() + 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() + sql = """SELECT type, smessage, image FROM events WHERE type = '%s' LIMIT 1""" % (newitem) + mycursor.execute(sql) + Info = mycursor.fetchall() + Info=Info[0] + image = discord.File('img/' + Info[2], filename=Info[2]) + embed = discord.Embed(title=Info[0], colour=discord.Colour(0x77ac18)) + embed.set_thumbnail(url='attachment://' + Info[2]) + embed.set_author(name="📢 Location Found") + await lchannel.send(file=image, embed=embed) + mycursor.close() + # Extra Server Info DB After this point + if config.EXSERVERINFO == True: + if(re.search(ssaved1, line)): + save1 = re.search(ssaved1, line).group(1) + mycursor = await get_cursor() + sql = """INSERT INTO exstats (savezdos, timestamp) VALUES ('%s', '%s')""" % (save1, int(time.time())) + mycursor.execute(sql) + mydb.commit() + 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() + 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) + Info = mycursor.fetchall() + row_count = mycursor.rowcount + if row_count == 1: + Info=Info[0] + if config.WORLDSIZE == True: + sql ="""UPDATE exstats SET savesec = '%s', worldsize = '%s' WHERE id = '%s'""" % (save2, '{:,.2f}'.format(os.path.getsize(config.worldfile)/float(1<<20)), Info[0]) + else: + sql = """UPDATE exstats SET savesec = '%s' WHERE id = '%s'""" % (save2, Info[0]) + mycursor.execute(sql) + mydb.commit() + else: + print(Fore.RED + 'ERROR: Could not find save zdos info' + Style.RESET_ALL) + if config.USEDEBUGCHAN == True: + bugerror = discord.Embed(title=":sos: **ERROR** :sos:", description="Could not find save zdos info", color=0xFF001E) + bugerror.set_author(name=server_name) + await bugchan.send(embed=bugerror) + mycursor.close() + # Getting and storing server version and Valheim Plus number in db, and announcing in channel if version was updated + if(re.search(sversion, line)): + serversion = re.search(sversion, line).group(1) + vplversion = re.search(sversion, line).group(2) + mycursor = await get_cursor() + sql = """SELECT id, serverversion, plusversion FROM exstats WHERE id = 1""" + mycursor.execute(sql) + Info = mycursor.fetchall() + row_count = mycursor.rowcount + if row_count == 0: + print(Fore.RED + 'ERROR: Extra server info is set, but missing database table/info' + Style.RESET_ALL) + if config.USEDEBUGCHAN == True: + bugerror = discord.Embed(title=":sos: **ERROR** :sos:", description="Extra server info is set, but missing database table/info", color=0xFF001E) + bugerror.set_author(name=server_name) + await bugchan.send(embed=bugerror) + else: + Info=Info[0] + vcount = 0 + if serversion != Info[1]: + sqlv = 'serverversion = "' + serversion + '"' + vcount = 1 + await lchannel.send('**INFO:** Server has been updated to version: ' + serversion + '') + if vplversion != Info[2]: + if vcount == 1: + sqlv = sqlv + ', plusversion = "' + vplversion + '"' + else: + sqlv = 'plusversion = "' + vplversion + '"' + vcount = 1 + await lchannel.send('**INFO:** Valheim Plus has been updated to version: ' + vplversion + '') + if vcount == 1: + sql = """UPDATE exstats SET %s WHERE id = '%s'""" % (sqlv, Info[0]) + mycursor.execute(sql) + mydb.commit() + 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() + sql = """INSERT INTO exstats (gameday, timestamp) VALUES ('%s', '%s')""" % (gamedays, int(time.time())) + mycursor.execute(sql) + mydb.commit() + await lchannel.send('**INFO:** Server reported in game day as: ' + gamedays + '') + mycursor.close() + await asyncio.sleep(0.2) + except IOError: + print('No valid log found, event reports disabled. Please check config.py') + print('To generate server logs, run server with -logfile launch flag') + print('Or permission error getting world file size') + if config.USEDEBUGCHAN == True: + bugerror = discord.Embed(title=":sos: **ERROR** :sos:", description="No valid log found, event reports disabled. Please check config.py \n To generate server logs, run server with -logfile launch flag \n Or permission error getting world file size", color=0xFF001E) + bugerror.set_author(name=server_name) + await bugchan.send(embed=bugerror) + +async def serverstatsupdate(): + try: + if a2s.info(config.SERVER_ADDRESS): + channel = bot.get_channel(chanID) + oplayers = a2s.info(config.SERVER_ADDRESS).player_count + if config.USEVCSTATS == True: + await channel.edit(name=f"{emoji.emojize(':house:')} In-Game: {oplayers}" +" / 10") + except Exception as e: + print(Fore.RED + await timenow(), e, 'from A2S' + Style.RESET_ALL) + channel = bot.get_channel(chanID) + oplayers = 0 + if config.USEVCSTATS == True: + await channel.edit(name=f"{emoji.emojize(':cross_mark:')} Server Offline") + else: + return oplayers + +async def serveronline(): + global sonline + await bot.wait_until_ready() + while not bot.is_closed(): + try: + if a2s.info(config.SERVER_ADDRESS): + meonline = a2s.info(config.SERVER_ADDRESS).player_count + if sonline == 0: + sonline = 1 + if config.USEVCSTATS == True: + channel = bot.get_channel(chanID) + await channel.edit(name=f"{emoji.emojize(':house:')} Server OnLine") + except Exception as e: + if config.USEVCSTATS == True: + channel = bot.get_channel(chanID) + await channel.edit(name=f"{emoji.emojize(':cross_mark:')} Server Offline") + if sonline == 1: + sonline = 0 + mycursor = await get_cursor() + sql2 = """INSERT INTO serverstats (date, timestamp, users) VALUES ('%s', '%s', '%s')""" % (await timenow(), int(time.time()), sonline) + mycursor.execute(sql2) + mydb.commit() + mycursor.close() + print(Fore.RED + await timenow(), e, 'from A2S, retrying (60s)...' + Style.RESET_ALL) + if config.USEDEBUGCHAN == True: + bugchan = bot.get_channel(dbchanID) + bugerror = discord.Embed(title=":sos: **ERROR** :sos:", description=e, color=0xFF001E) + bugerror.set_author(name=server_name) + await bugchan.send(embed=bugerror) + await asyncio.sleep(60) + +bot.loop.create_task(mainloop(file)) +bot.run(config.BOT_TOKEN) diff --git a/example/example.png b/example/example.png new file mode 100644 index 0000000..b2d291a Binary files /dev/null and b/example/example.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..258422e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +discord.py==1.6.0 +colorama==0.4.4 +matplotlib==3.3.4 +emoji==1.2.0 +pandas==1.2.2 +discord==1.0.1 +python-a2s==1.3.0 \ No newline at end of file diff --git a/table_info.sql b/table_info.sql new file mode 100644 index 0000000..c5544af --- /dev/null +++ b/table_info.sql @@ -0,0 +1,80 @@ +-- +-- Table structure for table `events` +-- + +CREATE TABLE `events` ( + `id` int NOT NULL AUTO_INCREMENT, + `type` text NOT NULL, + `smessage` text NOT NULL, + `image` text NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `type` (`type`(7)) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- +-- Data for table `events` +-- + + +INSERT INTO `events` (`id`, `type`, `smessage`, `image`) VALUES +(1, 'Skeletons', 'Skeleton Surprise', 'skeleton.png'), +(2, 'Blobs', '..', 'Ooze.png'), +(3, 'Foresttrolls', 'The ground is shaking', 'troll.png'), +(4, 'Wolves', 'You are being hunted', 'wolf.png'), +(5, 'Surtlings', 'There\'s a smell of sulfur in the air', 'surtling.png'), +(6, 'Eikthyrnir', 'Meadows', 'Eikthyr.png'), +(7, 'GDKing', 'Black Forest', 'The_Elder.png'), +(8, 'Bonemass', 'Swamp', 'Bonemass.png'), +(9, 'Dragonqueen', 'Mountain', 'Moder.png'), +(10, 'GoblinKing', 'Plains', 'Yagluth.png'), +(11, 'army_eikthyr', 'Eikthyr rallies the creatures of the forest', 'Boar.png'), +(12, 'army_theelder', 'The forest is moving...', 'Greydwarf.png'), +(13, 'army_bonemass', 'A foul smell from the swamp', 'Draugr.png'), +(14, 'army_moder', 'A cold wind blows from the mountains', 'Drake.png'), +(15, 'army_goblin', 'The horde is attacking', 'Fuling.png'); + +-- +-- Table structure for table `exstats` (Optinal Table only needed if enabling Extra Server Info in Config) +-- + +CREATE TABLE `exstats` ( + `id` int NOT NULL AUTO_INCREMENT, + `savezdos` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL, + `savesec` varchar(10) DEFAULT NULL, + `worldsize` varchar(10) DEFAULT NULL, + `serverversion` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL, + `plusversion` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL, + `gameday` int DEFAULT NULL, + `timestamp` bigint DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- +-- Table structure for table `players` +-- + +CREATE TABLE `players` ( + `id` int NOT NULL AUTO_INCREMENT, + `user` varchar(100) NOT NULL, + `deaths` int NOT NULL DEFAULT '0', + `valid` varchar(50) DEFAULT NULL, + `startdate` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL, + `playtime` bigint DEFAULT '0', + `jointime` bigint DEFAULT NULL, + `ingame` int NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `users` (`user`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- +-- Table structure for table `serverstats` +-- + +CREATE TABLE `serverstats` ( + `id` int NOT NULL AUTO_INCREMENT, + `date` varchar(20) DEFAULT NULL, + `timestamp` bigint DEFAULT NULL, + `users` int NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `timestamp` (`timestamp`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- cgit v1.2.3