2018-09-20 02:56:17 +00:00
import discord , logging , re , sys , traceback , asyncio , datetime , os , time
from minecraft_manager . models import Application , Player
from minecraft_manager . api import api
2018-10-21 03:59:10 +00:00
from django . contrib . auth . models import User
2018-09-20 02:56:17 +00:00
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 ' , [ ] )
2018-12-10 20:58:53 +00:00
new_member_roles = getattr ( settings , ' DISCORD_BOT_NEW_MEMBER_ROLES ' , [ ] )
2018-09-20 02:56:17 +00:00
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 ]
2018-12-09 16:22:52 +00:00
# FIX STALE DB CONNECTIONS
close_old_connections ( )
# IF NOT A MEMBER YET
if len ( member_roles ) == 1 :
# REGISTER
match = re . match ( " [ {0} ]register ( \ S+)?$ " . format ( self . prefix ) , message . content )
if match :
search = match . group ( 1 )
2018-12-10 21:10:41 +00:00
count = Application . objects . filter ( username__iexact = search , accepted = True ) . count ( )
2018-12-09 16:22:52 +00:00
2018-12-10 18:44:31 +00:00
if count == 0 :
2018-12-10 21:10:41 +00:00
count = Player . objects . filter ( username__iexact = search , application__accepted = True ) . count ( )
2018-12-10 18:44:31 +00:00
2018-12-09 16:22:52 +00:00
if count > 0 :
if count == 1 :
2018-12-10 21:10:41 +00:00
player = Player . objects . filter ( username__iexact = search , application__accepted = True ) . all ( ) [ 0 ]
2018-12-10 18:44:31 +00:00
nickname = player . username
if not player . is_banned :
2018-12-10 19:26:14 +00:00
member = discord . utils . get ( message . server . members , display_name = nickname )
if member is not None and member is not message . author :
msg = " {0} , a member with that name is already exists, please contact the staff " . format ( message . author . mention )
yield from self . discord_message ( message . channel , msg )
else :
2018-12-10 20:58:53 +00:00
for role_id in self . new_member_roles :
role = discord . utils . get ( message . server . roles , id = role_id )
yield from self . add_roles ( message . author , role )
msg = " Successfully added {0} as a member " . format ( nickname )
2018-12-10 19:26:14 +00:00
yield from self . change_nickname ( message . author , nickname )
yield from self . discord_message ( message . channel , msg )
2018-12-10 18:44:31 +00:00
else :
2018-12-10 21:22:02 +00:00
msg = " {0} You are currently banned. " . format ( message . author . mention )
2018-12-10 18:44:31 +00:00
yield from self . discord_message ( message . channel , msg )
2018-12-09 16:22:52 +00:00
return
else :
2018-12-10 21:22:02 +00:00
msg = " {0} , an application for {1} could not be found, please check your username and make sure you have applied. " . format ( message . author . mention , search )
2018-12-09 16:22:52 +00:00
yield from self . discord_message ( message . channel , msg )
2018-12-10 18:44:31 +00:00
return
2018-12-09 16:22:52 +00:00
2018-09-20 02:56:17 +00:00
# IF MEMBER IS NOT AUTHORIZED, IGNORE
if not any ( role in self . auth_roles for role in member_roles ) :
return
# 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 = " ----------------------------- " )
2018-12-10 21:28:32 +00:00
embed . add_field ( name = " {} register <username> " . format ( self . prefix ) , value = " Allows new members to join the Discord server if they have applied and been accepted. " )
2018-09-20 02:56:17 +00:00
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. " )
2018-12-06 19:25:26 +00:00
embed . add_field ( name = " {} demote <username> " . format ( self . prefix ) , value = " Demote a player to the role given to accepted applications. " )
2018-10-21 03:59:10 +00:00
embed . add_field ( name = " {} compare " . format ( self . prefix ) , value = " Compare Discord users to the Whitelist. " )
2018-09-20 02:56:17 +00:00
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 " "
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 )
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 :
2018-12-09 05:30:56 +00:00
info + = " \n **This is only 10 applications out of {0} found. Please narrow your search if possible.** " . format (
len ( applications ) )
2018-09-20 02:56:17 +00:00
else :
2018-12-09 05:35:29 +00:00
players = Player . objects . filter ( username__icontains = search , application__isnull = False ) [ : 10 ]
count = Player . objects . filter ( username__icontains = search , application__isnull = False ) . count ( )
2018-12-09 05:30:56 +00:00
if count > 0 :
if count == 1 :
2018-12-09 05:38:30 +00:00
yield from self . discord_message ( message . channel , " **No applications matched, however there is a player match** " )
2018-12-09 05:30:56 +00:00
info = self . build_info ( players [ 0 ] . application )
else :
2018-12-09 05:38:30 +00:00
info = " **No applications matched, however there are player matches** "
2018-12-09 05:30:56 +00:00
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. "
2018-09-20 02:56:17 +00:00
yield from self . discord_message ( message . channel , info )
2018-10-21 03:59:10 +00:00
# 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 ) )
2018-09-20 02:56:17 +00:00
# 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. " )
2018-12-09 16:35:37 +00:00
except :
api . discord_notification ( " OreAlert has crashed! " , ping = True )
2018-09-20 02:56:17 +00:00