328 lines
16 KiB
Python
328 lines
16 KiB
Python
import discord, logging, re, sys, traceback, asyncio, datetime, os, time
|
|
from minecraft_manager.models import Application, Player
|
|
from minecraft_manager.api import api
|
|
from django.contrib.auth.models import User
|
|
from django.conf import settings
|
|
from django.db import close_old_connections
|
|
from threading import Thread
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Discord(discord.Client):
|
|
discord_game = 'MCM'
|
|
prefix = getattr(settings, 'DISCORD_BOT_PREFIX', '!')
|
|
auth_roles = getattr(settings, 'DISCORD_BOT_ROLES', [])
|
|
error_users = getattr(settings, 'DISCORD_ERROR_USERS', [])
|
|
token = None
|
|
|
|
def __init__(self, token, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.token = token
|
|
|
|
@asyncio.coroutine
|
|
def on_ready(self):
|
|
print('Logged in as')
|
|
print(self.user.name)
|
|
print(self.user.id)
|
|
print(discord.__version__)
|
|
print('Voice Loaded: {0}'.format(discord.opus.is_loaded()))
|
|
print('OAuth URL: https://discordapp.com/api/oauth2/authorize?client_id={0}&permissions=0&scope=bot'.format(self.user.id))
|
|
print('------')
|
|
logger.info('Logged in as {0} ({1}) with discord.py v{2}'.format(self.user.name, self.user.id, discord.__version__))
|
|
yield from self.change_presence(game=discord.Game(name=self.discord_game))
|
|
|
|
@asyncio.coroutine
|
|
def discord_message(self, channel, message):
|
|
if isinstance(message, discord.Embed):
|
|
for idx, field in enumerate(message.fields):
|
|
if not field.value:
|
|
message.set_field_at(idx, name=field.name, value="N/A")
|
|
yield from self.send_message(channel, embed=message)
|
|
else:
|
|
yield from self.send_message(channel, message)
|
|
|
|
@asyncio.coroutine
|
|
def on_message(self, message):
|
|
|
|
# IGNORE DM AND BOTS
|
|
if message.author.bot is True or message.channel.is_private is True:
|
|
return
|
|
|
|
member_roles = [role.id for role in message.author.roles]
|
|
|
|
# IF MEMBER IS NOT AUTHORIZED, IGNORE
|
|
if not any(role in self.auth_roles for role in member_roles):
|
|
return
|
|
|
|
# FIX STALE DB CONNECTIONS
|
|
close_old_connections()
|
|
|
|
# HELP
|
|
match = re.match("[{0}]help$".format(self.prefix), message.content)
|
|
if match:
|
|
embed = discord.Embed(colour=discord.Colour(0x417505))
|
|
embed.set_thumbnail(url="https://cdn.discordapp.com/avatars/454457830918062081/b5792489bc43d9e17b8f657880a17dd4.png")
|
|
embed.add_field(name="Minecraft Manager Help", value="-----------------------------")
|
|
embed.add_field(name="{}[app ]search <username>".format(self.prefix), value="Search for applications by partial or exact username.")
|
|
embed.add_field(name="{}[app ]info <app ID>".format(self.prefix), value="Get detailed information about a specific application.")
|
|
embed.add_field(name="{}[app ]accept|deny <app ID>".format(self.prefix), value="Take action on an application.")
|
|
embed.add_field(name="{}demote <username>".format(self.prefix), value="Demote a player to the role given to accepted applications.")
|
|
embed.add_field(name="{}compare".format(self.prefix), value="Compare Discord users to the Whitelist.")
|
|
yield from self.discord_message(message.channel, embed)
|
|
# APP COMMANDS WITH APP ID
|
|
match = re.match("[{0}](?:app )?(i|info|a|accept|d|deny) (\d+)$".format(self.prefix), message.content)
|
|
if match:
|
|
if match.group(1) and match.group(2):
|
|
action = match.group(1)[:1]
|
|
action_display = "accept" if action == "a" else "deny" if action == "d" else ""
|
|
yield from self.discord_message(message.channel, "{0} App ID **{1}**".format(
|
|
"Retrieving info for" if action == "i" else "{0}ing".format(action_display.capitalize()),
|
|
match.group(2)))
|
|
application = None
|
|
try:
|
|
application = Application.objects.get(id=match.group(2))
|
|
except:
|
|
yield from self.discord_message(message.channel, "An Application with that ID doesn't exist.")
|
|
return
|
|
if action == "i":
|
|
# Info
|
|
msg = self.build_info(application)
|
|
else:
|
|
# Action
|
|
accept = True if action == "a" else False
|
|
if not application.accepted:
|
|
application.accepted = accept
|
|
application.save()
|
|
msg = "App ID **{0}** was successfully {1}.".format(match.group(2), "accepted" if accept else "denied")
|
|
api.plugin("accept" if accept else "deny", application.username)
|
|
else:
|
|
msg = "App ID **{0}** was already {1}.".format(match.group(2), "accepted" if application.accepted else "denied")
|
|
yield from self.discord_message(message.channel, msg)
|
|
return
|
|
# APP INFO WITH PARTIAL NAME SEARCH
|
|
match = re.match("[{0}](?:app )?(?:search|info) (\S+)?$".format(self.prefix), message.content)
|
|
if match:
|
|
search = match.group(1)
|
|
yield from self.discord_message(message.channel, "Searching for applications whose username contains '{0}'".format(search))
|
|
applications = Application.objects.filter(username__icontains=search)[:10]
|
|
count = Application.objects.filter(username__icontains=search).count()
|
|
if count > 0:
|
|
if count == 1:
|
|
info = self.build_info(applications[0])
|
|
else:
|
|
info = "**Found the following applications**"
|
|
for app in applications:
|
|
info += "\n{0} - {1} ({2})".format(app.id, app.username.replace("_", "\\_"), app.status)
|
|
if count > 10:
|
|
info += "\n**This is only 10 applications out of {0} found. Please narrow your search if possible.**".format(
|
|
len(applications))
|
|
else:
|
|
players = Player.objects.filter(username__icontains=search)[:10]
|
|
count = Player.objects.filter(username__icontains=search).count()
|
|
if count > 0:
|
|
if count == 1:
|
|
yield from self.discord_message(message.channel, "No applications matched, however there is a player match.")
|
|
info = self.build_info(players[0].application)
|
|
else:
|
|
info = "No applications matched, however there are player matches."
|
|
for player in players:
|
|
app = player.application
|
|
info += "\n{0} - {1} AKA {2} ({3})".format(app.id, app.username.replace("_", "\\_"), player.username.replace("_", "\\_"), app.status)
|
|
if count > 10:
|
|
info += "\n**This is only 10 players out of {0} found. Please narrow your search if possible.**".format(
|
|
len(players))
|
|
else:
|
|
info = "No applications matched that search."
|
|
yield from self.discord_message(message.channel, info)
|
|
# DEMOTE A PLAYER TO MEMBER
|
|
match = re.match("[{0}]demote (\w+)$".format(self.prefix), message.content)
|
|
if match:
|
|
yield from self.delete_message(message)
|
|
username = match.group(1)
|
|
api.plugin(api.PLUGIN_DEMOTE, username)
|
|
deactivated = ""
|
|
if User.objects.filter(username__iexact=username).exists():
|
|
user = User.objects.get(username__iexact=username)
|
|
user.is_active = False
|
|
user.save()
|
|
deactivated = " and de-activated"
|
|
yield from self.discord_message(message.channel, "{} has been demoted{}.".format(username, deactivated))
|
|
# COMPARE DISCORD USERS TO WHITELIST
|
|
match = re.match("[{0}]compare".format(self.prefix), message.content)
|
|
if match:
|
|
yield from self.delete_message(message)
|
|
yield from self.send_typing(message.channel)
|
|
no_player = []
|
|
no_application = []
|
|
for member in message.server.members:
|
|
if member.bot:
|
|
continue
|
|
name = member.nick if member.nick else member.name
|
|
try:
|
|
Player.objects.get(username__iexact=name)
|
|
except:
|
|
no_player.append(name)
|
|
try:
|
|
Application.objects.get(username__iexact=name)
|
|
except:
|
|
no_player = no_player[:-1]
|
|
no_application.append(name)
|
|
header = "**The following users have an application match, but no player match on the whitelist:**\n"
|
|
yield from self.discord_message(message.author, "{}```{}```".format(header, "\n".join(no_player)))
|
|
header = "**The following users do not have an application or player match on the whitelist:**\n"
|
|
yield from self.discord_message(message.author, "{}```{}```".format(header, "\n".join(no_application)))
|
|
|
|
|
|
def build_info(self, application):
|
|
embed = discord.Embed(colour=discord.Colour(0x417505))
|
|
embed.set_thumbnail(
|
|
url="https://minotar.net/helm/{0}/100.png".format(application.username))
|
|
embed.add_field(name="Application ID", value=application.id)
|
|
embed.add_field(name="Username", value=application.username.replace("_", "\\_"))
|
|
embed.add_field(name="Age", value=application.age)
|
|
embed.add_field(name="Type of Player", value=application.player_type)
|
|
embed.add_field(name="Ever been banned", value=application.ever_banned)
|
|
if application.ever_banned:
|
|
embed.add_field(name="Reason for being banned", value=application.ever_banned_explanation)
|
|
embed.add_field(name="Reference", value=application.reference)
|
|
embed.add_field(name="Read the Rules", value=application.read_rules)
|
|
embed.add_field(name="Date", value=application.date_display)
|
|
embed.add_field(name="Status", value=application.status)
|
|
return embed
|
|
|
|
@asyncio.coroutine
|
|
def on_error(self, event, *args, **kwargs):
|
|
print(sys.exc_info())
|
|
print("Exception raised by " + event)
|
|
error = '{0}\n{1}'.format(sys.exc_info()[1], ''.join(traceback.format_tb(sys.exc_info()[2])))
|
|
logger.error(error)
|
|
for user in self.error_users:
|
|
try:
|
|
user = discord.User(id=user)
|
|
yield from self.discord_message(user, '```python\n{}```'.format(error))
|
|
except:
|
|
pass
|
|
|
|
|
|
def run_bot(self):
|
|
self.run(self.token)
|
|
|
|
|
|
class OreAlert:
|
|
# Options
|
|
log = os.path.join(settings.MINECRAFT_BASE_DIR, 'logs/latest.log')
|
|
purge = 30 # How long until we purge, in minutes
|
|
rotate = 5 # How long without input before we assume the log has rotated
|
|
notify_start = 5 # How many veins found within the above purge minutes to notify
|
|
notify_each = 1 # After the initial alert, how many should be found in addition before more alerts?
|
|
notify_ping = 5 # After the initial alert, how many should be found in addition before more pings?
|
|
|
|
playerList = []
|
|
|
|
class Player:
|
|
|
|
def __init__(self, name, time, prev=[]):
|
|
self.name = name
|
|
self.time = time
|
|
self.prev = prev
|
|
|
|
def compare(self, player):
|
|
if self.name == player.name:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def to_string(self):
|
|
return str(self.name) + ": " + str(self.time) + ", previous: " + self.prev_to_string()
|
|
|
|
def prev_to_string(self):
|
|
ret = ""
|
|
if len(self.prev) > 0:
|
|
for p in self.prev:
|
|
ret += str(p) + " "
|
|
|
|
return ret.rstrip()
|
|
else:
|
|
return ret
|
|
|
|
def minute_interval(self, start, end):
|
|
reverse = False
|
|
if start > end:
|
|
start, end = end, start
|
|
reverse = True
|
|
delta = (end.hour - start.hour) * 60 + end.minute - start.minute + (end.second - start.second) / 60.0
|
|
if reverse:
|
|
delta = 24 * 60 - delta
|
|
return delta
|
|
|
|
def follow(self, filename):
|
|
thefile = open(filename, 'r', encoding='utf-8')
|
|
thefile.seek(0, 2) # Go to the end of the file
|
|
start = datetime.datetime.now()
|
|
end = datetime.datetime.now()
|
|
api.discord_notification('OreAlert has started successfully.')
|
|
while True:
|
|
line = thefile.readline()
|
|
if not line:
|
|
if self.minute_interval(start, end) > self.rotate:
|
|
thefile.close()
|
|
time.sleep(5)
|
|
thefile = open(filename, 'r', encoding='utf-8')
|
|
thefile.seek(0, 2) # Go to the end of the file
|
|
start = datetime.datetime.now()
|
|
end = datetime.datetime.now()
|
|
# api.discord_notification('OreAlert has closed and re-opened the log...hopefully it just rotated.')
|
|
continue
|
|
end = end + datetime.timedelta(milliseconds=100)
|
|
time.sleep(0.1) # Sleep briefly
|
|
continue
|
|
start = datetime.datetime.now()
|
|
end = datetime.datetime.now()
|
|
yield line
|
|
|
|
def run_bot(self):
|
|
cur_line = ""
|
|
try:
|
|
loglines = self.follow(self.log)
|
|
for line in loglines:
|
|
# [00: 03:46] [Server thread / INFO]: [MinecraftManager]: [OreAlert]: Etzelia
|
|
if "MinecraftManager" in line and "OreAlert" in line: # Filter out non-OreAlert log statements
|
|
cur_time = line.split()[0].replace("[", "").replace("]", "")
|
|
if ":" in cur_time: # Make sure we have a time.
|
|
time_array = cur_time.split(":")
|
|
name = line.split()[-1]
|
|
dt = datetime.time(int(time_array[0]), int(time_array[1]), int(time_array[2]))
|
|
p = self.Player(name, dt)
|
|
new_player = True
|
|
for player in self.playerList:
|
|
if p.compare(player):
|
|
new_player = False
|
|
player.prev[:] = [x for x in player.prev if not self.minute_interval(x,
|
|
dt) > self.purge] # First, purge any times older than our configured amount
|
|
player.prev.append(dt) # Add the new time
|
|
if len(player.prev) >= self.notify_start:
|
|
# MCM Alert
|
|
if len(player.prev) == self.notify_start:
|
|
api.create_alert("OreAlert: {0}".format(player.name))
|
|
if len(player.prev) % self.notify_each == 0:
|
|
# In-game Notification
|
|
api.plugin(api.PLUGIN_STAFF_CHAT,
|
|
"OreAlert {0} has found {1} diamond ore veins within {2} minutes.".format(
|
|
player.name, len(player.prev), self.purge))
|
|
# Discord Notification
|
|
ping = True if len(player.prev) % self.notify_ping == 0 else False
|
|
api.discord_notification(
|
|
'{0} has found {1} diamond ore veins within {2} minutes.'.format(
|
|
player.name.replace("_", "\\_"), len(player.prev), self.purge), ping=ping)
|
|
if new_player:
|
|
p.prev = [p.time]
|
|
self.playerList.append(p)
|
|
except KeyboardInterrupt:
|
|
api.discord_notification("OreAlert has been stopped manually.")
|
|
except:
|
|
api.discord_notification("OreAlert has crashed!", ping=True)
|
|
|
|
|