initial release

This commit is contained in:
Christian 2021-03-17 20:46:54 +01:00
parent 6444e20214
commit 4b3c28c853
48 changed files with 4437 additions and 0 deletions

View File

@ -0,0 +1,11 @@
[Unit]
Description=Restarting torch on ze every 30 minutes
[Service]
Type=simple
User=gameservers
WorkingDirectory=/home/gameservers/css_ze/torchlight3
ExecStart=/home/gameservers/css_ze/torchlight3/_start.sh
Restart=always
RuntimeMaxSec=1800

View File

@ -0,0 +1,11 @@
[Unit]
Description=Restarting torch on mg every 30 minutes
[Service]
Type=simple
User=gameservers
WorkingDirectory=/home/gameservers/css_ze/torchlight3
ExecStart=/home/gameservers/css_ze/torchlight3/_start_mg.sh
Restart=always
RuntimeMaxSec=1800

View File

@ -0,0 +1,27 @@
# Torchlight3
## 0. Requirements
* Python3.6
* FFMPEG
* youtube-dl
* On game server:
* custom sourcemod
* sm-ext-AsyncSocket extension
* smjansson extension
* SMJSONAPI plugin
* sm-ext-Voice extension
## 1. Install
* Install python3 and python-virtualenv
* Create a virtualenv: `virtualenv venv`
* Activate the virtualenv: `. venv/bin/activate`
* Install all dependencies: `pip install -r requirements.txt`
## 2. Usage
Set up game server stuff.
Adapt config.json.
##### Make sure you are in the virtualenv! (`. venv/bin/activate`)
Run: `python main.py`
Glacius was here. xd

View File

@ -0,0 +1,43 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import logging
import json
from collections import OrderedDict
class AccessManager():
ACCESS_FILE = "access.json"
def __init__(self):
self.Logger = logging.getLogger(__class__.__name__)
self.AccessDict = OrderedDict()
def Load(self):
self.Logger.info("Loading access from {0}".format(self.ACCESS_FILE))
with open(self.ACCESS_FILE, "r") as fp:
self.AccessDict = json.load(fp, object_pairs_hook = OrderedDict)
def Save(self):
self.Logger.info("Saving access to {0}".format(self.ACCESS_FILE))
self.AccessDict = OrderedDict(
sorted(self.AccessDict.items(), key = lambda x: x[1]["level"], reverse = True))
with open(self.ACCESS_FILE, "w") as fp:
json.dump(self.AccessDict, fp, indent = '\t')
def __len__(self):
return len(self.AccessDict)
def __getitem__(self, key):
if key in self.AccessDict:
return self.AccessDict[key]
def __setitem__(self, key, value):
self.AccessDict[key] = value
def __delitem__(self, key):
if key in self.AccessDict:
del self.AccessDict[key]
def __iter__(self):
return self.AccessDict.items().__iter__()

View File

@ -0,0 +1,96 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import asyncio
import logging
import json
class ClientProtocol(asyncio.Protocol):
def __init__(self, loop, master):
self.Loop = loop
self.Master = master
self.Transport = None
self.Buffer = bytearray()
def connection_made(self, transport):
self.Transport = transport
def data_received(self, data):
self.Buffer += data
chunks = self.Buffer.split(b'\0')
if data[-1] == b'\0':
chunks = chunks[:-1]
self.Buffer = bytearray()
else:
self.Buffer = bytearray(chunks[-1])
chunks = chunks[:-1]
for chunk in chunks:
self.Master.OnReceive(chunk)
def connection_lost(self, exc):
self.Transport.close()
self.Transport = None
self.Master.OnDisconnect(exc)
def Send(self, data):
if self.Transport:
self.Transport.write(data)
class AsyncClient():
def __init__(self, loop, host, port, master):
self.Logger = logging.getLogger(__class__.__name__)
self.Loop = loop
self.Host = host
self.Port = port
self.Master = master
self.Protocol = None
self.SendLock = asyncio.Lock()
self.RecvFuture = None
async def Connect(self):
while True:
self.Logger.warn("Reconnecting...")
try:
_, self.Protocol = await self.Loop.create_connection(lambda: ClientProtocol(self.Loop, self), host = self.Host, port = self.Port)
break
except:
await asyncio.sleep(1.0)
def OnReceive(self, data):
Obj = json.loads(data)
if "method" in Obj and Obj["method"] == "publish":
self.Master.OnPublish(Obj)
else:
if self.RecvFuture:
self.RecvFuture.set_result(Obj)
def OnDisconnect(self, exc):
self.Protocol = None
if self.RecvFuture:
self.RecvFuture.cancel()
self.Master.OnDisconnect(exc)
async def Send(self, obj):
if not self.Protocol:
return None
Data = json.dumps(obj, ensure_ascii = False, separators = (',', ':')).encode("UTF-8")
with (await self.SendLock):
if not self.Protocol:
return None
self.RecvFuture = asyncio.Future()
self.Protocol.Send(Data)
await self.RecvFuture
if self.RecvFuture.done():
Obj = self.RecvFuture.result()
else:
Obj = None
self.RecvFuture = None
return Obj

View File

@ -0,0 +1,351 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import logging
import sys
import io
import math
from .FFmpegAudioPlayer import FFmpegAudioPlayerFactory
class AudioPlayerFactory():
AUDIOPLAYER_FFMPEG = 1
def __init__(self, master):
self.Logger = logging.getLogger(__class__.__name__)
self.Master = master
self.Torchlight = self.Master.Torchlight
self.FFmpegAudioPlayerFactory = FFmpegAudioPlayerFactory(self)
def __del__(self):
self.Logger.info("~AudioPlayerFactory()")
def NewPlayer(self, _type):
if _type == self.AUDIOPLAYER_FFMPEG:
return self.FFmpegAudioPlayerFactory.NewPlayer()
class AntiSpam():
def __init__(self, master):
self.Logger = logging.getLogger(__class__.__name__)
self.Master = master
self.Torchlight = self.Master.Torchlight
self.LastClips = dict()
self.DisabledTime = None
self.SaidHint = False
def CheckAntiSpam(self, player):
if self.DisabledTime and self.DisabledTime > self.Torchlight().Master.Loop.time() and \
not (player.Access and player.Access["level"] >= self.Torchlight().Config["AntiSpam"]["ImmunityLevel"]):
self.Torchlight().SayPrivate(player, "Torchlight is currently on cooldown! ({0} seconds left)".format(
math.ceil(self.DisabledTime - self.Torchlight().Master.Loop.time())))
return False
return True
def SpamCheck(self, Delta):
Now = self.Torchlight().Master.Loop.time()
Duration = 0.0
for Key, Clip in list(self.LastClips.items()):
if not Clip["timestamp"]:
continue
if Clip["timestamp"] + Clip["duration"] + self.Torchlight().Config["AntiSpam"]["MaxUsageSpan"] < Now:
if not Clip["active"]:
del self.LastClips[Key]
continue
Duration += Clip["duration"]
if Duration > self.Torchlight().Config["AntiSpam"]["MaxUsageTime"]:
self.DisabledTime = self.Torchlight().Master.Loop.time() + self.Torchlight().Config["AntiSpam"]["PunishDelay"]
self.Torchlight().SayChat("Blocked voice commands for the next {0} seconds. Used {1} seconds within {2} seconds.".format(
self.Torchlight().Config["AntiSpam"]["PunishDelay"], self.Torchlight().Config["AntiSpam"]["MaxUsageTime"], self.Torchlight().Config["AntiSpam"]["MaxUsageSpan"]))
# Make a copy of the list since AudioClip.Stop() will change the list
for AudioClip in self.Master.AudioClips[:]:
if AudioClip.Level < self.Torchlight().Config["AntiSpam"]["ImmunityLevel"]:
AudioClip.Stop()
self.LastClips.clear()
def OnPlay(self, clip):
Now = self.Torchlight().Master.Loop.time()
self.LastClips[hash(clip)] = dict({"timestamp": Now, "duration": 0.0, "dominant": False, "active": True})
HasDominant = False
for Key, Clip in self.LastClips.items():
if Clip["dominant"]:
HasDominant = True
break
self.LastClips[hash(clip)]["dominant"] = not HasDominant
def OnStop(self, clip):
if hash(clip) not in self.LastClips:
return
self.LastClips[hash(clip)]["active"] = False
if self.LastClips[hash(clip)]["dominant"]:
for Key, Clip in self.LastClips.items():
if Clip["active"]:
Clip["dominant"] = True
break
self.LastClips[hash(clip)]["dominant"] = False
def OnUpdate(self, clip, old_position, new_position):
Delta = new_position - old_position
Clip = self.LastClips[hash(clip)]
if not Clip["dominant"]:
return
Clip["duration"] += Delta
self.SpamCheck(Delta)
class Advertiser():
def __init__(self, master):
self.Logger = logging.getLogger(__class__.__name__)
self.Master = master
self.Torchlight = self.Master.Torchlight
self.LastClips = dict()
self.AdStop = 0
self.NextAdStop = 0
def Think(self, Delta):
Now = self.Torchlight().Master.Loop.time()
Duration = 0.0
for Key, Clip in list(self.LastClips.items()):
if not Clip["timestamp"]:
continue
if Clip["timestamp"] + Clip["duration"] + self.Torchlight().Config["Advertiser"]["MaxSpan"] < Now:
if not Clip["active"]:
del self.LastClips[Key]
continue
Duration += Clip["duration"]
self.NextAdStop -= Delta
CeilDur = math.ceil(Duration)
if CeilDur > self.AdStop and self.NextAdStop <= 0 and CeilDur % self.Torchlight().Config["Advertiser"]["AdStop"] == 0:
self.Torchlight().SayChat("Hint: Type \x07FF0000!stop(ze) !pls(mg)\x01 to stop all currently playing sounds.")
self.AdStop = CeilDur
self.NextAdStop = 0
elif CeilDur < self.AdStop:
self.AdStop = 0
self.NextAdStop = self.Torchlight().Config["Advertiser"]["AdStop"] / 2
def OnPlay(self, clip):
Now = self.Torchlight().Master.Loop.time()
self.LastClips[hash(clip)] = dict({"timestamp": Now, "duration": 0.0, "dominant": False, "active": True})
HasDominant = False
for Key, Clip in self.LastClips.items():
if Clip["dominant"]:
HasDominant = True
break
self.LastClips[hash(clip)]["dominant"] = not HasDominant
def OnStop(self, clip):
if hash(clip) not in self.LastClips:
return
self.LastClips[hash(clip)]["active"] = False
if self.LastClips[hash(clip)]["dominant"]:
for Key, Clip in self.LastClips.items():
if Clip["active"]:
Clip["dominant"] = True
break
self.LastClips[hash(clip)]["dominant"] = False
def OnUpdate(self, clip, old_position, new_position):
Delta = new_position - old_position
Clip = self.LastClips[hash(clip)]
if not Clip["dominant"]:
return
Clip["duration"] += Delta
self.Think(Delta)
class AudioManager():
def __init__(self, torchlight):
self.Logger = logging.getLogger(__class__.__name__)
self.Torchlight = torchlight
self.AntiSpam = AntiSpam(self)
self.Advertiser = Advertiser(self)
self.AudioPlayerFactory = AudioPlayerFactory(self)
self.AudioClips = []
def __del__(self):
self.Logger.info("~AudioManager()")
def CheckLimits(self, player):
Level = 0
if player.Access:
Level = player.Access["level"]
if str(Level) in self.Torchlight().Config["AudioLimits"]:
if self.Torchlight().Config["AudioLimits"][str(Level)]["Uses"] >= 0 and \
player.Storage["Audio"]["Uses"] >= self.Torchlight().Config["AudioLimits"][str(Level)]["Uses"]:
self.Torchlight().SayPrivate(player, "You have used up all of your free uses! ({0} uses)".format(
self.Torchlight().Config["AudioLimits"][str(Level)]["Uses"]))
return False
if player.Storage["Audio"]["TimeUsed"] >= self.Torchlight().Config["AudioLimits"][str(Level)]["TotalTime"]:
self.Torchlight().SayPrivate(player, "You have used up all of your free time! ({0} seconds)".format(
self.Torchlight().Config["AudioLimits"][str(Level)]["TotalTime"]))
return False
TimeElapsed = self.Torchlight().Master.Loop.time() - player.Storage["Audio"]["LastUse"]
UseDelay = player.Storage["Audio"]["LastUseLength"] * self.Torchlight().Config["AudioLimits"][str(Level)]["DelayFactor"]
if TimeElapsed < UseDelay:
self.Torchlight().SayPrivate(player, "You are currently on cooldown! ({0} seconds left)".format(
round(UseDelay - TimeElapsed)))
return False
return True
def Stop(self, player, extra):
Level = 0
if player.Access:
Level = player.Access["level"]
for AudioClip in self.AudioClips[:]:
if extra and not extra.lower() in AudioClip.Player.Name.lower():
continue
if not Level or (Level < AudioClip.Level and Level < self.Torchlight().Config["AntiSpam"]["StopLevel"]):
AudioClip.Stops.add(player.UserID)
if len(AudioClip.Stops) >= 3:
AudioClip.Stop()
self.Torchlight().SayPrivate(AudioClip.Player, "Your audio clip was stopped.")
if player != AudioClip.Player:
self.Torchlight().SayPrivate(player, "Stopped \"{0}\"({1}) audio clip.".format(AudioClip.Player.Name, AudioClip.Player.UserID))
else:
self.Torchlight().SayPrivate(player, "This audio clip needs {0} more !stop's.".format(3 - len(AudioClip.Stops)))
else:
AudioClip.Stop()
self.Torchlight().SayPrivate(AudioClip.Player, "Your audio clip was stopped.")
if player != AudioClip.Player:
self.Torchlight().SayPrivate(player, "Stopped \"{0}\"({1}) audio clip.".format(AudioClip.Player.Name, AudioClip.Player.UserID))
def AudioClip(self, player, uri, _type = AudioPlayerFactory.AUDIOPLAYER_FFMPEG):
Level = 0
if player.Access:
Level = player.Access["level"]
if self.Torchlight().Disabled and self.Torchlight().Disabled > Level:
self.Torchlight().SayPrivate(player, "Torchlight is currently disabled!")
return None
if not self.AntiSpam.CheckAntiSpam(player):
return None
if not self.CheckLimits(player):
return None
Clip = AudioClip(self, player, uri, _type)
self.AudioClips.append(Clip)
if not player.Access or player.Access["level"] < self.Torchlight().Config["AntiSpam"]["ImmunityLevel"]:
Clip.AudioPlayer.AddCallback("Play", lambda *args: self.AntiSpam.OnPlay(Clip, *args))
Clip.AudioPlayer.AddCallback("Stop", lambda *args: self.AntiSpam.OnStop(Clip, *args))
Clip.AudioPlayer.AddCallback("Update", lambda *args: self.AntiSpam.OnUpdate(Clip, *args))
Clip.AudioPlayer.AddCallback("Play", lambda *args: self.Advertiser.OnPlay(Clip, *args))
Clip.AudioPlayer.AddCallback("Stop", lambda *args: self.Advertiser.OnStop(Clip, *args))
Clip.AudioPlayer.AddCallback("Update", lambda *args: self.Advertiser.OnUpdate(Clip, *args))
return Clip
def OnDisconnect(self, player):
for AudioClip in self.AudioClips[:]:
if AudioClip.Player == player:
AudioClip.Stop()
class AudioClip():
def __init__(self, master, player, uri, _type):
self.Logger = logging.getLogger(__class__.__name__)
self.Master = master
self.Torchlight = self.Master.Torchlight
self.Player = player
self.Type = _type
self.URI = uri
self.LastPosition = None
self.Stops = set()
self.Level = 0
if self.Player.Access:
self.Level = self.Player.Access["level"]
self.AudioPlayer = self.Master.AudioPlayerFactory.NewPlayer(self.Type)
self.AudioPlayer.AddCallback("Play", self.OnPlay)
self.AudioPlayer.AddCallback("Stop", self.OnStop)
self.AudioPlayer.AddCallback("Update", self.OnUpdate)
def __del__(self):
self.Logger.info("~AudioClip()")
def Play(self, seconds = None, *args):
return self.AudioPlayer.PlayURI(self.URI, seconds, *args)
def Stop(self):
return self.AudioPlayer.Stop()
def OnPlay(self):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + self.URI)
self.Player.Storage["Audio"]["Uses"] += 1
self.Player.Storage["Audio"]["LastUse"] = self.Torchlight().Master.Loop.time()
self.Player.Storage["Audio"]["LastUseLength"] = 0.0
def OnStop(self):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + self.URI)
self.Master.AudioClips.remove(self)
if self.AudioPlayer.Playing:
Delta = self.AudioPlayer.Position - self.LastPosition
self.Player.Storage["Audio"]["TimeUsed"] += Delta
self.Player.Storage["Audio"]["LastUseLength"] += Delta
if str(self.Level) in self.Torchlight().Config["AudioLimits"]:
if self.Player.Storage:
if self.Player.Storage["Audio"]["TimeUsed"] >= self.Torchlight().Config["AudioLimits"][str(self.Level)]["TotalTime"]:
self.Torchlight().SayPrivate(self.Player, "You have used up all of your free time! ({0} seconds)".format(
self.Torchlight().Config["AudioLimits"][str(self.Level)]["TotalTime"]))
elif self.Player.Storage["Audio"]["LastUseLength"] >= self.Torchlight().Config["AudioLimits"][str(self.Level)]["MaxLength"]:
self.Torchlight().SayPrivate(self.Player, "Your audio clip exceeded the maximum length! ({0} seconds)".format(
self.Torchlight().Config["AudioLimits"][str(self.Level)]["MaxLength"]))
del self.AudioPlayer
def OnUpdate(self, old_position, new_position):
Delta = new_position - old_position
self.LastPosition = new_position
self.Player.Storage["Audio"]["TimeUsed"] += Delta
self.Player.Storage["Audio"]["LastUseLength"] += Delta
if not str(self.Level) in self.Torchlight().Config["AudioLimits"]:
return
if (self.Player.Storage["Audio"]["TimeUsed"] >= self.Torchlight().Config["AudioLimits"][str(self.Level)]["TotalTime"] or
self.Player.Storage["Audio"]["LastUseLength"] >= self.Torchlight().Config["AudioLimits"][str(self.Level)]["MaxLength"]):
self.Stop()

View File

@ -0,0 +1,115 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import logging
import asyncio
import sys
import re
import traceback
import math
from importlib import reload
from . import Commands
class CommandHandler():
def __init__(self, Torchlight):
self.Logger = logging.getLogger(__class__.__name__)
self.Torchlight = Torchlight
self.Commands = []
self.NeedsReload = False
def Setup(self):
Counter = len(self.Commands)
self.Commands.clear()
if Counter:
self.Logger.info(sys._getframe().f_code.co_name + " Unloaded {0} commands!".format(Counter))
Counter = 0
for subklass in sorted(Commands.BaseCommand.__subclasses__(), key = lambda x: x.Order, reverse = True):
try:
Command = subklass(self.Torchlight)
if hasattr(Command, "_setup"):
Command._setup()
except Exception as e:
self.Logger.error(traceback.format_exc())
else:
self.Commands.append(Command)
Counter += 1
self.Logger.info(sys._getframe().f_code.co_name + " Loaded {0} commands!".format(Counter))
def Reload(self):
try:
reload(Commands)
except Exception as e:
self.Logger.error(traceback.format_exc())
else:
self.Setup()
async def HandleCommand(self, line, player):
Message = line.split(sep = ' ', maxsplit = 1)
if len(Message) < 2:
Message.append("")
Message[1] = Message[1].strip()
if Message[1] and self.Torchlight().LastUrl:
Message[1] = Message[1].replace("!last", self.Torchlight().LastUrl)
line = Message[0] + ' ' + Message[1]
Level = 0
if player.Access:
Level = player.Access["level"]
RetMessage = None
Ret = None
for Command in self.Commands:
for Trigger in Command.Triggers:
Match = False
RMatch = None
if isinstance(Trigger, tuple):
if Message[0].lower().startswith(Trigger[0], 0, Trigger[1]):
Match = True
elif isinstance(Trigger, str):
if Message[0].lower() == Trigger.lower():
Match = True
else: # compiled regex
RMatch = Trigger.search(line)
if RMatch:
Match = True
if not Match:
continue
self.Logger.debug(sys._getframe().f_code.co_name + " \"{0}\" Match -> {1} | {2}".format(player.Name, Command.__class__.__name__, Trigger))
if Level < Command.Level:
RetMessage = "You do not have access to this command! (You: {0} | Required: {1})".format(Level, Command.Level)
continue
try:
if RMatch:
Ret = await Command._rfunc(line, RMatch, player)
else:
Ret = await Command._func(Message, player)
except Exception as e:
self.Logger.error(traceback.format_exc())
self.Torchlight().SayChat("Error: {0}".format(str(e)))
RetMessage = None
if isinstance(Ret, str):
Message = Ret.split(sep = ' ', maxsplit = 1)
Ret = None
if Ret != None and Ret > 0:
break
if Ret != None and Ret >= 0:
break
if RetMessage:
self.Torchlight().SayPrivate(player, RetMessage)
if self.NeedsReload:
self.NeedsReload = False
self.Reload()
return Ret

View File

@ -0,0 +1,116 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import logging
import asyncio
import sys
import re
import traceback
import math
from importlib import reload
from . import Commandsmg
class CommandHandlermg():
def __init__(self, Torchlight):
self.Logger = logging.getLogger(__class__.__name__)
self.Torchlight = Torchlight
self.Commands = []
self.NeedsReload = False
def Setup(self):
Counter = len(self.Commands)
self.Commands.clear()
if Counter:
self.Logger.info(sys._getframe().f_code.co_name + " Unloaded {0} commands!".format(Counter))
Counter = 0
for subklass in sorted(Commandsmg.BaseCommand.__subclasses__(), key = lambda x: x.Order, reverse = True):
try:
Command = subklass(self.Torchlight)
if hasattr(Command, "_setup"):
Command._setup()
except Exception as e:
self.Logger.error(traceback.format_exc())
else:
self.Commands.append(Command)
Counter += 1
self.Logger.info(sys._getframe().f_code.co_name + " Loaded {0} commands!".format(Counter))
def Reload(self):
try:
reload(Commandsmg)
except Exception as e:
self.Logger.error(traceback.format_exc())
else:
self.Setup()
async def HandleCommand(self, line, player):
Message = line.split(sep = ' ', maxsplit = 1)
if len(Message) < 2:
Message.append("")
Message[1] = Message[1].strip()
if Message[1] and self.Torchlight().LastUrl:
Message[1] = Message[1].replace("!last", self.Torchlight().LastUrl)
line = Message[0] + ' ' + Message[1]
Level = 0
if player.Access:
Level = player.Access["level"]
RetMessage = None
Ret = None
for Command in self.Commands:
for Trigger in Command.Triggers:
Match = False
RMatch = None
if isinstance(Trigger, tuple):
if Message[0].lower().startswith(Trigger[0], 0, Trigger[1]):
Match = True
elif isinstance(Trigger, str):
if Message[0].lower() == Trigger.lower():
Match = True
else: # compiled regex
RMatch = Trigger.search(line)
if RMatch:
Match = True
if not Match:
continue
self.Logger.debug(sys._getframe().f_code.co_name + " \"{0}\" Match -> {1} | {2}".format(player.Name, Command.__class__.__name__, Trigger))
if Level < Command.Level:
RetMessage = "You do not have access to this command! (You: {0} | Required: {1})".format(Level, Command.Level)
continue
try:
if RMatch:
Ret = await Command._rfunc(line, RMatch, player)
else:
Ret = await Command._func(Message, player)
except Exception as e:
self.Logger.error(traceback.format_exc())
self.Torchlight().SayChat("Error: {0}".format(str(e)))
RetMessage = None
if isinstance(Ret, str):
Message = Ret.split(sep = ' ', maxsplit = 1)
Ret = None
if Ret != None and Ret > 0:
break
if Ret != None and Ret >= 0:
break
if RetMessage:
self.Torchlight().SayPrivate(player, RetMessage)
if self.NeedsReload:
self.NeedsReload = False
self.Reload()
return Ret

View File

@ -0,0 +1,928 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import asyncio
import os
import sys
import logging
import math
from .Utils import Utils, DataHolder
import traceback
class BaseCommand():
Order = 0
def __init__(self, torchlight):
self.Logger = logging.getLogger(__class__.__name__)
self.Torchlight = torchlight
self.Triggers = []
self.Level = 0
def check_chat_cooldown(self, player):
if player.ChatCooldown > self.Torchlight().Master.Loop.time():
cooldown = player.ChatCooldown - self.Torchlight().Master.Loop.time()
self.Torchlight().SayPrivate(player, "You're on cooldown for the next {0:.1f} seconds.".format(cooldown))
return True
def check_disabled(self, player):
Level = 0
if player.Access:
Level = player.Access["level"]
Disabled = self.Torchlight().Disabled
if Disabled and (Disabled > Level or Disabled == Level and Level < self.Torchlight().Config["AntiSpam"]["ImmunityLevel"]):
self.Torchlight().SayPrivate(player, "Torchlight is currently disabled!")
return True
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name)
class URLFilter(BaseCommand):
Order = 1
import re
import aiohttp
import magic
import datetime
import json
import io
from bs4 import BeautifulSoup
from PIL import Image
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = [self.re.compile(r'''(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))''', self.re.IGNORECASE)]
self.Level = 10
self.re_youtube = self.re.compile(r'.*?(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)([a-zA-Z0-9_-]{6,11}).*?')
async def URLInfo(self, url, yt = False):
Text = None
Info = None
match = self.re_youtube.search(url)
if match or yt:
Temp = DataHolder()
Time = None
if Temp(url.find("&t=")) != -1 or Temp(url.find("?t=")) != -1 or Temp(url.find("#t=")) != -1:
TimeStr = url[Temp.value + 3:].split('&')[0].split('?')[0].split('#')[0]
if TimeStr:
Time = Utils.ParseTime(TimeStr)
Proc = await asyncio.create_subprocess_exec("youtube-dl", "--dump-json", "-g", url,
stdout = asyncio.subprocess.PIPE)
Out, _ = await Proc.communicate()
parts = Out.split(b'\n')
parts.pop() # trailing new line
Info = parts.pop()
url = parts.pop()
url = url.strip().decode("ascii")
Info = self.json.loads(Info)
if Info["extractor_key"] == "Youtube":
self.Torchlight().SayChat("\x07E52D27[YouTube]\x01 {0} | {1} | {2}/5.00 | {3:,}".format(
Info["title"], str(self.datetime.timedelta(seconds = Info["duration"])), round(Info["average_rating"], 2), int(Info["view_count"])))
else:
match = None
if Time:
url += "#t={0}".format(Time)
else:
try:
async with self.aiohttp.ClientSession() as session:
Response = await asyncio.wait_for(session.get(url), 5)
if Response:
ContentType = Response.headers.get("Content-Type")
ContentLength = Response.headers.get("Content-Length")
Content = await asyncio.wait_for(Response.content.read(65536), 5)
if not ContentLength:
ContentLength = -1
if ContentType.startswith("text"):
if ContentType.startswith("text/plain"):
Text = Content.decode("utf-8", errors = "ignore")
else:
Soup = self.BeautifulSoup(Content.decode("utf-8", errors = "ignore"), "lxml")
if Soup.title:
self.Torchlight().SayChat("[URL] {0}".format(Soup.title.string))
elif ContentType.startswith("image"):
fp = self.io.BytesIO(Content)
im = self.Image.open(fp)
self.Torchlight().SayChat("[IMAGE] {0} | Width: {1} | Height: {2} | Size: {3}".format(im.format, im.size[0], im.size[1], Utils.HumanSize(ContentLength)))
fp.close()
else:
Filetype = self.magic.from_buffer(bytes(Content))
self.Torchlight().SayChat("[FILE] {0} | Size: {1}".format(Filetype, Utils.HumanSize(ContentLength)))
Response.close()
except Exception as e:
self.Torchlight().SayChat("Error: {0}".format(str(e)))
self.Logger.error(traceback.format_exc())
self.Torchlight().LastUrl = url
return url, Text
async def _rfunc(self, line, match, player):
Url = match.groups()[0]
if not Url.startswith("http") and not Url.startswith("ftp"):
Url = "http://" + Url
if line.startswith("!yt "):
URL, _ = await self.URLInfo(Url, True)
return "!yt " + URL
if line.startswith("!dec "):
_, text = await self.URLInfo(Url, False)
if text:
return "!dec " + text
asyncio.ensure_future(self.URLInfo(Url))
return -1
def FormatAccess(Torchlight, player):
Answer = "#{0} \"{1}\"({2}) is ".format(player.UserID, player.Name, player.UniqueID)
Level = str(0)
if player.Access:
Level = str(player.Access["level"])
Answer += "level {0!s} as {1}.".format(Level, player.Access["name"])
else:
Answer += "not authenticated."
if Level in Torchlight().Config["AudioLimits"]:
Uses = Torchlight().Config["AudioLimits"][Level]["Uses"]
TotalTime = Torchlight().Config["AudioLimits"][Level]["TotalTime"]
if Uses >= 0:
Answer += " Uses: {0}/{1}".format(player.Storage["Audio"]["Uses"], Uses)
if TotalTime >= 0:
Answer += " Time: {0}/{1}".format(round(player.Storage["Audio"]["TimeUsed"], 2), round(TotalTime, 2))
return Answer
class Access(BaseCommand):
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!access"]
self.Level = 0
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if self.check_chat_cooldown(player):
return -1
Count = 0
if message[0] == "!access":
if message[1]:
return -1
self.Torchlight().SayChat(FormatAccess(self.Torchlight, player), player)
return 0
class Who(BaseCommand):
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!who", "!whois"]
self.Level = 1
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
Count = 0
if message[0] == "!who":
for Player in self.Torchlight().Players:
if Player.Name.lower().find(message[1].lower()) != -1:
self.Torchlight().SayChat(FormatAccess(self.Torchlight, Player))
Count += 1
if Count >= 3:
break
elif message[0] == "!whois":
for UniqueID, Access in self.Torchlight().Access:
if Access["name"].lower().find(message[1].lower()) != -1:
Player = self.Torchlight().Players.FindUniqueID(UniqueID)
if Player:
self.Torchlight().SayChat(FormatAccess(self.Torchlight, Player))
else:
self.Torchlight().SayChat("#? \"{0}\"({1}) is level {2!s} is currently offline.".format(Access["name"], UniqueID, Access["level"]))
Count += 1
if Count >= 3:
break
return 0
class WolframAlpha(BaseCommand):
import urllib.parse
import aiohttp
import xml.etree.ElementTree as etree
import re
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!cc"]
self.Level = 10
def Clean(self, Text):
return self.re.sub("[ ]{2,}", " ", Text.replace(' | ', ': ').replace('\n', ' | ').replace('~~', '')).strip()
async def Calculate(self, Params, player):
async with self.aiohttp.ClientSession() as session:
Response = await asyncio.wait_for(session.get("http://api.wolframalpha.com/v2/query", params=Params), 10)
if not Response:
return 1
Data = await asyncio.wait_for(Response.text(), 5)
if not Data:
return 2
Root = self.etree.fromstring(Data)
# Find all pods with plaintext answers
# Filter out None -answers, strip strings and filter out the empty ones
Pods = list(filter(None, [p.text.strip() for p in Root.findall('.//subpod/plaintext') if p is not None and p.text is not None]))
# no answer pods found, check if there are didyoumeans-elements
if not Pods:
Didyoumeans = Root.find("didyoumeans")
# no support for future stuff yet, TODO?
if not Didyoumeans:
# If there's no pods, the question clearly wasn't understood
self.Torchlight().SayChat("Sorry, couldn't understand the question.", player)
return 3
Options = []
for Didyoumean in Didyoumeans:
Options.append("\"{0}\"".format(Didyoumean.text))
Line = " or ".join(Options)
Line = "Did you mean {0}?".format(Line)
self.Torchlight().SayChat(Line, player)
return 0
# If there's only one pod with text, it's probably the answer
# example: "integral x²"
if len(Pods) == 1:
Answer = self.Clean(Pods[0])
self.Torchlight().SayChat(Answer, player)
return 0
# If there's multiple pods, first is the question interpretation
Question = self.Clean(Pods[0].replace(' | ', ' ').replace('\n', ' '))
# and second is the best answer
Answer = self.Clean(Pods[1])
self.Torchlight().SayChat("{0} = {1}".format(Question, Answer), player)
return 0
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if self.check_chat_cooldown(player):
return -1
if self.check_disabled(player):
return -1
Params = dict({"input": message[1], "appid": self.Torchlight().Config["WolframAPIKey"]})
Ret = await self.Calculate(Params, player)
return Ret
class UrbanDictionary(BaseCommand):
import aiohttp
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!define", "!ud"]
self.Level = 10
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if self.check_chat_cooldown(player):
return -1
if self.check_disabled(player):
return -1
async with self.aiohttp.ClientSession() as session:
Response = await asyncio.wait_for(session.get("https://api.urbandictionary.com/v0/define?term={0}".format(message[1])), 5)
if not Response:
return 1
Data = await asyncio.wait_for(Response.json(), 5)
if not Data:
return 3
if not 'list' in Data or not Data["list"]:
self.Torchlight().SayChat("[UB] No definition found for: {}".format(message[1]), player)
return 4
def print_item(item):
self.Torchlight().SayChat("[UD] {word} ({thumbs_up}/{thumbs_down}): {definition}\n{example}".format(**item), player)
print_item(Data["list"][0])
class OpenWeather(BaseCommand):
import aiohttp
import geoip2.database
def __init__(self, torchlight):
super().__init__(torchlight)
self.GeoIP = self.geoip2.database.Reader("/usr/share/GeoIP/GeoLite2-City.mmdb")
self.Triggers = ["!w", "!vv"]
self.Level = 10
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if self.check_chat_cooldown(player):
return -1
if self.check_disabled(player):
return -1
if not message[1]:
# Use GeoIP location
info = self.GeoIP.city(player.Address.split(":")[0])
Search = "lat={}&lon={}".format(info.location.latitude, info.location.longitude)
else:
Search = "q={}".format(message[1])
async with self.aiohttp.ClientSession() as session:
Response = await asyncio.wait_for(session.get("https://api.openweathermap.org/data/2.5/weather?APPID={0}&units=metric&{1}".format(
self.Torchlight().Config["OpenWeatherAPIKey"], Search)), 5)
if not Response:
return 2
Data = await asyncio.wait_for(Response.json(), 5)
if not Data:
return 3
if Data["cod"] != 200:
self.Torchlight().SayPrivate(player, "[OW] {0}".format(Data["message"]))
return 5
degToCardinal = lambda d: ["N", "NE", "E", "SE", "S", "SW", "W", "NW"][int(((d + 22.5)/45.0) % 8)]
if "deg" in Data["wind"]:
windDir = degToCardinal(Data["wind"]["deg"])
else:
windDir = "?"
timezone = "{}{}".format('+' if Data["timezone"] > 0 else '', int(Data["timezone"] / 3600))
if Data["timezone"] % 3600 != 0:
timezone += ":{}".format((Data["timezone"] % 3600) / 60)
self.Torchlight().SayChat("[{}, {}](UTC{}) {}°C ({}/{}) {}: {} | Wind {} {}kph | Clouds: {}%% | Humidity: {}%%".format(Data["name"], Data["sys"]["country"], timezone,
Data["main"]["temp"], Data["main"]["temp_min"], Data["main"]["temp_max"], Data["weather"][0]["main"], Data["weather"][0]["description"],
windDir, Data["wind"]["speed"], Data["clouds"]["all"], Data["main"]["humidity"]), player)
return 0
'''
class WUnderground(BaseCommand):
import aiohttp
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!w"]
self.Level = 0
async def _func(self, message, player):
if not message[1]:
# Use IP address
Search = "autoip"
Additional = "?geo_ip={0}".format(player.Address.split(":")[0])
else:
async with self.aiohttp.ClientSession() as session:
Response = await asyncio.wait_for(session.get("http://autocomplete.wunderground.com/aq?format=JSON&query={0}".format(message[1])), 5)
if not Response:
return 2
Data = await asyncio.wait_for(Response.json(), 5)
if not Data:
return 3
if not Data["RESULTS"]:
self.Torchlight().SayPrivate(player, "[WU] No cities match your search query.")
return 4
Search = Data["RESULTS"][0]["name"]
Additional = ""
async with self.aiohttp.ClientSession() as session:
Response = await asyncio.wait_for(session.get("http://api.wunderground.com/api/{0}/conditions/q/{1}.json{2}".format(
self.Torchlight().Config["WundergroundAPIKey"], Search, Additional)), 5)
if not Response:
return 2
Data = await asyncio.wait_for(Response.json(), 5)
if not Data:
return 3
if "error" in Data["response"]:
self.Torchlight().SayPrivate(player, "[WU] {0}.".format(Data["response"]["error"]["description"]))
return 5
if not "current_observation" in Data:
Choices = str()
NumResults = len(Data["response"]["results"])
for i, Result in enumerate(Data["response"]["results"]):
Choices += "{0}, {1}".format(Result["city"],
Result["state"] if Result["state"] else Result ["country_iso3166"])
if i < NumResults - 1:
Choices += " | "
self.Torchlight().SayPrivate(player, "[WU] Did you mean: {0}".format(Choices))
return 6
Observation = Data["current_observation"]
self.Torchlight().SayChat("[{0}, {1}] {2}°C ({3}F) {4} | Wind {5} {6}kph ({7}mph) | Humidity: {8}".format(Observation["display_location"]["city"],
Observation["display_location"]["state"] if Observation["display_location"]["state"] else Observation["display_location"]["country_iso3166"],
Observation["temp_c"], Observation["temp_f"], Observation["weather"],
Observation["wind_dir"], Observation["wind_kph"], Observation["wind_mph"],
Observation["relative_humidity"]))
return 0
'''
class VoteDisable(BaseCommand):
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!votedisable", "!disablevote"]
self.Level = 0
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if self.Torchlight().Disabled:
self.Torchlight().SayPrivate(player, "Torchlight is already disabled for the duration of this map.")
return
self.Torchlight().DisableVotes.add(player.UniqueID)
have = len(self.Torchlight().DisableVotes)
needed = len(self.Torchlight().Players) // 5
if have >= needed:
self.Torchlight().SayChat("Torchlight has been disabled for the duration of this map.")
self.Torchlight().Disabled = 6
else:
self.Torchlight().SayPrivate(player, "Torchlight needs {0} more disable votes to be disabled.".format(needed - have))
class VoiceCommands(BaseCommand):
import json
import random
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!random", "!search"]
self.Level = 0
def LoadTriggers(self):
try:
with open("triggers.json", "r") as fp:
Triggers = self.json.load(fp)
except ValueError as e:
self.Logger.error(sys._getframe().f_code.co_name + ' ' + str(e))
self.Torchlight().SayChat(str(e))
self.VoiceTriggers = dict()
for Line in Triggers:
for Trigger in Line["names"]:
self.VoiceTriggers[Trigger] = Line["sound"]
def _setup(self):
self.Logger.debug(sys._getframe().f_code.co_name)
self.LoadTriggers()
for Trigger in self.VoiceTriggers.keys():
self.Triggers.append(Trigger)
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if self.check_disabled(player):
return -1
Level = 0
if player.Access:
Level = player.Access["level"]
message[0] = message[0].lower()
message[1] = message[1].lower()
if message[0][0] != '!' and Level < 2:
return 1
if message[0] == "!search":
res = []
for key in self.VoiceTriggers.keys():
if message[1] in key.lower():
res.append(key)
self.Torchlight().SayPrivate(player, "{} results: {}".format(len(res), ", ".join(res)))
return 0
elif Level < 2:
return 0
if message[0] == "!random":
Trigger = self.random.choice(list(self.VoiceTriggers.values()))
if isinstance(Trigger, list):
Sound = self.random.choice(Trigger)
else:
Sound = Trigger
else:
Sounds = self.VoiceTriggers[message[0]]
try:
Num = int(message[1])
except ValueError:
Num = None
if isinstance(Sounds, list):
if Num and Num > 0 and Num <= len(Sounds):
Sound = Sounds[Num - 1]
elif message[1]:
searching = message[1].startswith('?')
search = message[1][1:] if searching else message[1]
Sound = None
names = []
matches = []
for sound in Sounds:
name = os.path.splitext(os.path.basename(sound))[0]
names.append(name)
if search and search in name.lower():
matches.append((name, sound))
if matches:
matches.sort(key=lambda t: len(t[0]))
mlist = [t[0] for t in matches]
if searching:
self.Torchlight().SayPrivate(player, "{} results: {}".format(len(mlist), ", ".join(mlist)))
return 0
Sound = matches[0][1]
if len(matches) > 1:
self.Torchlight().SayPrivate(player, "Multiple matches: {}".format(", ".join(mlist)))
if not Sound and not Num:
if not searching:
self.Torchlight().SayPrivate(player, "Couldn't find {} in list of sounds.".format(message[1]))
self.Torchlight().SayPrivate(player, ", ".join(names))
return 1
elif Num:
self.Torchlight().SayPrivate(player, "Number {} is out of bounds, max {}.".format(Num, len(Sounds)))
return 1
else:
Sound = self.random.choice(Sounds)
else:
Sound = Sounds
if not Sound:
return 1
Path = os.path.abspath(os.path.join("sounds", Sound))
AudioClip = self.Torchlight().AudioManager.AudioClip(player, "file://" + Path)
if not AudioClip:
return 1
return AudioClip.Play()
class YouTube(BaseCommand):
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!yt"]
self.Level = 3
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if self.check_disabled(player):
return -1
if self.Torchlight().LastUrl:
message[1] = message[1].replace("!last", self.Torchlight().LastUrl)
Temp = DataHolder()
Time = None
if Temp(message[1].find("&t=")) != -1 or Temp(message[1].find("?t=")) != -1 or Temp(message[1].find("#t=")) != -1:
TimeStr = message[1][Temp.value + 3:].split('&')[0].split('?')[0].split('#')[0]
if TimeStr:
Time = Utils.ParseTime(TimeStr)
AudioClip = self.Torchlight().AudioManager.AudioClip(player, message[1])
if not AudioClip:
return 1
return AudioClip.Play(Time)
class YouTubeSearch(BaseCommand):
import json
import datetime
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!yts"]
self.Level = 3
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if self.check_disabled(player):
return -1
Temp = DataHolder()
Time = None
if Temp(message[1].find("&t=")) != -1 or Temp(message[1].find("?t=")) != -1 or Temp(message[1].find("#t=")) != -1:
TimeStr = message[1][Temp.value + 3:].split('&')[0].split('?')[0].split('#')[0]
if TimeStr:
Time = Utils.ParseTime(TimeStr)
message[1] = message[1][:Temp.value]
Proc = await asyncio.create_subprocess_exec("youtube-dl", "--dump-json", "-xg", "ytsearch:" + message[1],
stdout = asyncio.subprocess.PIPE)
Out, _ = await Proc.communicate()
print('out value: ', Out)
url, Info = Out.split(b'\n', maxsplit = 1)
url = url.strip().decode("ascii")
Info = self.json.loads(Info)
if Info["extractor_key"] == "Youtube":
self.Torchlight().SayChat("\x07E52D27[YouTube]\x01 {0} | {1} | {2}/5.00 | {3:,}".format(
Info["title"], str(self.datetime.timedelta(seconds = Info["duration"])), round(Info["average_rating"] or 0, 2), int(Info["view_count"])))
AudioClip = self.Torchlight().AudioManager.AudioClip(player, url)
if not AudioClip:
return 1
self.Torchlight().LastUrl = url
return AudioClip.Play(Time)
class Say(BaseCommand):
import gtts
import tempfile
VALID_LANGUAGES = [lang for lang in gtts.lang.tts_langs().keys()]
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = [("!say", 4)]
self.Level = 2
async def Say(self, player, language, message):
GTTS = self.gtts.gTTS(text = message, lang = language)
TempFile = self.tempfile.NamedTemporaryFile(delete = False)
GTTS.write_to_fp(TempFile)
TempFile.close()
AudioClip = self.Torchlight().AudioManager.AudioClip(player, "file://" + TempFile.name)
if not AudioClip:
os.unlink(TempFile.name)
return 1
if AudioClip.Play():
AudioClip.AudioPlayer.AddCallback("Stop", lambda: os.unlink(TempFile.name))
return 0
else:
os.unlink(TempFile.name)
return 1
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if self.check_disabled(player):
return -1
if not message[1]:
return 1
Language = "en"
if len(message[0]) > 4:
Language = message[0][4:]
if not Language in self.VALID_LANGUAGES:
return 1
asyncio.ensure_future(self.Say(player, Language, message[1]))
return 0
'''
class DECTalk(BaseCommand):
import tempfile
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!dec"]
self.Level = 0
async def Say(self, player, message):
message = "[:phoneme on]" + message
TempFile = self.tempfile.NamedTemporaryFile(delete = False)
TempFile.close()
Proc = await asyncio.create_subprocess_exec("wine", "say.exe", "-w", TempFile.name,
cwd = "dectalk", stdin = asyncio.subprocess.PIPE)
await Proc.communicate(message.encode('utf-8', errors='ignore'))
AudioClip = self.Torchlight().AudioManager.AudioClip(player, "file://" + TempFile.name)
if not AudioClip:
os.unlink(TempFile.name)
return 1
if AudioClip.Play(None, "-af", "volume=10dB"):
AudioClip.AudioPlayer.AddCallback("Stop", lambda: os.unlink(TempFile.name))
return 0
else:
os.unlink(TempFile.name)
return 1
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if self.check_disabled(player):
return -1
if not message[1]:
return 1
asyncio.ensure_future(self.Say(player, message[1]))
return 0
'''
class Stop(BaseCommand):
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!stop"]
self.Level = 0
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
self.Torchlight().AudioManager.Stop(player, message[1])
return True
class EnableDisable(BaseCommand):
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!enable", "!disable"]
self.Level = 3
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if message[0] == "!enable":
if self.Torchlight().Disabled:
if self.Torchlight().Disabled > player.Access["level"]:
self.Torchlight().SayPrivate(player, "You don't have access to enable torchlight, since it was disabled by a higher level user.")
return 1
self.Torchlight().SayChat("Torchlight has been enabled for the duration of this map - Type !disable to disable it again.")
self.Torchlight().Disabled = False
elif message[0] == "!disable":
if self.Torchlight().Disabled > player.Access["level"]:
self.Torchlight().SayPrivate(player, "You don't have access to disable torchlight, since it was already disabled by a higher level user.")
return 1
self.Torchlight().SayChat("Torchlight has been disabled for the duration of this map - Type !enable to enable it again.")
self.Torchlight().Disabled = player.Access["level"]
class AdminAccess(BaseCommand):
from collections import OrderedDict
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!access"]
self.Level = 4
def ReloadValidUsers(self):
self.Torchlight().Access.Load()
for Player in self.Torchlight().Players:
Access = self.Torchlight().Access[Player.UniqueID]
Player.Access = Access
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if not message[1]:
return -1
if message[1].lower() == "reload":
self.ReloadValidUsers()
self.Torchlight().SayChat("Loaded access list with {0} users".format(len(self.Torchlight().Access)))
elif message[1].lower() == "save":
self.Torchlight().Access.Save()
self.Torchlight().SayChat("Saved access list with {0} users".format(len(self.Torchlight().Access)))
# Modify access
else:
Player = None
Buf = message[1]
Temp = Buf.find(" as ")
if Temp != -1:
try:
Regname, Level = Buf[Temp + 4:].rsplit(' ', 1)
except ValueError as e:
self.Torchlight().SayChat(str(e))
return 1
Regname = Regname.strip()
Level = Level.strip()
Buf = Buf[:Temp].strip()
else:
try:
Buf, Level = Buf.rsplit(' ', 1)
except ValueError as e:
self.Torchlight().SayChat(str(e))
return 2
Buf = Buf.strip()
Level = Level.strip()
# Find user by User ID
if Buf[0] == '#' and Buf[1:].isnumeric():
Player = self.Torchlight().Players.FindUserID(int(Buf[1:]))
# Search user by name
else:
for Player_ in self.Torchlight().Players:
if Player_.Name.lower().find(Buf.lower()) != -1:
Player = Player_
break
if not Player:
self.Torchlight().SayChat("Couldn't find user: {0}".format(Buf))
return 3
if Level.isnumeric() or (Level.startswith('-') and Level[1:].isdigit()):
Level = int(Level)
if Level >= player.Access["level"] and player.Access["level"] < 10:
self.Torchlight().SayChat("Trying to assign level {0}, which is higher or equal than your level ({1})".format(Level, player.Access["level"]))
return 4
if Player.Access:
if Player.Access["level"] >= player.Access["level"] and player.Access["level"] < 10:
self.Torchlight().SayChat("Trying to modify level {0}, which is higher or equal than your level ({1})".format(Player.Access["level"], player.Access["level"]))
return 5
if "Regname" in locals():
self.Torchlight().SayChat("Changed \"{0}\"({1}) as {2} level/name from {3} to {4} as {5}".format(
Player.Name, Player.UniqueID, Player.Access["name"], Player.Access["level"], Level, Regname))
Player.Access["name"] = Regname
else:
self.Torchlight().SayChat("Changed \"{0}\"({1}) as {2} level from {3} to {4}".format(
Player.Name, Player.UniqueID, Player.Access["name"], Player.Access["level"], Level))
Player.Access["level"] = Level
self.Torchlight().Access[Player.UniqueID] = Player.Access
else:
if not "Regname" in locals():
Regname = Player.Name
self.Torchlight().Access[Player.UniqueID] = self.OrderedDict([("name", Regname), ("level", Level)])
Player.Access = self.Torchlight().Access[Player.UniqueID]
self.Torchlight().SayChat("Added \"{0}\"({1}) to access list as {2} with level {3}".format(Player.Name, Player.UniqueID, Regname, Level))
else:
if Level == "revoke" and Player.Access:
if Player.Access["level"] >= player.Access["level"] and player.Access["level"] < 10:
self.Torchlight().SayChat("Trying to revoke level {0}, which is higher or equal than your level ({1})".format(Player.Access["level"], player.Access["level"]))
return 6
self.Torchlight().SayChat("Removed \"{0}\"({1}) from access list (was {2} with level {3})".format(
Player.Name, Player.UniqueID, Player.Access["name"], Player.Access["level"]))
del self.Torchlight().Access[Player.UniqueID]
Player.Access = None
return 0
class Reload(BaseCommand):
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!reload"]
self.Level = 4
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
self.Torchlight().Reload()
return 0
"""
class Exec(BaseCommand):
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!exec"]
self.Level = 100
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
try:
Response = eval(message[1])
except Exception as e:
self.Torchlight().SayChat("Error: {0}".format(str(e)))
return 1
self.Torchlight().SayChat(str(Response))
return 0
"""

View File

@ -0,0 +1,930 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import asyncio
import os
import sys
import logging
import math
from .Utils import Utils, DataHolder
import traceback
class BaseCommand():
Order = 0
def __init__(self, torchlight):
self.Logger = logging.getLogger(__class__.__name__)
self.Torchlight = torchlight
self.Triggers = []
self.Level = 0
def check_chat_cooldown(self, player):
if player.ChatCooldown > self.Torchlight().Master.Loop.time():
cooldown = player.ChatCooldown - self.Torchlight().Master.Loop.time()
self.Torchlight().SayPrivate(player, "You're on cooldown for the next {0:.1f} seconds.".format(cooldown))
return True
def check_disabled(self, player):
Level = 0
if player.Access:
Level = player.Access["level"]
Disabled = self.Torchlight().Disabled
if Disabled and (Disabled > Level or Disabled == Level and Level < self.Torchlight().Config["AntiSpam"]["ImmunityLevel"]):
self.Torchlight().SayPrivate(player, "Torchlight is currently disabled!")
return True
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name)
class URLFilter(BaseCommand):
Order = 1
import re
import aiohttp
import magic
import datetime
import json
import io
from bs4 import BeautifulSoup
from PIL import Image
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = [self.re.compile(r'''(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))''', self.re.IGNORECASE)]
self.Level = 10
self.re_youtube = self.re.compile(r'.*?(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)([a-zA-Z0-9_-]{6,11}).*?')
async def URLInfo(self, url, yt = False):
Text = None
Info = None
match = self.re_youtube.search(url)
if match or yt:
Temp = DataHolder()
Time = None
if Temp(url.find("&t=")) != -1 or Temp(url.find("?t=")) != -1 or Temp(url.find("#t=")) != -1:
TimeStr = url[Temp.value + 3:].split('&')[0].split('?')[0].split('#')[0]
if TimeStr:
Time = Utils.ParseTime(TimeStr)
Proc = await asyncio.create_subprocess_exec("youtube-dl", "--dump-json", "-g", url,
stdout = asyncio.subprocess.PIPE)
Out, _ = await Proc.communicate()
parts = Out.split(b'\n')
parts.pop() # trailing new line
Info = parts.pop()
url = parts.pop()
url = url.strip().decode("ascii")
Info = self.json.loads(Info)
if Info["extractor_key"] == "Youtube":
self.Torchlight().SayChat("\x07E52D27[YouTube]\x01 {0} | {1} | {2}/5.00 | {3:,}".format(
Info["title"], str(self.datetime.timedelta(seconds = Info["duration"])), round(Info["average_rating"], 2), int(Info["view_count"])))
else:
match = None
if Time:
url += "#t={0}".format(Time)
else:
try:
async with self.aiohttp.ClientSession() as session:
Response = await asyncio.wait_for(session.get(url), 5)
if Response:
ContentType = Response.headers.get("Content-Type")
ContentLength = Response.headers.get("Content-Length")
Content = await asyncio.wait_for(Response.content.read(65536), 5)
if not ContentLength:
ContentLength = -1
if ContentType.startswith("text"):
if ContentType.startswith("text/plain"):
Text = Content.decode("utf-8", errors = "ignore")
else:
Soup = self.BeautifulSoup(Content.decode("utf-8", errors = "ignore"), "lxml")
if Soup.title:
self.Torchlight().SayChat("[URL] {0}".format(Soup.title.string))
elif ContentType.startswith("image"):
fp = self.io.BytesIO(Content)
im = self.Image.open(fp)
self.Torchlight().SayChat("[IMAGE] {0} | Width: {1} | Height: {2} | Size: {3}".format(im.format, im.size[0], im.size[1], Utils.HumanSize(ContentLength)))
fp.close()
else:
Filetype = self.magic.from_buffer(bytes(Content))
self.Torchlight().SayChat("[FILE] {0} | Size: {1}".format(Filetype, Utils.HumanSize(ContentLength)))
Response.close()
except Exception as e:
self.Torchlight().SayChat("Error: {0}".format(str(e)))
self.Logger.error(traceback.format_exc())
self.Torchlight().LastUrl = url
return url, Text
async def _rfunc(self, line, match, player):
Url = match.groups()[0]
if not Url.startswith("http") and not Url.startswith("ftp"):
Url = "http://" + Url
if line.startswith("!yt "):
return
#URL, _ = await self.URLInfo(Url, True)
#return "!yt " + URL
if line.startswith("!dec "):
_, text = await self.URLInfo(Url, False)
if text:
return "!dec " + text
asyncio.ensure_future(self.URLInfo(Url))
return -1
def FormatAccess(Torchlight, player):
Answer = "#{0} \"{1}\"({2}) is ".format(player.UserID, player.Name, player.UniqueID)
Level = str(0)
if player.Access:
Level = str(player.Access["level"])
Answer += "level {0!s} as {1}.".format(Level, player.Access["name"])
else:
Answer += "not authenticated."
if Level in Torchlight().Config["AudioLimits"]:
Uses = Torchlight().Config["AudioLimits"][Level]["Uses"]
TotalTime = Torchlight().Config["AudioLimits"][Level]["TotalTime"]
if Uses >= 0:
Answer += " Uses: {0}/{1}".format(player.Storage["Audio"]["Uses"], Uses)
if TotalTime >= 0:
Answer += " Time: {0}/{1}".format(round(player.Storage["Audio"]["TimeUsed"], 2), round(TotalTime, 2))
return Answer
class Access(BaseCommand):
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!access"]
self.Level = 0
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if self.check_chat_cooldown(player):
return -1
Count = 0
if message[0] == "!access":
if message[1]:
return -1
self.Torchlight().SayChat(FormatAccess(self.Torchlight, player), player)
return 0
class Who(BaseCommand):
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!who", "!whois"]
self.Level = 1
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
Count = 0
if message[0] == "!who":
for Player in self.Torchlight().Players:
if Player.Name.lower().find(message[1].lower()) != -1:
self.Torchlight().SayChat(FormatAccess(self.Torchlight, Player))
Count += 1
if Count >= 3:
break
elif message[0] == "!whois":
for UniqueID, Access in self.Torchlight().Access:
if Access["name"].lower().find(message[1].lower()) != -1:
Player = self.Torchlight().Players.FindUniqueID(UniqueID)
if Player:
self.Torchlight().SayChat(FormatAccess(self.Torchlight, Player))
else:
self.Torchlight().SayChat("#? \"{0}\"({1}) is level {2!s} is currently offline.".format(Access["name"], UniqueID, Access["level"]))
Count += 1
if Count >= 3:
break
return 0
class WolframAlpha(BaseCommand):
import urllib.parse
import aiohttp
import xml.etree.ElementTree as etree
import re
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!cc"]
self.Level = 10
def Clean(self, Text):
return self.re.sub("[ ]{2,}", " ", Text.replace(' | ', ': ').replace('\n', ' | ').replace('~~', '')).strip()
async def Calculate(self, Params, player):
async with self.aiohttp.ClientSession() as session:
Response = await asyncio.wait_for(session.get("http://api.wolframalpha.com/v2/query", params=Params), 10)
if not Response:
return 1
Data = await asyncio.wait_for(Response.text(), 5)
if not Data:
return 2
Root = self.etree.fromstring(Data)
# Find all pods with plaintext answers
# Filter out None -answers, strip strings and filter out the empty ones
Pods = list(filter(None, [p.text.strip() for p in Root.findall('.//subpod/plaintext') if p is not None and p.text is not None]))
# no answer pods found, check if there are didyoumeans-elements
if not Pods:
Didyoumeans = Root.find("didyoumeans")
# no support for future stuff yet, TODO?
if not Didyoumeans:
# If there's no pods, the question clearly wasn't understood
self.Torchlight().SayChat("Sorry, couldn't understand the question.", player)
return 3
Options = []
for Didyoumean in Didyoumeans:
Options.append("\"{0}\"".format(Didyoumean.text))
Line = " or ".join(Options)
Line = "Did you mean {0}?".format(Line)
self.Torchlight().SayChat(Line, player)
return 0
# If there's only one pod with text, it's probably the answer
# example: "integral x²"
if len(Pods) == 1:
Answer = self.Clean(Pods[0])
self.Torchlight().SayChat(Answer, player)
return 0
# If there's multiple pods, first is the question interpretation
Question = self.Clean(Pods[0].replace(' | ', ' ').replace('\n', ' '))
# and second is the best answer
Answer = self.Clean(Pods[1])
self.Torchlight().SayChat("{0} = {1}".format(Question, Answer), player)
return 0
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if self.check_chat_cooldown(player):
return -1
if self.check_disabled(player):
return -1
Params = dict({"input": message[1], "appid": self.Torchlight().Config["WolframAPIKey"]})
Ret = await self.Calculate(Params, player)
return Ret
class UrbanDictionary(BaseCommand):
import aiohttp
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!define", "!ud"]
self.Level = 10
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if self.check_chat_cooldown(player):
return -1
if self.check_disabled(player):
return -1
async with self.aiohttp.ClientSession() as session:
Response = await asyncio.wait_for(session.get("https://api.urbandictionary.com/v0/define?term={0}".format(message[1])), 5)
if not Response:
return 1
Data = await asyncio.wait_for(Response.json(), 5)
if not Data:
return 3
if not 'list' in Data or not Data["list"]:
self.Torchlight().SayChat("[UB] No definition found for: {}".format(message[1]), player)
return 4
def print_item(item):
self.Torchlight().SayChat("[UD] {word} ({thumbs_up}/{thumbs_down}): {definition}\n{example}".format(**item), player)
print_item(Data["list"][0])
class OpenWeather(BaseCommand):
import aiohttp
import geoip2.database
def __init__(self, torchlight):
super().__init__(torchlight)
self.GeoIP = self.geoip2.database.Reader("/usr/share/GeoIP/GeoLite2-City.mmdb")
self.Triggers = ["!w", "!vv"]
self.Level = 10
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if self.check_chat_cooldown(player):
return -1
if self.check_disabled(player):
return -1
if not message[1]:
# Use GeoIP location
info = self.GeoIP.city(player.Address.split(":")[0])
Search = "lat={}&lon={}".format(info.location.latitude, info.location.longitude)
else:
Search = "q={}".format(message[1])
async with self.aiohttp.ClientSession() as session:
Response = await asyncio.wait_for(session.get("https://api.openweathermap.org/data/2.5/weather?APPID={0}&units=metric&{1}".format(
self.Torchlight().Config["OpenWeatherAPIKey"], Search)), 5)
if not Response:
return 2
Data = await asyncio.wait_for(Response.json(), 5)
if not Data:
return 3
if Data["cod"] != 200:
self.Torchlight().SayPrivate(player, "[OW] {0}".format(Data["message"]))
return 5
degToCardinal = lambda d: ["N", "NE", "E", "SE", "S", "SW", "W", "NW"][int(((d + 22.5)/45.0) % 8)]
if "deg" in Data["wind"]:
windDir = degToCardinal(Data["wind"]["deg"])
else:
windDir = "?"
timezone = "{}{}".format('+' if Data["timezone"] > 0 else '', int(Data["timezone"] / 3600))
if Data["timezone"] % 3600 != 0:
timezone += ":{}".format((Data["timezone"] % 3600) / 60)
self.Torchlight().SayChat("[{}, {}](UTC{}) {}°C ({}/{}) {}: {} | Wind {} {}kph | Clouds: {}%% | Humidity: {}%%".format(Data["name"], Data["sys"]["country"], timezone,
Data["main"]["temp"], Data["main"]["temp_min"], Data["main"]["temp_max"], Data["weather"][0]["main"], Data["weather"][0]["description"],
windDir, Data["wind"]["speed"], Data["clouds"]["all"], Data["main"]["humidity"]), player)
return 0
'''
class WUnderground(BaseCommand):
import aiohttp
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!w"]
self.Level = 0
async def _func(self, message, player):
if not message[1]:
# Use IP address
Search = "autoip"
Additional = "?geo_ip={0}".format(player.Address.split(":")[0])
else:
async with self.aiohttp.ClientSession() as session:
Response = await asyncio.wait_for(session.get("http://autocomplete.wunderground.com/aq?format=JSON&query={0}".format(message[1])), 5)
if not Response:
return 2
Data = await asyncio.wait_for(Response.json(), 5)
if not Data:
return 3
if not Data["RESULTS"]:
self.Torchlight().SayPrivate(player, "[WU] No cities match your search query.")
return 4
Search = Data["RESULTS"][0]["name"]
Additional = ""
async with self.aiohttp.ClientSession() as session:
Response = await asyncio.wait_for(session.get("http://api.wunderground.com/api/{0}/conditions/q/{1}.json{2}".format(
self.Torchlight().Config["WundergroundAPIKey"], Search, Additional)), 5)
if not Response:
return 2
Data = await asyncio.wait_for(Response.json(), 5)
if not Data:
return 3
if "error" in Data["response"]:
self.Torchlight().SayPrivate(player, "[WU] {0}.".format(Data["response"]["error"]["description"]))
return 5
if not "current_observation" in Data:
Choices = str()
NumResults = len(Data["response"]["results"])
for i, Result in enumerate(Data["response"]["results"]):
Choices += "{0}, {1}".format(Result["city"],
Result["state"] if Result["state"] else Result ["country_iso3166"])
if i < NumResults - 1:
Choices += " | "
self.Torchlight().SayPrivate(player, "[WU] Did you mean: {0}".format(Choices))
return 6
Observation = Data["current_observation"]
self.Torchlight().SayChat("[{0}, {1}] {2}°C ({3}F) {4} | Wind {5} {6}kph ({7}mph) | Humidity: {8}".format(Observation["display_location"]["city"],
Observation["display_location"]["state"] if Observation["display_location"]["state"] else Observation["display_location"]["country_iso3166"],
Observation["temp_c"], Observation["temp_f"], Observation["weather"],
Observation["wind_dir"], Observation["wind_kph"], Observation["wind_mph"],
Observation["relative_humidity"]))
return 0
'''
class VoteDisable(BaseCommand):
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!votedisable", "!disablevote"]
self.Level = 0
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if self.Torchlight().Disabled:
self.Torchlight().SayPrivate(player, "Torchlight is already disabled for the duration of this map.")
return
self.Torchlight().DisableVotes.add(player.UniqueID)
have = len(self.Torchlight().DisableVotes)
needed = len(self.Torchlight().Players) // 5
if have >= needed:
self.Torchlight().SayChat("Torchlight has been disabled for the duration of this map.")
self.Torchlight().Disabled = 6
else:
self.Torchlight().SayPrivate(player, "Torchlight needs {0} more disable votes to be disabled.".format(needed - have))
class VoiceCommands(BaseCommand):
import json
import random
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!random", "!search"]
self.Level = 0
def LoadTriggers(self):
try:
with open("triggers.json", "r") as fp:
Triggers = self.json.load(fp)
except ValueError as e:
self.Logger.error(sys._getframe().f_code.co_name + ' ' + str(e))
self.Torchlight().SayChat(str(e))
self.VoiceTriggers = dict()
for Line in Triggers:
for Trigger in Line["names"]:
self.VoiceTriggers[Trigger] = Line["sound"]
def _setup(self):
self.Logger.debug(sys._getframe().f_code.co_name)
self.LoadTriggers()
for Trigger in self.VoiceTriggers.keys():
self.Triggers.append(Trigger)
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if self.check_disabled(player):
return -1
Level = 0
if player.Access:
Level = player.Access["level"]
message[0] = message[0].lower()
message[1] = message[1].lower()
if message[0][0] != '!' and Level < 2:
return 1
if message[0] == "!search":
res = []
for key in self.VoiceTriggers.keys():
if message[1] in key.lower():
res.append(key)
self.Torchlight().SayPrivate(player, "{} results: {}".format(len(res), ", ".join(res)))
return 0
elif Level < 2:
return 0
if message[0] == "!random":
Trigger = self.random.choice(list(self.VoiceTriggers.values()))
if isinstance(Trigger, list):
Sound = self.random.choice(Trigger)
else:
Sound = Trigger
else:
Sounds = self.VoiceTriggers[message[0]]
try:
Num = int(message[1])
except ValueError:
Num = None
if isinstance(Sounds, list):
if Num and Num > 0 and Num <= len(Sounds):
Sound = Sounds[Num - 1]
elif message[1]:
searching = message[1].startswith('?')
search = message[1][1:] if searching else message[1]
Sound = None
names = []
matches = []
for sound in Sounds:
name = os.path.splitext(os.path.basename(sound))[0]
names.append(name)
if search and search in name.lower():
matches.append((name, sound))
if matches:
matches.sort(key=lambda t: len(t[0]))
mlist = [t[0] for t in matches]
if searching:
self.Torchlight().SayPrivate(player, "{} results: {}".format(len(mlist), ", ".join(mlist)))
return 0
Sound = matches[0][1]
if len(matches) > 1:
self.Torchlight().SayPrivate(player, "Multiple matches: {}".format(", ".join(mlist)))
if not Sound and not Num:
if not searching:
self.Torchlight().SayPrivate(player, "Couldn't find {} in list of sounds.".format(message[1]))
self.Torchlight().SayPrivate(player, ", ".join(names))
return 1
elif Num:
self.Torchlight().SayPrivate(player, "Number {} is out of bounds, max {}.".format(Num, len(Sounds)))
return 1
else:
Sound = self.random.choice(Sounds)
else:
Sound = Sounds
if not Sound:
return 1
Path = os.path.abspath(os.path.join("sounds", Sound))
AudioClip = self.Torchlight().AudioManager.AudioClip(player, "file://" + Path)
if not AudioClip:
return 1
return AudioClip.Play()
class YouTube(BaseCommand):
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!yt"]
self.Level = 3
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if self.check_disabled(player):
return -1
if self.Torchlight().LastUrl:
message[1] = message[1].replace("!last", self.Torchlight().LastUrl)
Temp = DataHolder()
Time = None
if Temp(message[1].find("&t=")) != -1 or Temp(message[1].find("?t=")) != -1 or Temp(message[1].find("#t=")) != -1:
TimeStr = message[1][Temp.value + 3:].split('&')[0].split('?')[0].split('#')[0]
if TimeStr:
Time = Utils.ParseTime(TimeStr)
AudioClip = self.Torchlight().AudioManager.AudioClip(player, message[1])
if not AudioClip:
return 1
return AudioClip.Play(Time)
class YouTubeSearch(BaseCommand):
import json
import datetime
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!yts"]
self.Level = 3
async def _func(self, message, player):
return -1
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if self.check_disabled(player):
return -1
Temp = DataHolder()
Time = None
if Temp(message[1].find("&t=")) != -1 or Temp(message[1].find("?t=")) != -1 or Temp(message[1].find("#t=")) != -1:
TimeStr = message[1][Temp.value + 3:].split('&')[0].split('?')[0].split('#')[0]
if TimeStr:
Time = Utils.ParseTime(TimeStr)
message[1] = message[1][:Temp.value]
Proc = await asyncio.create_subprocess_exec("youtube-dl", "--dump-json", "-xg", "ytsearch:" + message[1],
stdout = asyncio.subprocess.PIPE)
Out, _ = await Proc.communicate()
print('out value: ', Out)
url, Info = Out.split(b'\n', maxsplit = 1)
url = url.strip().decode("ascii")
Info = self.json.loads(Info)
if Info["extractor_key"] == "Youtube":
self.Torchlight().SayChat("\x07E52D27[YouTube]\x01 {0} | {1} | {2}/5.00 | {3:,}".format(
Info["title"], str(self.datetime.timedelta(seconds = Info["duration"])), round(Info["average_rating"] or 0, 2), int(Info["view_count"])))
AudioClip = self.Torchlight().AudioManager.AudioClip(player, url)
if not AudioClip:
return 1
self.Torchlight().LastUrl = url
return AudioClip.Play(Time)
class Say(BaseCommand):
import gtts
import tempfile
VALID_LANGUAGES = [lang for lang in gtts.lang.tts_langs().keys()]
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = [("!say", 4)]
self.Level = 2
async def Say(self, player, language, message):
GTTS = self.gtts.gTTS(text = message, lang = language)
TempFile = self.tempfile.NamedTemporaryFile(delete = False)
GTTS.write_to_fp(TempFile)
TempFile.close()
AudioClip = self.Torchlight().AudioManager.AudioClip(player, "file://" + TempFile.name)
if not AudioClip:
os.unlink(TempFile.name)
return 1
if AudioClip.Play():
AudioClip.AudioPlayer.AddCallback("Stop", lambda: os.unlink(TempFile.name))
return 0
else:
os.unlink(TempFile.name)
return 1
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if self.check_disabled(player):
return -1
if not message[1]:
return 1
Language = "en"
if len(message[0]) > 4:
Language = message[0][4:]
if not Language in self.VALID_LANGUAGES:
return 1
asyncio.ensure_future(self.Say(player, Language, message[1]))
return 0
'''
class DECTalk(BaseCommand):
import tempfile
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!dec"]
self.Level = 0
async def Say(self, player, message):
message = "[:phoneme on]" + message
TempFile = self.tempfile.NamedTemporaryFile(delete = False)
TempFile.close()
Proc = await asyncio.create_subprocess_exec("wine", "say.exe", "-w", TempFile.name,
cwd = "dectalk", stdin = asyncio.subprocess.PIPE)
await Proc.communicate(message.encode('utf-8', errors='ignore'))
AudioClip = self.Torchlight().AudioManager.AudioClip(player, "file://" + TempFile.name)
if not AudioClip:
os.unlink(TempFile.name)
return 1
if AudioClip.Play(None, "-af", "volume=10dB"):
AudioClip.AudioPlayer.AddCallback("Stop", lambda: os.unlink(TempFile.name))
return 0
else:
os.unlink(TempFile.name)
return 1
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if self.check_disabled(player):
return -1
if not message[1]:
return 1
asyncio.ensure_future(self.Say(player, message[1]))
return 0
'''
class Stop(BaseCommand):
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!pls"]
self.Level = 0
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
self.Torchlight().AudioManager.Stop(player, message[1])
return True
class EnableDisable(BaseCommand):
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!enable", "!disable"]
self.Level = 3
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if message[0] == "!enable":
if self.Torchlight().Disabled:
if self.Torchlight().Disabled > player.Access["level"]:
self.Torchlight().SayPrivate(player, "You don't have access to enable torchlight, since it was disabled by a higher level user.")
return 1
self.Torchlight().SayChat("Torchlight has been enabled for the duration of this map - Type !disable to disable it again.")
self.Torchlight().Disabled = False
elif message[0] == "!disable":
if self.Torchlight().Disabled > player.Access["level"]:
self.Torchlight().SayPrivate(player, "You don't have access to disable torchlight, since it was already disabled by a higher level user.")
return 1
self.Torchlight().SayChat("Torchlight has been disabled for the duration of this map - Type !enable to enable it again.")
self.Torchlight().Disabled = player.Access["level"]
class AdminAccess(BaseCommand):
from collections import OrderedDict
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!access"]
self.Level = 4
def ReloadValidUsers(self):
self.Torchlight().Access.Load()
for Player in self.Torchlight().Players:
Access = self.Torchlight().Access[Player.UniqueID]
Player.Access = Access
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
if not message[1]:
return -1
if message[1].lower() == "reload":
self.ReloadValidUsers()
self.Torchlight().SayChat("Loaded access list with {0} users".format(len(self.Torchlight().Access)))
elif message[1].lower() == "save":
self.Torchlight().Access.Save()
self.Torchlight().SayChat("Saved access list with {0} users".format(len(self.Torchlight().Access)))
# Modify access
else:
Player = None
Buf = message[1]
Temp = Buf.find(" as ")
if Temp != -1:
try:
Regname, Level = Buf[Temp + 4:].rsplit(' ', 1)
except ValueError as e:
self.Torchlight().SayChat(str(e))
return 1
Regname = Regname.strip()
Level = Level.strip()
Buf = Buf[:Temp].strip()
else:
try:
Buf, Level = Buf.rsplit(' ', 1)
except ValueError as e:
self.Torchlight().SayChat(str(e))
return 2
Buf = Buf.strip()
Level = Level.strip()
# Find user by User ID
if Buf[0] == '#' and Buf[1:].isnumeric():
Player = self.Torchlight().Players.FindUserID(int(Buf[1:]))
# Search user by name
else:
for Player_ in self.Torchlight().Players:
if Player_.Name.lower().find(Buf.lower()) != -1:
Player = Player_
break
if not Player:
self.Torchlight().SayChat("Couldn't find user: {0}".format(Buf))
return 3
if Level.isnumeric() or (Level.startswith('-') and Level[1:].isdigit()):
Level = int(Level)
if Level >= player.Access["level"] and player.Access["level"] < 10:
self.Torchlight().SayChat("Trying to assign level {0}, which is higher or equal than your level ({1})".format(Level, player.Access["level"]))
return 4
if Player.Access:
if Player.Access["level"] >= player.Access["level"] and player.Access["level"] < 10:
self.Torchlight().SayChat("Trying to modify level {0}, which is higher or equal than your level ({1})".format(Player.Access["level"], player.Access["level"]))
return 5
if "Regname" in locals():
self.Torchlight().SayChat("Changed \"{0}\"({1}) as {2} level/name from {3} to {4} as {5}".format(
Player.Name, Player.UniqueID, Player.Access["name"], Player.Access["level"], Level, Regname))
Player.Access["name"] = Regname
else:
self.Torchlight().SayChat("Changed \"{0}\"({1}) as {2} level from {3} to {4}".format(
Player.Name, Player.UniqueID, Player.Access["name"], Player.Access["level"], Level))
Player.Access["level"] = Level
self.Torchlight().Access[Player.UniqueID] = Player.Access
else:
if not "Regname" in locals():
Regname = Player.Name
self.Torchlight().Access[Player.UniqueID] = self.OrderedDict([("name", Regname), ("level", Level)])
Player.Access = self.Torchlight().Access[Player.UniqueID]
self.Torchlight().SayChat("Added \"{0}\"({1}) to access list as {2} with level {3}".format(Player.Name, Player.UniqueID, Regname, Level))
else:
if Level == "revoke" and Player.Access:
if Player.Access["level"] >= player.Access["level"] and player.Access["level"] < 10:
self.Torchlight().SayChat("Trying to revoke level {0}, which is higher or equal than your level ({1})".format(Player.Access["level"], player.Access["level"]))
return 6
self.Torchlight().SayChat("Removed \"{0}\"({1}) from access list (was {2} with level {3})".format(
Player.Name, Player.UniqueID, Player.Access["name"], Player.Access["level"]))
del self.Torchlight().Access[Player.UniqueID]
Player.Access = None
return 0
class Reload(BaseCommand):
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!reload"]
self.Level = 4
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
self.Torchlight().Reload()
return 0
"""
class Exec(BaseCommand):
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!exec"]
self.Level = 100
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
try:
Response = eval(message[1])
except Exception as e:
self.Torchlight().SayChat("Error: {0}".format(str(e)))
return 1
self.Torchlight().SayChat(str(Response))
return 0
"""

View File

@ -0,0 +1,29 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import logging
import json
import sys
class Config():
def __init__(self):
self.Logger = logging.getLogger(__class__.__name__)
self.Config = dict()
if len(sys.argv) >= 2:
self.ConfigPath = sys.argv[1]
else:
self.ConfigPath = "config.json"
self.Load()
def Load(self):
try:
with open(self.ConfigPath, "r") as fp:
self.Config = json.load(fp)
except ValueError as e:
self.Logger.error(sys._getframe().f_code.co_name + ' ' + str(e))
return 1
return 0
def __getitem__(self, key):
if key in self.Config:
return self.Config[key]
return None

View File

@ -0,0 +1,26 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
MAXPLAYERS = 65
ADMFLAG_RESERVATION = (1<<0)
ADMFLAG_GENERIC = (1<<1)
ADMFLAG_KICK = (1<<2)
ADMFLAG_BAN = (1<<3)
ADMFLAG_UNBAN = (1<<4)
ADMFLAG_SLAY = (1<<5)
ADMFLAG_CHANGEMAP = (1<<6)
ADMFLAG_CONVARS = (1<<7)
ADMFLAG_CONFIG = (1<<8)
ADMFLAG_CHAT = (1<<9)
ADMFLAG_VOTE = (1<<10)
ADMFLAG_PASSWORD = (1<<11)
ADMFLAG_RCON = (1<<12)
ADMFLAG_CHEATS = (1<<13)
ADMFLAG_ROOT = (1<<14)
ADMFLAG_CUSTOM1 = (1<<15)
ADMFLAG_CUSTOM2 = (1<<16)
ADMFLAG_CUSTOM3 = (1<<17)
ADMFLAG_CUSTOM4 = (1<<18)
ADMFLAG_CUSTOM5 = (1<<19)
ADMFLAG_CUSTOM6 = (1<<20)

View File

@ -0,0 +1,180 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import logging
import traceback
import asyncio
import datetime
import time
import socket
import struct
import sys
SAMPLEBYTES = 2
class FFmpegAudioPlayerFactory():
VALID_CALLBACKS = ["Play", "Stop", "Update"]
def __init__(self, master):
self.Logger = logging.getLogger(__class__.__name__)
self.Master = master
self.Torchlight = self.Master.Torchlight
def __del__(self):
self.Master.Logger.info("~FFmpegAudioPlayerFactory()")
self.Quit()
def NewPlayer(self):
self.Logger.debug(sys._getframe().f_code.co_name)
Player = FFmpegAudioPlayer(self)
return Player
def Quit(self):
self.Master.Logger.info("FFmpegAudioPlayerFactory->Quit()")
class FFmpegAudioPlayer():
def __init__(self, master):
self.Master = master
self.Torchlight = self.Master.Torchlight
self.Playing = False
self.Host = (
self.Torchlight().Config["VoiceServer"]["Host"],
self.Torchlight().Config["VoiceServer"]["Port"]
)
self.SampleRate = float(self.Torchlight().Config["VoiceServer"]["SampleRate"])
self.StartedPlaying = None
self.StoppedPlaying = None
self.Seconds = 0.0
self.Writer = None
self.Process = None
self.Callbacks = []
def __del__(self):
self.Master.Logger.debug("~FFmpegAudioPlayer()")
self.Stop()
def PlayURI(self, uri, position, *args):
if position:
PosStr = str(datetime.timedelta(seconds = position))
Command = ["/usr/bin/ffmpeg", "-ss", PosStr, "-i", uri, "-acodec", "pcm_s16le", "-ac", "1", "-ar", str(int(self.SampleRate)), "-f", "s16le", "-vn", *args, "-"]
else:
Command = ["/usr/bin/ffmpeg", "-i", uri, "-acodec", "pcm_s16le", "-ac", "1", "-ar", str(int(self.SampleRate)), "-f", "s16le", "-vn", *args, "-"]
print(Command)
self.Playing = True
asyncio.ensure_future(self._stream_subprocess(Command))
return True
def Stop(self, force = True):
if not self.Playing:
return False
if self.Process:
try:
self.Process.terminate()
self.Process.kill()
self.Process = None
except ProcessLookupError:
pass
if self.Writer:
if force:
Socket = self.Writer.transport.get_extra_info("socket")
if Socket:
Socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,
struct.pack("ii", 1, 0))
self.Writer.transport.abort()
self.Writer.close()
self.Playing = False
self.Callback("Stop")
del self.Callbacks
return True
def AddCallback(self, cbtype, cbfunc):
if not cbtype in FFmpegAudioPlayerFactory.VALID_CALLBACKS:
return False
self.Callbacks.append((cbtype, cbfunc))
return True
def Callback(self, cbtype, *args, **kwargs):
for Callback in self.Callbacks:
if Callback[0] == cbtype:
try:
Callback[1](*args, **kwargs)
except Exception as e:
self.Master.Logger.error(traceback.format_exc())
async def _updater(self):
LastSecondsElapsed = 0.0
while self.Playing:
SecondsElapsed = time.time() - self.StartedPlaying
if SecondsElapsed > self.Seconds:
SecondsElapsed = self.Seconds
self.Callback("Update", LastSecondsElapsed, SecondsElapsed)
if SecondsElapsed >= self.Seconds:
if not self.StoppedPlaying:
print("BUFFER UNDERRUN!")
self.Stop(False)
return
LastSecondsElapsed = SecondsElapsed
await asyncio.sleep(0.1)
async def _read_stream(self, stream, writer):
Started = False
while stream and self.Playing:
Data = await stream.read(65536)
if Data:
writer.write(Data)
await writer.drain()
Bytes = len(Data)
Samples = Bytes / SAMPLEBYTES
Seconds = Samples / self.SampleRate
self.Seconds += Seconds
if not Started:
Started = True
self.Callback("Play")
self.StartedPlaying = time.time()
asyncio.ensure_future(self._updater())
else:
self.Process = None
break
self.StoppedPlaying = time.time()
async def _stream_subprocess(self, cmd):
if not self.Playing:
return
_, self.Writer = await asyncio.open_connection(self.Host[0], self.Host[1])
Process = await asyncio.create_subprocess_exec(*cmd,
stdout = asyncio.subprocess.PIPE, stderr = asyncio.subprocess.DEVNULL)
self.Process = Process
await self._read_stream(Process.stdout, self.Writer)
await Process.wait()
if self.Seconds == 0.0:
self.Stop()

View File

@ -0,0 +1,149 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import asyncio
import logging
import traceback
class GameEvents():
def __init__(self, master):
self.Logger = logging.getLogger(__class__.__name__)
self.Torchlight = master
self.Callbacks = {}
def __del__(self):
if not len(self.Callbacks) or not self.Torchlight():
return
Obj = {
"method": "unsubscribe",
"module": "gameevents",
"events": self.Callbacks.keys()
}
asyncio.ensure_future(self.Torchlight().Send(Obj))
async def _Register(self, events):
if type(events) is not list:
events = [ events ]
Obj = {
"method": "subscribe",
"module": "gameevents",
"events": events
}
Res = await self.Torchlight().Send(Obj)
Ret = []
for i, ret in enumerate(Res["events"]):
if ret >= 0:
Ret.append(True)
if not events[i] in self.Callbacks:
self.Callbacks[events[i]] = set()
else:
Ret.append(False)
if len(Ret) == 1:
Ret = Ret[0]
return Ret
async def _Unregister(self, events):
if type(events) is not list:
events = [ events ]
Obj = {
"method": "unsubscribe",
"module": "gameevents",
"events": events
}
Res = await self.Torchlight().Send(Obj)
Ret = []
for i, ret in enumerate(Res["events"]):
if ret >= 0:
Ret.append(True)
if events[i] in self.Callbacks:
del self.Callbacks[events[i]]
else:
Ret.append(False)
if len(Ret) == 1:
Ret = Ret[0]
return Ret
def HookEx(self, event, callback):
asyncio.ensure_future(self.Hook(event, callback))
def UnhookEx(self, event, callback):
asyncio.ensure_future(self.Unhook(event, callback))
def ReplayEx(self, events):
asyncio.ensure_future(self.Replay(events))
async def Hook(self, event, callback):
if not event in self.Callbacks:
if not await self._Register(event):
return False
self.Callbacks[event].add(callback)
return True
async def Unhook(self, event, callback):
if not event in self.Callbacks:
return True
if not callback in self.Callbacks[event]:
return True
self.Callbacks[event].discard(callback)
if len(a) == 0:
return await self._Unregister(event)
return True
async def Replay(self, events):
if type(events) is not list:
events = [ events ]
for event in events[:]:
if not event in self.Callbacks:
events.remove(event)
Obj = {
"method": "replay",
"module": "gameevents",
"events": events
}
Res = await self.Torchlight().Send(Obj)
Ret = []
for i, ret in enumerate(Res["events"]):
if ret >= 0:
Ret.append(True)
else:
Ret.append(False)
if len(Ret) == 1:
Ret = Ret[0]
return Ret
def OnPublish(self, obj):
Event = obj["event"]
if not Event["name"] in self.Callbacks:
return False
Callbacks = self.Callbacks[Event["name"]]
for Callback in Callbacks:
try:
Callback(**Event["data"])
except Exception as e:
self.Logger.error(traceback.format_exc())
self.Logger.error(Event)
return True

View File

@ -0,0 +1,201 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import asyncio
import logging
import numpy
from .Constants import *
class PlayerManager():
def __init__(self, master):
self.Logger = logging.getLogger(__class__.__name__)
self.Torchlight = master
self.Players = numpy.empty(MAXPLAYERS + 1, dtype = object)
self.Storage = self.StorageManager(self)
self.Torchlight().GameEvents.HookEx("player_connect", self.Event_PlayerConnect)
self.Torchlight().GameEvents.HookEx("player_activate", self.Event_PlayerActivate)
self.Torchlight().Forwards.HookEx("OnClientPostAdminCheck", self.OnClientPostAdminCheck)
self.Torchlight().GameEvents.HookEx("player_info", self.Event_PlayerInfo)
self.Torchlight().GameEvents.HookEx("player_disconnect", self.Event_PlayerDisconnect)
self.Torchlight().GameEvents.HookEx("server_spawn", self.Event_ServerSpawn)
def Event_PlayerConnect(self, name, index, userid, networkid, address, bot):
index += 1
self.Logger.info("OnConnect(name={0}, index={1}, userid={2}, networkid={3}, address={4}, bot={5})"
.format(name, index, userid, networkid, address, bot))
if self.Players[index] != None:
self.Logger.error("!!! Player already exists, overwriting !!!")
self.Players[index] = self.Player(self, index, userid, networkid, address, name)
self.Players[index].OnConnect()
def Event_PlayerActivate(self, userid):
self.Logger.info("Pre_OnActivate(userid={0})".format(userid))
index = self.FindUserID(userid).Index
self.Logger.info("OnActivate(index={0}, userid={1})".format(index, userid))
self.Players[index].OnActivate()
def OnClientPostAdminCheck(self, client):
self.Logger.info("OnClientPostAdminCheck(client={0})".format(client))
asyncio.ensure_future(self.Players[client].OnClientPostAdminCheck())
def Event_PlayerInfo(self, name, index, userid, networkid, bot):
index += 1
self.Logger.info("OnInfo(name={0}, index={1}, userid={2}, networkid={3}, bot={4})"
.format(name, index, userid, networkid, bot))
# We've connected to the server and receive info events about the already connected players
# Emulate connect message
if not self.Players[index]:
self.Event_PlayerConnect(name, index - 1, userid, networkid, bot)
else:
self.Players[index].OnInfo(name)
def Event_PlayerDisconnect(self, userid, reason, name, networkid, bot):
index = self.FindUserID(userid).Index
self.Logger.info("OnDisconnect(index={0}, userid={1}, reason={2}, name={3}, networkid={4}, bot={5})"
.format(index, userid, reason, name, networkid, bot))
self.Players[index].OnDisconnect(reason)
self.Players[index] = None
def Event_ServerSpawn(self, hostname, address, ip, port, game, mapname, maxplayers, os, dedicated, password):
self.Logger.info("ServerSpawn(mapname={0})"
.format(mapname))
self.Storage.Reset()
for i in range(1, self.Players.size):
if self.Players[i]:
self.Players[i].OnDisconnect("mapchange")
self.Players[i].OnConnect()
def FindUniqueID(self, uniqueid):
for Player in self.Players:
if Player and Player.UniqueID == uniqueid:
return Player
def FindUserID(self, userid):
for Player in self.Players:
if Player and Player.UserID == userid:
return Player
def FindName(self, name):
for Player in self.Players:
if Player and Player.Name == name:
return Player
def __len__(self):
Count = 0
for i in range(1, self.Players.size):
if self.Players[i]:
Count += 1
return Count
def __setitem__(self, key, value):
if key > 0 and key <= MAXPLAYERS:
self.Players[key] = value
def __getitem__(self, key):
if key > 0 and key <= MAXPLAYERS:
return self.Players[key]
def __iter__(self):
for i in range(1, self.Players.size):
if self.Players[i]:
yield self.Players[i]
class StorageManager():
def __init__(self, master):
self.PlayerManager = master
self.Storage = dict()
def Reset(self):
self.Storage = dict()
def __getitem__(self, key):
if not key in self.Storage:
self.Storage[key] = dict()
return self.Storage[key]
class Admin():
def __init__(self):
self._FlagBits = 0
def FlagBits(self):
return self._FlagBits
def Reservation(self): return (self._FlagBits & ADMFLAG_RESERVATION)
def Generic(self): return (self._FlagBits & ADMFLAG_GENERIC)
def Kick(self): return (self._FlagBits & ADMFLAG_KICK)
def Ban(self): return (self._FlagBits & ADMFLAG_BAN)
def Unban(self): return (self._FlagBits & ADMFLAG_UNBAN)
def Slay(self): return (self._FlagBits & ADMFLAG_SLAY)
def Changemap(self): return (self._FlagBits & ADMFLAG_CHANGEMAP)
def Convars(self): return (self._FlagBits & ADMFLAG_CONVARS)
def Config(self): return (self._FlagBits & ADMFLAG_CONFIG)
def Chat(self): return (self._FlagBits & ADMFLAG_CHAT)
def Vote(self): return (self._FlagBits & ADMFLAG_VOTE)
def Password(self): return (self._FlagBits & ADMFLAG_PASSWORD)
def RCON(self): return (self._FlagBits & ADMFLAG_RCON)
def Cheats(self): return (self._FlagBits & ADMFLAG_CHEATS)
def Root(self): return (self._FlagBits & ADMFLAG_ROOT)
def Custom1(self): return (self._FlagBits & ADMFLAG_CUSTOM1)
def Custom2(self): return (self._FlagBits & ADMFLAG_CUSTOM2)
def Custom3(self): return (self._FlagBits & ADMFLAG_CUSTOM3)
def Custom4(self): return (self._FlagBits & ADMFLAG_CUSTOM4)
def Custom5(self): return (self._FlagBits & ADMFLAG_CUSTOM5)
def Custom6(self): return (self._FlagBits & ADMFLAG_CUSTOM6)
class Player():
def __init__(self, master, index, userid, uniqueid, address, name):
self.PlayerManager = master
self.Torchlight = self.PlayerManager.Torchlight
self.Index = index
self.UserID = userid
self.UniqueID = uniqueid
self.Address = address
self.Name = name
self.Access = None
self.Admin = self.PlayerManager.Admin()
self.Storage = None
self.Active = False
self.ChatCooldown = 0
def OnConnect(self):
self.Storage = self.PlayerManager.Storage[self.UniqueID]
if not "Audio" in self.Storage:
self.Storage["Audio"] = dict({"Uses": 0, "LastUse": 0.0, "LastUseLength": 0.0, "TimeUsed": 0.0})
self.Access = self.Torchlight().Access[self.UniqueID]
def OnActivate(self):
self.Active = True
async def OnClientPostAdminCheck(self):
self.Admin._FlagBits = (await self.Torchlight().API.GetUserFlagBits(self.Index))["result"]
self.PlayerManager.Logger.info("#{0} \"{1}\"({2}) FlagBits: {3}".format(self.UserID, self.Name, self.UniqueID, self.Admin._FlagBits))
if not self.Access:
if self.Admin.RCON():
self.Access = dict({"level": 6, "name": "SAdmin"})
elif self.Admin.Generic():
self.Access = dict({"level": 3, "name": "Admin"})
elif self.Admin.Custom1():
self.Access = dict({"level": 1, "name": "VIP"})
if self.PlayerManager.Torchlight().Config["DefaultLevel"]:
if self.Access and self.Access["level"] < self.PlayerManager.Torchlight().Config["DefaultLevel"]:
self.Access = dict({"level": self.PlayerManager.Torchlight().Config["DefaultLevel"], "name": "Default"})
def OnInfo(self, name):
self.Name = name
def OnDisconnect(self, message):
self.Active = False
self.Storage = None
self.Torchlight().AudioManager.OnDisconnect(self)

View File

@ -0,0 +1,27 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import functools
class SourceModAPI:
def __init__(self, master):
self.Torchlight = master
def __getattr__(self, attr):
try:
return super(SourceModAPI, self).__getattr__(attr)
except AttributeError:
return functools.partial(self._MakeCall, attr)
async def _MakeCall(self, function, *args, **kwargs):
Obj = {
"method": "function",
"function": function,
"args": args
}
Res = await self.Torchlight().Send(Obj)
if Res["error"]:
raise Exception("{0}({1})\n{2}".format(function, args, Res["error"]))
return Res

View File

@ -0,0 +1,106 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import logging
import asyncio
import sys
import socket
import struct
import time
import traceback
from importlib import reload
from .PlayerManager import PlayerManager
class SourceRCONServer():
class SourceRCONClient():
def __init__(self, Server, Socket, Name):
self.Loop = Server.Loop
self.Server = Server
self._sock = Socket
self.Name = Name
self.Authenticated = False
asyncio.Task(self._peer_handler())
def send(self, data):
return self.Loop.sock_sendall(self._sock, data)
@asyncio.coroutine
def _peer_handler(self):
try:
yield from self._peer_loop()
except IOError:
pass
finally:
self.Server.Remove(self)
@asyncio.coroutine
def _peer_loop(self):
while True:
Data = yield from self.Loop.sock_recv(self._sock, 1024)
if Data == b'':
break
while Data:
p_size = struct.unpack("<l", Data[:4])[0]
if len(Data) < p_size+4:
break
self.ParsePacket(Data[:p_size+4])
Data = Data[p_size+4:]
def p_send(self, p_id, p_type, p_body):
Data = struct.pack('<l', p_id) + struct.pack('<l', p_type) + p_body.encode("UTF-8") + b'\x00\x00'
self.send(struct.pack('<l', len(Data)) + Data)
def ParsePacket(self, Data):
p_size, p_id, p_type = struct.unpack('<lll', Data[:12])
Data = Data[12:p_size+2].decode(encoding="UTF-8", errors="ignore").split('\x00')[0]
if not self.Authenticated:
if p_type == 3:
if Data == self.Server.Password:
self.Authenticated = True
self.Server.Logger.info(sys._getframe().f_code.co_name + " Connection authenticated from {0}".format(self.Name))
self.p_send(p_id, 0 , '')
self.p_send(p_id, 2 , '')
self.p_send(p_id, 0, "Welcome to torchlight! - Authenticated!\n")
else:
self.Server.Logger.info(sys._getframe().f_code.co_name + " Connection denied from {0}".format(self.Name))
self.p_send(p_id, 0 , '')
self.p_send(-1, 2 , '')
self._sock.close()
"""else:
if p_type == 2:
if Data:
Data = Data.strip('"')
self.Server.Logger.info(sys._getframe().f_code.co_name + " Exec: \"{0}\"".format(Data))
Player = PlayerManager.Player(self.Server.TorchlightHandler.Torchlight.Players, 0, 0, "[CONSOLE]", "127.0.0.1", "CONSOLE")
Player.Access = dict({"name": "CONSOLE", "level": 9001})
Player.Storage = dict({"Audio": {"Uses": 0, "LastUse": 0.0, "LastUseLength": 0.0, "TimeUsed": 0.0}})
asyncio.Task(self.Server.TorchlightHandler.Torchlight.CommandHandler.HandleCommand(Data, Player))
#self.p_send(p_id, 0, self._server.torchlight.GetLine())
"""
def __init__(self, Loop, TorchlightHandler, Host="", Port=27015, Password="secret"):
self.Logger = logging.getLogger(__class__.__name__)
self.Loop = Loop
self._serv_sock = socket.socket()
self._serv_sock.setblocking(0)
self._serv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._serv_sock.bind((Host, Port))
self._serv_sock.listen(5)
self.Peers = []
self.TorchlightHandler = TorchlightHandler
self.Password = Password
"""asyncio.Task(self._server())
def Remove(self, Peer):
self.Logger.info(sys._getframe().f_code.co_name + " Peer {0} disconnected!".format(Peer.Name))
self.Peers.remove(Peer)
@asyncio.coroutine
def _server(self):
while True:
PeerSocket, PeerName = yield from self.Loop.sock_accept(self._serv_sock)
PeerSocket.setblocking(0)
Peer = self.SourceRCONClient(self, PeerSocket, PeerName)
self.Peers.append(Peer)
self.Logger.info(sys._getframe().f_code.co_name + " Peer {0} connected!".format(Peer.Name))"""

View File

@ -0,0 +1,159 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import asyncio
import logging
import traceback
class SubscribeBase():
def __init__(self, master, module):
self.Logger = logging.getLogger(__class__.__name__)
self.Torchlight = master
self.Module = module
self.Callbacks = {}
def __del__(self):
if not len(self.Callbacks) or not self.Torchlight():
return
Obj = {
"method": "unsubscribe",
"module": self.Module,
"events": self.Callbacks.keys()
}
asyncio.ensure_future(self.Torchlight().Send(Obj))
async def _Register(self, events):
if type(events) is not list:
events = [ events ]
Obj = {
"method": "subscribe",
"module": self.Module,
"events": events
}
Res = await self.Torchlight().Send(Obj)
Ret = []
for i, ret in enumerate(Res["events"]):
if ret >= 0:
Ret.append(True)
if not events[i] in self.Callbacks:
self.Callbacks[events[i]] = set()
else:
Ret.append(False)
if len(Ret) == 1:
Ret = Ret[0]
return Ret
async def _Unregister(self, events):
if type(events) is not list:
events = [ events ]
Obj = {
"method": "unsubscribe",
"module": self.Module,
"events": events
}
Res = await self.Torchlight().Send(Obj)
Ret = []
for i, ret in enumerate(Res["events"]):
if ret >= 0:
Ret.append(True)
if events[i] in self.Callbacks:
del self.Callbacks[events[i]]
else:
Ret.append(False)
if len(Ret) == 1:
Ret = Ret[0]
return Ret
def HookEx(self, event, callback):
asyncio.ensure_future(self.Hook(event, callback))
def UnhookEx(self, event, callback):
asyncio.ensure_future(self.Unhook(event, callback))
def ReplayEx(self, events):
asyncio.ensure_future(self.Replay(events))
async def Hook(self, event, callback):
if not event in self.Callbacks:
if not await self._Register(event):
return False
self.Callbacks[event].add(callback)
return True
async def Unhook(self, event, callback):
if not event in self.Callbacks:
return True
if not callback in self.Callbacks[event]:
return True
self.Callbacks[event].discard(callback)
if len(a) == 0:
return await self._Unregister(event)
return True
async def Replay(self, events):
if type(events) is not list:
events = [ events ]
for event in events[:]:
if not event in self.Callbacks:
events.remove(event)
Obj = {
"method": "replay",
"module": self.Module,
"events": events
}
Res = await self.Torchlight().Send(Obj)
Ret = []
for i, ret in enumerate(Res["events"]):
if ret >= 0:
Ret.append(True)
else:
Ret.append(False)
if len(Ret) == 1:
Ret = Ret[0]
return Ret
def OnPublish(self, obj):
Event = obj["event"]
if not Event["name"] in self.Callbacks:
return False
Callbacks = self.Callbacks[Event["name"]]
for Callback in Callbacks:
try:
Callback(**Event["data"])
except Exception as e:
self.Logger.error(traceback.format_exc())
self.Logger.error(Event)
return True
class GameEvents(SubscribeBase):
def __init__(self, master):
super().__init__(master, "gameevents")
class Forwards(SubscribeBase):
def __init__(self, master):
super().__init__(master, "forwards")

View File

@ -0,0 +1,149 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import logging
import asyncio
import sys
import json
import time
import weakref
import traceback
import textwrap
from .AsyncClient import AsyncClient
from .SourceModAPI import SourceModAPI
from .Subscribe import GameEvents, Forwards
from .Utils import Utils
from .Config import Config
from .CommandHandler import CommandHandler
from .AccessManager import AccessManager
from .PlayerManager import PlayerManager
from .AudioManager import AudioManager
class Torchlight():
def __init__(self, master):
self.Logger = logging.getLogger(__class__.__name__)
self.Master = master
self.Config = self.Master.Config
self.WeakSelf = weakref.ref(self)
self.API = SourceModAPI(self.WeakSelf)
self.GameEvents = GameEvents(self.WeakSelf)
self.Forwards = Forwards(self.WeakSelf)
self.DisableVotes = set()
self.Disabled = 0
self.LastUrl = None
def InitModules(self):
self.Access = AccessManager()
self.Access.Load()
self.Players = PlayerManager(self.WeakSelf)
self.AudioManager = AudioManager(self.WeakSelf)
self.CommandHandler = CommandHandler(self.WeakSelf)
self.CommandHandler.Setup()
self.GameEvents.HookEx("server_spawn", self.Event_ServerSpawn)
self.GameEvents.HookEx("player_say", self.Event_PlayerSay)
def SayChat(self, message, player=None):
message = "\x0700FFFA[Torchlight]: \x01{0}".format(message)
if len(message) > 976:
message = message[:973] + "..."
lines = textwrap.wrap(message, 244, break_long_words = True)
for line in lines:
asyncio.ensure_future(self.API.PrintToChatAll(line))
if player:
Level = 0
if player.Access:
Level = player.Access["level"]
if Level < self.Config["AntiSpam"]["ImmunityLevel"]:
cooldown = len(lines) * self.Config["AntiSpam"]["ChatCooldown"]
if player.ChatCooldown > self.Master.Loop.time():
player.ChatCooldown += cooldown
else:
player.ChatCooldown = self.Master.Loop.time() + cooldown
def SayPrivate(self, player, message):
message = "\x0700FFFA[Torchlight]: \x01{0}".format(message)
if len(message) > 976:
message = message[:973] + "..."
lines = textwrap.wrap(message, 244, break_long_words = True)
for line in lines:
asyncio.ensure_future(self.API.PrintToChat(player.Index, line))
def Reload(self):
self.Config.Load()
self.CommandHandler.NeedsReload = True
async def Send(self, data):
return await self.Master.Send(data)
def OnPublish(self, obj):
if obj["module"] == "gameevents":
self.GameEvents.OnPublish(obj)
elif obj["module"] == "forwards":
self.Forwards.OnPublish(obj)
def Event_ServerSpawn(self, hostname, address, ip, port, game, mapname, maxplayers, os, dedicated, password):
self.DisableVotes = set()
self.Disabled = 0
def Event_PlayerSay(self, userid, text):
if userid == 0:
return
Player = self.Players.FindUserID(userid)
asyncio.ensure_future(self.CommandHandler.HandleCommand(text, Player))
def __del__(self):
self.Logger.debug("~Torchlight()")
class TorchlightHandler():
def __init__(self, loop):
self.Logger = logging.getLogger(__class__.__name__)
self.Loop = loop if loop else asyncio.get_event_loop()
self._Client = None
self.Torchlight = None
self.Config = Config()
asyncio.ensure_future(self._Connect(), loop = self.Loop)
async def _Connect(self):
# Connect to API
self._Client = AsyncClient(self.Loop, self.Config["SMAPIServer"]["Host"], self.Config["SMAPIServer"]["Port"], self)
await self._Client.Connect()
self.Torchlight = Torchlight(self)
# Pre Hook for late load
await self.Torchlight.GameEvents._Register(["player_connect", "player_activate"])
await self.Torchlight.Forwards._Register(["OnClientPostAdminCheck"])
self.Torchlight.InitModules()
# Late load
await self.Torchlight.GameEvents.Replay(["player_connect", "player_activate"])
await self.Torchlight.Forwards.Replay(["OnClientPostAdminCheck"])
async def Send(self, data):
return await self._Client.Send(data)
def OnPublish(self, obj):
self.Torchlight.OnPublish(obj)
def OnDisconnect(self, exc):
self.Logger.info("OnDisconnect({0})".format(exc))
self.Torchlight = None
asyncio.ensure_future(self._Connect(), loop = self.Loop)
def __del__(self):
self.Logger.debug("~TorchlightHandler()")

View File

@ -0,0 +1,150 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import logging
import asyncio
import sys
import json
import time
import weakref
import traceback
import textwrap
from .AsyncClient import AsyncClient
from .SourceModAPI import SourceModAPI
from .Subscribe import GameEvents, Forwards
from .Utils import Utils
from .Config import Config
from .CommandHandlermg import CommandHandlermg
from .AccessManager import AccessManager
from .PlayerManager import PlayerManager
from .AudioManager import AudioManager
class Torchlight():
def __init__(self, master):
self.Logger = logging.getLogger(__class__.__name__)
self.Master = master
self.Config = self.Master.Config
self.WeakSelf = weakref.ref(self)
self.API = SourceModAPI(self.WeakSelf)
self.GameEvents = GameEvents(self.WeakSelf)
self.Forwards = Forwards(self.WeakSelf)
self.DisableVotes = set()
self.Disabled = 0
self.LastUrl = None
def InitModules(self):
self.Access = AccessManager()
self.Access.Load()
self.Players = PlayerManager(self.WeakSelf)
self.AudioManager = AudioManager(self.WeakSelf)
self.CommandHandler = CommandHandlermg(self.WeakSelf)
self.CommandHandler.Setup()
self.GameEvents.HookEx("server_spawn", self.Event_ServerSpawn)
self.GameEvents.HookEx("player_say", self.Event_PlayerSay)
def SayChat(self, message, player=None):
message = "\x0700FFFA[Torchlight]: \x01{0}".format(message)
if len(message) > 976:
message = message[:973] + "..."
lines = textwrap.wrap(message, 244, break_long_words = True)
for line in lines:
asyncio.ensure_future(self.API.PrintToChatAll(line))
if player:
Level = 0
if player.Access:
Level = player.Access["level"]
if Level < self.Config["AntiSpam"]["ImmunityLevel"]:
cooldown = len(lines) * self.Config["AntiSpam"]["ChatCooldown"]
if player.ChatCooldown > self.Master.Loop.time():
player.ChatCooldown += cooldown
else:
player.ChatCooldown = self.Master.Loop.time() + cooldown
def SayPrivate(self, player, message):
message = "\x0700FFFA[Torchlight]: \x01{0}".format(message)
if len(message) > 976:
message = message[:973] + "..."
lines = textwrap.wrap(message, 244, break_long_words = True)
for line in lines:
asyncio.ensure_future(self.API.PrintToChat(player.Index, line))
def Reload(self):
self.Config.Load()
self.CommandHandler.NeedsReload = True
async def Send(self, data):
return await self.Master.Send(data)
def OnPublish(self, obj):
if obj["module"] == "gameevents":
self.GameEvents.OnPublish(obj)
elif obj["module"] == "forwards":
self.Forwards.OnPublish(obj)
def Event_ServerSpawn(self, hostname, address, ip, port, game, mapname, maxplayers, os, dedicated, password):
self.DisableVotes = set()
self.Disabled = 0
def Event_PlayerSay(self, userid, text):
if userid == 0:
return
Player = self.Players.FindUserID(userid)
asyncio.ensure_future(self.CommandHandler.HandleCommand(text, Player))
def __del__(self):
self.Logger.debug("~Torchlight()")
class TorchlightHandler():
def __init__(self, loop):
self.Logger = logging.getLogger(__class__.__name__)
self.Loop = loop if loop else asyncio.get_event_loop()
self._Client = None
self.Torchlight = None
self.Config = Config()
asyncio.ensure_future(self._Connect(), loop = self.Loop)
async def _Connect(self):
# Connect to API
self._Client = AsyncClient(self.Loop, self.Config["SMAPIServer"]["Host"], self.Config["SMAPIServer"]["Port"], self)
await self._Client.Connect()
self.Torchlight = Torchlight(self)
# Pre Hook for late load
await self.Torchlight.GameEvents._Register(["player_connect", "player_activate"])
await self.Torchlight.Forwards._Register(["OnClientPostAdminCheck"])
self.Torchlight.InitModules()
# Late load
await self.Torchlight.GameEvents.Replay(["player_connect", "player_activate"])
await self.Torchlight.Forwards.Replay(["OnClientPostAdminCheck"])
async def Send(self, data):
return await self._Client.Send(data)
def OnPublish(self, obj):
self.Torchlight.OnPublish(obj)
def OnDisconnect(self, exc):
self.Logger.info("OnDisconnect({0})".format(exc))
self.Torchlight = None
asyncio.ensure_future(self._Connect(), loop = self.Loop)
def __del__(self):
self.Logger.debug("~TorchlightHandler()")

View File

@ -0,0 +1,94 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import math
class DataHolder:
def __init__(self, value=None, attr_name='value'):
self._attr_name = attr_name
self.set(value)
def __call__(self, value):
return self.set(value)
def set(self, value):
setattr(self, self._attr_name, value)
return value
def get(self):
return getattr(self, self._attr_name)
class Utils():
@staticmethod
def GetNum(Text):
Ret = ''
for c in Text:
if c.isdigit():
Ret += c
elif Ret:
break
elif c == '-':
Ret += c
return Ret
@staticmethod
def ParseTime(TimeStr):
Negative = False
Time = 0
while TimeStr:
Val = Utils.GetNum(TimeStr)
if not Val:
break
Val = int(Val)
if not Val:
break
if Val < 0:
TimeStr = TimeStr[1:]
if Time == 0:
Negative = True
Val = abs(Val)
ValLen = int(math.log10(Val)) + 1
if len(TimeStr) > ValLen:
Mult = TimeStr[ValLen].lower()
TimeStr = TimeStr[ValLen + 1:]
if Mult == 'h':
Val *= 3600
elif Mult == 'm':
Val *= 60
else:
TimeStr = None
Time += Val
if Negative:
return -Time
else:
return Time
@staticmethod
def HumanSize(size_bytes):
"""
format a size in bytes into a 'human' file size, e.g. bytes, KB, MB, GB, TB, PB
Note that bytes/KB will be reported in whole numbers but MB and above will have greater precision
e.g. 1 byte, 43 bytes, 443 KB, 4.3 MB, 4.43 GB, etc
"""
if size_bytes == 1:
# because I really hate unnecessary plurals
return "1 byte"
suffixes_table = [('bytes', 0),('KB', 0),('MB', 1),('GB', 2),('TB', 2), ('PB', 2)]
num = float(size_bytes)
for suffix, precision in suffixes_table:
if num < 1024.0:
break
num /= 1024.0
if precision == 0:
formatted_size = str(int(num))
else:
formatted_size = str(round(num, ndigits=precision))
return "{0}{1}".format(formatted_size, suffix)

View File

@ -0,0 +1,2 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-

View File

@ -0,0 +1,4 @@
#!/bin/bash
cd "$(dirname "$0")"
source venv/bin/activate
python3 main.py

View File

@ -0,0 +1,4 @@
#!/bin/bash
cd "$(dirname "$0")"
source venv/bin/activate
python3 mainmg.py config-mg.json

View File

@ -0,0 +1,22 @@
{
"[U:1:69566635]": {
"level": 100,
"name": "Jenz"
},
"[U:1:64494019]": {
"level": 100,
"name": "Neon"
},
"[U:1:126555404]": {
"level": 3,
"name": "Migza"
},
"[U:1:20383465]": {
"name": "WASD",
"level": 3
},
"[U:1:28627906]": {
"name": "Berry",
"level": 1337
}
}

View File

@ -0,0 +1,37 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import logging
import asyncio
import os
import sys
import threading
import traceback
import gc
from importlib import reload
global TorchMaster
import Torchlight.Torchlight
from Torchlight.SourceRCONServer import SourceRCONServer
if __name__ == '__main__':
logging.basicConfig(
level = logging.DEBUG,
format = "[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s",
datefmt = "%H:%M:%S"
)
Loop = asyncio.get_event_loop()
global TorchMaster
TorchMaster = Torchlight.Torchlight.TorchlightHandler(Loop)
# Handles new connections on 0.0.0.0:27015
RCONConfig = TorchMaster.Config["TorchRCON"]
"""RCONServer = SourceRCONServer(Loop, TorchMaster,
Host = RCONConfig["Host"],
Port = RCONConfig["Port"],
Password = RCONConfig["Password"])"""
# Run!
Loop.run_forever()

View File

@ -0,0 +1,37 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import logging
import asyncio
import os
import sys
import threading
import traceback
import gc
from importlib import reload
global TorchMaster
import Torchlight.Torchlightmg
from Torchlight.SourceRCONServer import SourceRCONServer
if __name__ == '__main__':
logging.basicConfig(
level = logging.DEBUG,
format = "[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s",
datefmt = "%H:%M:%S"
)
Loop = asyncio.get_event_loop()
global TorchMaster
TorchMaster = Torchlight.Torchlightmg.TorchlightHandler(Loop)
# Handles new connections on 0.0.0.0:27015
RCONConfig = TorchMaster.Config["TorchRCON"]
"""RCONServer = SourceRCONServer(Loop, TorchMaster,
Host = RCONConfig["Host"],
Port = RCONConfig["Port"],
Password = RCONConfig["Password"])"""
# Run!
Loop.run_forever()

View File

@ -0,0 +1,23 @@
aiohttp
appdirs
async-timeout
beautifulsoup4
certifi
chardet
gTTS
gTTS-token
idna
lxml
multidict
numpy
olefile
packaging
Pillow
pyparsing
python-magic
requests
six
urllib3
yarl
cython
geoip2

View File

@ -0,0 +1,5 @@
#!/bin/bash
screen kill torchlight
screen kill torchlight-noct
screen -L -A -m -d -S torchlight ./_start.sh
screen -L -A -m -d -S torchlight-mg ./_start_mg.sh

View File

@ -0,0 +1,405 @@
[
{"names": ["!pyramid"], "sound": "ascream7.mp3"},
{"names": ["!applause"], "sound": "applause.wav"},
{"names": ["!seeyou", "!seeyouagain"], "sound": "sephiseeyou.mp3"},
{"names": ["!chosen"], "sound": "sephonlythechosen.mp3"},
{"names": ["!laser"], "sound": "bladeout.wav"},
{"names": ["!prepare", "!battle"], "sound": "prepareforbattle.mp3"},
{"names": ["!tuturu"], "sound": "tuturu.mp3"},
{"names": ["!baka"], "sound": "baka.wav"},
{"names": ["!omae"], "sound": "Omae.mp3"},
{"names": ["!poi"], "sound": "poi.wav"},
{"names": ["!shit"], "sound": "shit.wav"},
{"names": ["!sugoi"], "sound": "sugoi.wav"},
{"names": ["!allahu", "!akbar"], "sound": "allahu_akbar1.mp3"},
{"names": ["!allahu2", "!akbar2"], "sound": "AllahuAkbarDubstep.mp3"},
{"names": ["!quack"], "sound": "quack.mp3"},
{"names": ["!english"], "sound": "english.wav"},
{"names": ["!squad", "!suicidesquad"], "sound": "suicidesquad.mp3"},
{"names": ["!cena"], "sound": "john.mp3"},
{"names": ["!solo"], "sound": "solo.mp3"},
{"names": ["!popopo"], "sound": "popopoguys1.mp3"},
{"names": ["!tageule"], "sound": "tagueule.wav"},
{"names": ["!idiot"], "sound": "omguidiot.mp3"},
{"names": ["!game"], "sound": "isonly1.mp3"},
{"names": ["!wah"], "sound": "wah wah sound effect.mp3"},
{"names": ["!yes"], "sound": ["ohyes.mp3", "yes.mp3", "m-bison-yes-yes.mp3", "yesyesyesjojo.mp3"]},
{"names": ["!overconfidence"], "sound": "Monster_Kill.wav"},
{"names": ["!beer", "!pussy"], "sound": "grababeer.mp3"},
{"names": ["!nein"], "sound": "Nein nein nein2.mp3"},
{"names": ["!ayy", "!lmao"], "sound": "AyyyLmao_louer.mp3"},
{"names": ["!blyat"], "sound": "BLYAT_louder.mp3"},
{"names": ["!monkey", "!language"], "sound": "NomnkyLanguage_louder.mp3"},
{"names": ["!wanker"], "sound": "OIYAWANKER_louder.mp3"},
{"names": ["!vortex"], "sound": "WAAAAhByVortexx_louder.mp3"},
{"names": ["!slutty"], "sound": "sluttyrainbow.wav"},
{"names": ["!hello"], "sound": "Hello2.mp3"},
{"names": ["!george"], "sound": "George.mp3"},
{"names": ["!george2"], "sound": "George2.mp3"},
{"names": ["!erebus"], "sound": "erebus.wav"},
{"names": ["!kabzor", "!neon"], "sound": "kabzorrage.wav"},
{"names": ["!otominas"], "sound": "Otominas.mp3"},
{"names": ["!airhorn"], "sound": "AIRHORN.mp3"},
{"names": ["!reee", "!reeee"], "sound": "reeeeeeee_v2.mp3"},
{"names": ["!wombo"], "sound": "wombo combo2.mp3"},
{"names": ["!vaperwave"], "sound": "Vaperwave.mp3"},
{"names": ["!roasted"], "sound": "roasted2.mp3"},
{"names": ["!ogre"], "sound": "its all ogre now.mp3"},
{"names": ["!failed"], "sound": "Mission Failed.mp3"},
{"names": ["!youmad"], "sound": "meshlem_arrive2_good.mp3"},
{"names": ["!daniel"], "sound": "daniel.mp3"},
{"names": ["!action"], "sound": "Action is Coming.mp3"},
{"names": ["!brucelee"], "sound": "Ugandan Bruce Lee.mp3"},
{"names": ["!gwegwe"], "sound": "Gwe Gwe Gwe.mp3"},
{"names": ["!bruceu"], "sound": "We Call Him Bruce U.mp3"},
{"names": ["!commando"], "sound": "COMMAND COMMANDO PagChomp.wav"},
{"names": ["!nuffin"], "sound": "DINDU NUFFIN.wav"},
{"names": ["!dinosaurs"], "sound": "DINOSAURS ZULUL.wav"},
{"names": ["!deadlyactions"], "sound": "NON STOP DEADLY ACTIONS PagChomp.wav"},
{"names": ["!gimme"], "sound": "GIMME GIMME GIMME GIMME.wav"},
{"names": ["!skicker"], "sound": "SUPA KICKER.wav"},
{"names": ["!sbiker"], "sound": "SUPA BIKER.wav"},
{"names": ["!sfighter"], "sound": "SUPA FIGHTER.wav"},
{"names": ["!smafia"], "sound": "SUPA MAFIA.wav"},
{"names": ["!tigercage"], "sound": "THE TIGER IS IN THE CAGE.wav"},
{"names": ["!serious"], "sound": "THIS IS SERIOUS.wav"},
{"names": ["!hehehe"], "sound": "HE HE HE.wav"},
{"names": ["!tigermafia"], "sound": "TIGER MAFIA.wav"},
{"names": ["!cheeki"], "sound": "AH NU CHEEKI BREEKI IV DAMKE.mp3"},
{"names": ["_legend"], "sound": "legend.mp3"},
{"names": ["!admun_pls"], "sound": "Admun Please Electro (Short).mp3"},
{"names": ["!dolphin"], "sound": "Dolfin - Song.mp3"},
{"names": ["!omg_remix"], "sound": "O my Gah (Remix #3).mp3"},
{"names": ["!rainbow_panda"], "sound": "ITS RAINBOW PANDA.mp3"},
{"names": ["!dong"], "sound": "Ding Dong.mp3"},
{"names": ["!ching_club"], "sound": "Ching Chong (Remix #9) Club.mp3"},
{"names": ["!tonguetwister"], "sound": "tongue-twister.mp3"},
{"names": ["!cooler"], "sound": "cooler.mp3"},
{"names": ["!fatality"], "sound": "fatality.mp3"},
{"names": ["!inception"], "sound": "inceptionbutton.mp3"},
{"names": ["!leroy"], "sound": "leroy.mp3"},
{"names": ["!ecchi"], "sound": "ecchidarling.mp3"},
{"names": ["!moe"], "sound": "moe.mp3"},
{"names": ["!nants"], "sound": "nants_ingonyama_bagithi_baba_-_the_circle_of_life-mp3cut.mp3"},
{"names": ["!nyanpasu"], "sound": "nyanpass_2.mp3"},
{"names": ["!oww"], "sound": "oww.mp3"},
{"names": ["!wololo"], "sound": "wololo.mp3"},
{"names": ["!yuuta"], "sound": "yuuuta_itaiiii.mp3"},
{"names": ["!zawarudo"], "sound": "za-warudo-stop-time-sound.mp3"},
{"names": ["!slow"], "sound": "seph_slow.wav"},
{"names": ["!tuturu"], "sound": "Tutturuu_v1.wav"},
{"names": ["!baka"], "sound": ["baka.wav", "Baka.mp3"]},
{"names": ["!buhi"], "sound": "buhi.wav"},
{"names": ["!nyan"], "sound": "nyanpass.wav"},
{"names": ["!trial"], "sound": "trial.wav"},
{"names": ["!mail"], "sound": "yuu_new_mail.wav"},
{"names": ["!balrog"], "sound": "balrog_scream.wav"},
{"names": ["!bluescreen", "!error"], "sound": "error.wav"},
{"names": ["!hallelujah"], "sound": "hallelujah.wav"},
{"names": ["!trap"], "sound": "itsatrap.wav"},
{"names": ["!whores"], "sound": "whores.wav"},
{"names": ["!gandalf"], "sound": "vozyoucannotpass.wav"},
{"names": ["!cat"], "sound": "gato.wav"},
{"names": ["!turtle", "!turtles"], "sound": ["iliketurtles.wav", "wherearetheturtles.mp3"]},
{"names": ["!laser"], "sound": "blade_out.wav"},
{"names": ["!king", "!kingoftheworld"], "sound": "vulcan.wav"},
{"names": ["!seeyou", "!seeyouagain"], "sound": "seph_iseeyou.wav"},
{"names": ["!chosen"], "sound": "seph_onlythechosen.wav"},
{"names": ["!goodbye", "!saygoodbye"], "sound": "seph_saygoodbye.wav"},
{"names": ["!slow"], "sound": "seph_slow.wav"},
{"names": ["!late", "!toolate"], "sound": "seph_toolate.wav"},
{"names": ["!growl"], "sound": "bahamut_growl2.wav"},
{"names": ["!buyspins", "!buy_spins"], "sound": "BUYSPINS1.wav"},
{"names": ["!death", "!deathscream"], "sound": "d_death_scream.wav"},
{"names": ["!attack", "!meetyourend"], "sound": "z_godend.wav"},
{"names": ["!die", "!guardian"], "sound": "z_guardianend.wav"},
{"names": ["!applause"], "sound": "applause.wav"},
{"names": ["!retreat"], "sound": "stage_x_gandalf_retreat-1-0.wav"},
{"names": ["!idiot"], "sound": "idiot.wav"},
{"names": ["!duck", "!quack"], "sound": "quack.wav"},
{"names": ["!triple"], "sound": "triple.wav"},
{"names": ["!solo"], "sound": "solo.wav"},
{"names": ["!oniichan", "!onii-chan"], "sound": "Onii-chan.wav"},
{"names": ["!gameover"], "sound": "Gameover.wav"},
{"names": ["!wombo"], "sound": "WomboCombo.wav"},
{"names": ["!camera"], "sound": "MOM GET THE CAMERA.wav"},
{"names": ["!hihi"], "sound": "skullz.wav"},
{"names": ["!yatta"], "sound": "Hiro Yatta.wav"},
{"names": ["!over"], "sound": "over.wav"},
{"names": ["!cena"], "sound": "cena.wav"},
{"names": ["!victory"], "sound": "ffvii_victory.wav"},
{"names": ["!2012"], "sound": ["2012_1.wav", "2012_2.wav", "2012_3.wav", "2012_4.wav"]},
{"names": ["!nein"], "sound": "hitler_nein.wav"},
{"names": ["!allahu", "!akbar"], "sound": "allahu_akbar.wav"},
{"names": ["!chopper"], "sound": "gettothechopper.wav"},
{"names": ["!payback"], "sound": "blain - payback time.wav"},
{"names": ["!scotland"], "sound": "scotland.wav"},
{"names": ["!happening"], "sound": "happening.wav"},
{"names": ["!topkek"], "sound": "topkek.wav"},
{"names": ["!welcome", "!ricefields"], "sound": "welcome_to_the_ricefields.wav"},
{"names": ["!inception"], "sound": "inception.wav"},
{"names": ["!x"], "sound": "xfiles.wav"},
{"names": ["!stfu"], "sound": ["stfu.wav", "stfu2.mp3"]},
{"names": ["!goat"], "sound": ["goat1.wav", "goat2.wav", "goat3.wav", "goat4.wav", "goat5.wav", "goat6.wav"]},
{"names": ["!doit"], "sound": "doit.wav"},
{"names": ["!benis"], "sound": "grossenbenis.wav"},
{"names": ["!omg"], "sound": "omg.wav"},
{"names": ["!wow"], "sound": ["wow.wav", "rexy_wow.wav"]},
{"names": ["!prepare", "!battle"], "sound": "stage_2_preparaos-0-0.wav"},
{"names": ["!meme"], "sound": "nicememe.wav"},
{"names": ["!nyaa"], "sound": ["nyaa_1.wav", "nyaa_2.wav", "nyaa_3.wav", "nyaa_4.wav", "nyaa_5.wav", "nyaa_6.wav", "nyaa-3.mp3", "nyaa4.mp3"]},
{"names": ["!ah"], "sound": "FFVII_Cry.wav"},
{"names": ["!feelsbad"], "sound": "feelsbadman.wav"},
{"names": ["!admun"], "sound": "admun_please.wav"},
{"names": ["!population"], "sound": "population.wav"},
{"names": ["!why", "!immigration"], "sound": "immigration.wav"},
{"names": ["!tutury"], "sound": "tutury.wav"},
{"names": ["!lew"], "sound": "Bewlewlewlew.wav"},
{"names": ["!pudi"], "sound": "Pudi Pudi (Short).wav"},
{"names": ["!hey", "!listen"], "sound": "Hey Listen.wav"},
{"names": ["!comeon"], "sound": "Come on.mp3"},
{"names": ["!stopp"], "sound": "itstimetostop.mp3"},
{"names": ["!cancer"], "sound": "cancer.mp3"},
{"names": ["!heil"], "sound": "heil.mp3"},
{"names": ["!nico"], "sound": "niconiconi.mp3"},
{"names": ["!cock"], "sound": ["cock1.mp3", "cock2.mp3", "cock3.mp3", "cock4.mp3", "cock5.mp3", "cock6.mp3"]},
{"names": ["!rock"], "sound": "LetsRock.mp3"},
{"names": ["!fmlaugh"], "sound": "fmlaugh.mp3"},
{"names": ["!monopoly"], "sound": "monopoly.mp3"},
{"names": ["!kiddin", "!kidding"], "sound": "rexy r u kiddin me.mp3"},
{"names": ["!noice"], "sound": "Noice.mp3"},
{"names": ["!rexy"], "sound": "rexywaah.mp3"},
{"names": ["!kaboom"], "sound": "kaboom.mp3"},
{"names": ["!honk"], "sound": "honk.wav"},
{"names": ["!spam"], "sound": "No_Spammerino_In_The_Chatterino.mp3"},
{"names": ["!ohoho"], "sound": ["ohoho_1.mp3", "ohoho_3.mp3", "ohoho_4.mp3", "ohoho_5.mp3", "ohoho_6.mp3", "ohoho_7.mp3", "ohoho_8.mp3", "ohoho_9.mp3", "ohoho_10.mp3", "ohoho_11.mp3", "ohoho_12.mp3", "ohoho_13.mp3", "ohoho_14.mp3", "ohoho_15.mp3", "ohoho_16.mp3", "ohoho_17.mp3", "ohoho_18.mp3", "ohoho_19.mp3", "ohoho_20.mp3", "ohoho_21.mp3", "ohoho_22.mp3", "ohoho_23.mp3", "ohoho_24.mp3", "ohoho_25.mp3", "ohoho_26.mp3", "ohoho_27.mp3", "ohoho_28.mp3", "ohoho_29.mp3", "ohoho_30.mp3", "ohoho_31.mp3", "ohoho_32.mp3", "ohoho_33.mp3", "ohoho_34.mp3", "ohoho_35.mp3", "ohoho_36.mp3", "ohoho_37.mp3", "ohoho_38.mp3", "ohoho_39.mp3", "ohoho_40.mp3", "ohoho_41.mp3", "ohoho_42.mp3", "ohoho_43.mp3", "ohoho_44.mp3", "ohoho_45.mp3", "ohoho_46.mp3", "ohoho_47.mp3", "ohoho_48.mp3", "ohoho_49.mp3", "ohoho_50.mp3", "ohoho_51.mp3", "ohoho_52.mp3", "ohoho_53.mp3", "ohoho_54.mp3", "ohoho_55.mp3", "ohoho_56.mp3", "ohoho_57.mp3", "ohoho_58.mp3", "ohoho_59.mp3", "ohoho_60.mp3", "ohoho_61.mp3", "ohoho_62.mp3", "ohoho_63.mp3", "ohoho_64.mp3", "ohoho_65.mp3", "ohoho_66.mp3", "ohoho_67.mp3", "ohoho_68.mp3", "ohoho_69.mp3", "ohoho_70.mp3", "ohoho_71.mp3", "ohoho_72.mp3", "ohoho_73.mp3", "ohoho_74.mp3", "ohoho_75.mp3", "ohoho_76.mp3", "ohoho_77.mp3", "ohoho_78.mp3", "ohoho_79.mp3", "ohoho_80.mp3", "ohoho_81.mp3", "ohoho_82.mp3", "ohoho_83.mp3", "ohoho_84.mp3", "ohoho_85.mp3", "ohoho_86.mp3", "ohoho_87.mp3", "ohoho_88.mp3", "ohoho_89.mp3"]},
{"names": ["!sugoi"], "sound": "sugoi_sugoi.mp3"},
{"names": ["!cry"], "sound": ["cry1.wav", "cry2.wav", "cry3.wav", "cry4.wav", "cry5.wav", "cry6.wav", "cry7.wav"]},
{"names": ["!hehe", "!giggle"], "sound": "Giggle.mp3"},
{"names": ["!monkey"], "sound": ["chimp1.wav", "chimp2.wav"]},
{"names": ["!ka", "!kaka"], "sound": ["Nisemonogatari-Shinobu-Kaka.ogg", "Nisemonogatari-Shinobu-K-ka.ogg"]},
{"names": ["!jodel"], "sound": "jodel.mp3"},
{"names": ["!nyaaa"], "sound": "nyaaa.mp3"},
{"names": ["!run"], "sound": "run.wav"},
{"names": ["!goodbye"], "sound": "goodbye.wav"},
{"names": ["!noo"], "sound": "LOTR_Noooooo.wav"},
{"names": ["!dayum"], "sound": "daaamn.mp3"},
{"names": ["!goddammit", "!goddamnit"], "sound": "goddammit.mp3"},
{"names": ["!surprise"], "sound": "surprisemotherfucker1.mp3"},
{"names": ["!csi"], "sound": "yeeaah.mp3"},
{"names": ["!nope"], "sound": "engineer_no01.mp3"},
{"names": ["!joke"], "sound": "rimshot.mp3"},
{"names": ["!weed"], "sound": "smokeweederryday.mp3"},
{"names": ["!toasty"], "sound": "toasty.mp3"},
{"names": ["!damn"], "sound": "wheredyoufindthis.mp3"},
{"names": ["!nuts"], "sound": "suckmynuts.mp3"},
{"names": ["!wake"], "sound": "wakemeup.mp3"},
{"names": ["!bye"], "sound": "bye.mp3"},
{"names": ["!ilikeit"], "sound": "ilikeit.mp3"},
{"names": ["!milk"], "sound": "milk.mp3"},
{"names": ["!pussy"], "sound": "pussy.mp3"},
{"names": ["!retard"], "sound": ["retard.mp3", "retard2.mp3"]},
{"names": ["!sorry"], "sound": "sry.mp3"},
{"names": ["!wtf"], "sound": "wtf.mp3"},
{"names": ["!brb"], "sound": "brb.mp3"},
{"names": ["!cricket"], "sound": "cricket.mp3"},
{"names": ["!hax"], "sound": "hax.mp3"},
{"names": ["!hi"], "sound": "hi.mp3"},
{"names": ["!moo"], "sound": "moo.mp3"},
{"names": ["!rape"], "sound": "rape.mp3"},
{"names": ["!tada"], "sound": "tada.mp3"},
{"names": ["!yay"], "sound": "yay.mp3"},
{"names": ["!cyka"], "sound": "cyka.mp3"},
{"names": ["!racist"], "sound": "racist.mp3"},
{"names": ["!roger"], "sound": "roger.mp3"},
{"names": ["!tooslow"], "sound": "tooslow.mp3"},
{"names": ["!steam"], "sound": "steam.wav"},
{"names": ["!good"], "sound": "that_s_pretty_good.mp3"},
{"names": ["!crawling"], "sound": "crawling.mp3"},
{"names": ["!nukyun"], "sound": ["nukyun1.mp3", "nukyun2.mp3", "nukyun3.mp3", "nukyun4.mp3", "nukyun5.mp3", "nukyun6.mp3", "nukyun7.mp3", "nukyun8.mp3", "nukyun9.mp3", "nukyun10.mp3", "nukyun11.mp3", "nukyun12.mp3", "nukyun13.mp3", "nukyun14.mp3", "nukyun15.mp3", "nukyun16.mp3", "nukyun17.mp3", "nukyun18.mp3", "nukyun19.mp3", "nukyun20.mp3", "nukyun21.mp3", "nukyun22.mp3", "nukyun23.mp3", "nukyun24.mp3", "nukyun25.mp3", "nukyun26.mp3", "nukyun27.mp3"]},
{"names": ["!harambe"], "sound": "harambe.mp3"},
{"names": ["!horn"], "sound": "vu_horn_quick.wav"},
{"names": ["!hood"], "sound": "hood.mp3"},
{"names": ["!gtfo"], "sound": ["gtfo.mp3", "gtfo2.mp3"]},
{"names": ["!pomf"], "sound": "pomf.mp3"},
{"names": ["!gay"], "sound": "putingay.mp3"},
{"names": ["!pedo"], "sound": "pedobear.mp3"},
{"names": ["!kys"], "sound": "kys.mp3"},
{"names": ["!english"], "sound": "englishonly.mp3"},
{"names": ["!knowledge"], "sound": "Knowledge.m4a"},
{"names": ["!mana"], "sound": "mana.mp3"},
{"names": ["!dodge"], "sound": ["dodge1.mp3", "dodge2.mp3"]},
{"names": ["!love"], "sound": "sheep.mp3"},
{"names": ["!timotei"], "sound": "Timotei.wav"},
{"names": ["!daniel"], "sound": "daniel.mp3"},
{"names": ["!cne"], "sound": ["it_is_may.mp3", "cnelaugh.mp3"]},
{"names": ["!avocados"], "sound": "avocados.mp3"},
{"names": ["!tutu", "!papa"], "sound": "papatutuwawa.mp3"},
{"names": ["!nani"], "sound": "nani.mp3"},
{"names": ["!squee"], "sound": "squee.mp3"},
{"names": ["!ptb"], "sound": "07ptb.wav"},
{"names": ["!wall"], "sound": "wall.mp3"},
{"names": ["!bomb"], "sound": "bomb.mp3"},
{"names": ["!wrong"], "sound": "wrong.mp3"},
{"names": ["!china"], "sound": ["china1.mp3", "china2.mp3", "china3.mp3", "china4.mp3", "china5.mp3", "china6.mp3", "china7.mp3"]},
{"names": ["!oof"], "sound": "oof.mp3"},
{"names": ["!pan"], "sound": "panpakapan.mp3"},
{"names": ["!shoulder"], "sound": "oh_my_shoulder.mp3"},
{"names": ["!kizuna"], "sound": ["kizuna_fucku.mp3", "kizuna_fucku2.mp3", "kizuna_omg.mp3"]},
{"names": ["!zegawa"], "sound": ["moaning_leBuP3p.mp3","1caae40.mp3","ara_ara.mp3","bndarling.mp3","watashi_hotto_jaate.mp3"]},
{"names": ["!screenshot"], "sound": "camera1.wav"},
{"names": ["!badro"], "sound": "vip.mp3"},
{"names": ["!omarlaser"], "sound": "omarlaser.mp3"},
{"names": ["!omarlenny"], "sound": "omarlenny.mp3"},
{"names": ["!silent"], "sound": ["silent_scream.mp3" , "silent_laught.mp3"]},
{"names": ["!willy"], "sound": "rippr_lotr_plz.mp3"},
{"names": ["!anone"], "sound": "anone.mp3"},
{"names": ["!jayz"], "sound": "jayz.wav"},
{"names": ["!succ"], "sound": "succ.mp3"},
{"names": ["!uguu"], "sound": "uguu_1.mp3"},
{"names": ["!doot"], "sound": "skullsound2.mp3"},
{"names": ["!widow"], "sound": "widowmaker_-_americans-1.mp3"},
{"names": ["!winston"], "sound": "i-wanna-be-winston.mp3"},
{"names": ["!try"], "sound": "try.mp3"},
{"names": ["!smug"], "sound": "smug.mp3"},
{"names": ["!skullz_tw"], "sound": "tw_skullzrage.mp3"},
{"names": ["!genesis"], "sound": "genesis.mp3"},
{"names": ["!borealis"], "sound": ["borealis1.mp3", "borealis2.mp3"]},
{"names": ["!clams"], "sound": ["clams1.mp3", "clams2.mp3", "clams3.mp3"]},
{"names": ["!hams"], "sound": ["hams1.mp3", "hams2.mp3"]},
{"names": ["!expression"], "sound": "expression.mp3"},
{"names": ["!albany"], "sound": "expression.mp3"},
{"names": ["!steamedyes"], "sound": "steamedyes.mp3"},
{"names": ["!steamedno"], "sound": "steamedno.mp3"},
{"names": ["!luncheon"], "sound": "luncheon.mp3"},
{"names": ["!devilish"], "sound": "devilish.mp3"},
{"names": ["!roast"], "sound": "roast.mp3"},
{"names": ["!goodlord"], "sound": "lord_unloze.mp3"},
{"names": ["!time"], "sound": "time.mp3"},
{"names": ["!dialect"], "sound": "dialect.mp3"},
{"names": ["!attention"], "sound": "attention.mp3"},
{"names": ["!great"], "sound": "great.mp3"},
{"names": ["!epicsolo"], "sound": "epicsolo.mp3"},
{"names": ["!everyonertv"], "sound": "everyonertv.mp3"},
{"names": ["!esk"], "sound": "esk.mp3"},
{"names": ["!epc"], "sound": "epc.mp3"},
{"names": ["!ges"], "sound": "ges.mp3"},
{"names": ["!eze"], "sound": "eze.mp3"},
{"names": ["!eod"], "sound": "eod.mp3"},
{"names": ["!tce"], "sound": "tce.mp3"},
{"names": ["!trouble"], "sound": "trouble.mp3"},
{"names": ["!teamwin"], "sound": "teamwin.mp3"},
{"names": ["!nide"], "sound": ["nide.mp3", "nide2.mp3"]},
{"names": ["!heaven"], "sound": "heaven.mp3"},
{"names": ["!unskilled"], "sound": "unskilled.mp3"},
{"names": ["!russia"], "sound": "russia.mp3"},
{"names": ["!gg"], "sound": "gg.mp3"},
{"names": ["!notrigger"], "sound": "notrig.wav"},
{"names": ["!enemy"], "sound": ["enemee1.wav", "enemee2.wav", "enemee3.wav", "enemee4.wav", "enemee5.wav"]},
{"names": ["!glacius"], "sound": "Glacius.m4a"},
{"names": ["!ayaya"], "sound": ["ayayakaren1.mp3", "ayayakaren2.mp3"]},
{"names": ["!dogan"], "sound": "dogan_im_going_to_fucking_fuck_you.mp3"},
{"names": ["!destroy"], "sound": "dickstodestroy.mp3"},
{"names": ["!boi"], "sound": "ainsley_harriott_and_his_spicy_meatconverttoaudio.mp3"},
{"names": ["!gunga", "!ginga"], "sound": "gunga_ginga.mp3"},
{"names": ["!leaders"], "sound": "leaders.mp3"},
{"names": ["!never"], "sound": "never.mp3"},
{"names": ["!hl1", "!blackmesa"], "sound": ["hl1_whatyoudoing.wav", "hl1_otis_talkmuch.wav", "hl1_pain3.wav", "hl1_otis_virgin.wav", "hl1_pain4.wav", "hl1_otis_die.wav", "hl1_pain1.wav", "hl1_otis_mom.wav", "hl1_pain2.wav", "hl1_scream05.wav", "hl1_iwounded.wav", "hl1_fear12.wav", "hl1_fear14.wav", "hl1_fear15.wav", "hl1_fear6.wav", "hl1_fear7.wav", "hl1_fear8.wav", "hl1_fear11.wav", "hl1_scream07.wav", "hl1_scream01.wav"]},
{"names": ["!lügen"], "sound": "lügen.mp3"},
{"names": ["!lachen"], "sound": "lachen.mp3"},
{"names": ["!incoming"], "sound": "phalanx_incoming.mp3"},
{"names": ["!address"], "sound": "humanz_send_me_your_address.wav"},
{"names": ["!abuse"], "sound": "humanz_abuse.wav"},
{"names": ["!killme"], "sound": "doit_comeon_killme.mp3"},
{"names": ["!sickening"], "sound": "sickening.mp3"},
{"names": ["!disgusting"], "sound": "disgusting_scuffed.mp3"},
{"names": ["!icq"], "sound": "icq_old_sound.wav"},
{"names": ["!discord"], "sound": "discord-notification.mp3"},
{"names": ["!law", "!order"], "sound": "Law_&_Order_Sound.mp3"},
{"names": ["!deutschland"], "sound": "VorUnsLiegtDeutschland.mp3"},
{"names": ["!qwerpi"], "sound": ["qwerpi_aotsu/answer_the_question1.mp3", "qwerpi_aotsu/answer_the_question2.mp3", "qwerpi_aotsu/answer_the_question3.mp3", "qwerpi_aotsu/answer_the_question4.mp3", "qwerpi_aotsu/answer_the_question5.mp3", "qwerpi_aotsu/answer_the_question6.mp3", "qwerpi_aotsu/answer_the_question7.mp3", "qwerpi_aotsu/aotsuki_autism.mp3", "qwerpi_aotsu/aotsuki_come_on_stop.mp3", "qwerpi_aotsu/aotsuki_kouya.mp3", "qwerpi_aotsu/aotsuki_pls.mp3", "qwerpi_aotsu/aotsukitsukitsuki1.mp3", "qwerpi_aotsu/aotsukitsukitsuki2.mp3", "qwerpi_aotsu/aotsukyaotsukyaotsuky1.mp3", "qwerpi_aotsu/aotsukyaotsukyaotsuky2.mp3", "qwerpi_aotsu/can_you_pls_tell_me_your_opinion_takeshima.mp3", "qwerpi_aotsu/come_on_aotsukyyy.mp3", "qwerpi_aotsu/faking_faking_faking.mp3", "qwerpi_aotsu/i_changed_my_name_he_can_hear_me.mp3", "qwerpi_aotsu/japan_or_america1.mp3", "qwerpi_aotsu/japan_or_america10.mp3", "qwerpi_aotsu/japan_or_america11.mp3", "qwerpi_aotsu/japan_or_america12.mp3", "qwerpi_aotsu/japan_or_america2.mp3", "qwerpi_aotsu/japan_or_america3.mp3", "qwerpi_aotsu/japan_or_america4.mp3", "qwerpi_aotsu/japan_or_america5.mp3", "qwerpi_aotsu/japan_or_america6.mp3", "qwerpi_aotsu/japan_or_america7.mp3", "qwerpi_aotsu/japan_or_america8.mp3", "qwerpi_aotsu/japan_or_america9.mp3", "qwerpi_aotsu/onegaishimasu.mp3", "qwerpi_aotsu/pls_win_round_aotsuki.mp3", "qwerpi_aotsu/takeshima1.mp3", "qwerpi_aotsu/takeshima2.mp3", "qwerpi_aotsu/takeshima3.mp3", "qwerpi_aotsu/takeshima4.mp3", "qwerpi_aotsu/takeshima5.mp3", "qwerpi_aotsu/takeshima6.mp3", "qwerpi_aotsu/takeshima7.mp3", "qwerpi_aotsu/unnamed.mp3", "qwerpi_aotsu/why_do_you_do_this_to_me.mp3", "qwerpi_aotsu/why_dont_you_just_answer1.mp3", "qwerpi_aotsu/why_dont_you_just_answer2.mp3", "qwerpi_aotsu/why_dont_you_just_answer3.mp3", "qwerpi_aotsu/why_dont_you_just_answer4.mp3", "qwerpi_aotsu/why_dont_you_just_answer5.mp3", "qwerpi_aotsu/why_dont_you_just_answer6.mp3", "qwerpi_aotsu/you_not_japanese1.mp3", "qwerpi_aotsu/you_not_japanese2.mp3", "qwerpi_aotsu/you_not_japanese3.mp3", "qwerpi_aotsu/you_will_never_get_rid_of_me_aotsuki.mp3"]},
{"names": ["!panic"], "sound": "greta_panic.mp3"},
{"names": ["!gachi"], "sound": ["gachi/300.mp3", "gachi/ah.mp3", "gachi/amazing.mp3", "gachi/anal.mp3", "gachi/artist.mp3", "gachi/asswecan.mp3", "gachi/attention.mp3", "gachi/bigger.mp3", "gachi/boss.mp3", "gachi/boynextdoor.mp3", "gachi/collegeboy.mp3", "gachi/comeon.mp3", "gachi/daddy.mp3", "gachi/deep.mp3", "gachi/door.mp3", "gachi/doyoulike.mp3", "gachi/dungeon.mp3", "gachi/embarrassing.mp3", "gachi/fantasies.mp3", "gachi/fuckyou.mp3", "gachi/fuckyou2.mp3", "gachi/fuckyou3.mp3", "gachi/gangingup.mp3", "gachi/happy.mp3", "gachi/interruption.mp3", "gachi/jabroni.mp3", "gachi/lash.mp3", "gachi/likethat.mp3", "gachi/loads.mp3", "gachi/lube.mp3", "gachi/mmh.mp3", "gachi/pants.mp3", "gachi/power.mp3", "gachi/rip.mp3", "gachi/slaves.mp3", "gachi/sorry.mp3", "gachi/sorry2.mp3", "gachi/spank.mp3", "gachi/suction.mp3", "gachi/takeit.mp3", "gachi/website.mp3", "gachi/woo.mp3", "gachi/wth.mp3"]},
{"names": ["!ali", "!alia"], "sound": "AliA.mp3"},
{"names": ["!dropit"], "sound": "dropit.mp3"},
{"names": ["!murica"], "sound": "MURICA.mp3"},
{"names": ["!ocean", "!oceanman"], "sound": "oceanman.mp3"},
{"names": ["!thanos"], "sound": "thanos_snaps_fingers_sfx.mp3"},
{"names": ["!greta"], "sound": ["greta_1.mp3", "greta_2.mp3", "greta_3.mp3"]},
{"names": ["!playb"], "sound": ["PlayB_-_back_back_back.mp3", "PlayB_-_back_doorhug.mp3", "PlayB_-_dont_stay_fkng_nubs.mp3", "PlayB_-_not_FF"]},
{"names": ["!alarm"], "sound": "iphone_alarm.mp3"},
{"names": ["!iphone", "!ringtone"], "sound": "iphone_ringtone.mp3"},
{"names": ["!gman"], "sound": ["gman_choose1.wav", "gman_mono34.wav", "Gman_wellseeaboutthat.ogg", "gman_choose1.wav", "gman_choose2.wav", "gman_wisely_done.wav", "gman_wisely_done_freeman.wav", "gman_well_shit.wav"]},
{"names": ["!steamedjenz"], "sound": "jenz_yes.mp3"},
{"names": ["!sinaknife"], "sound": "sinaknife.mp3"},
{"names": ["!sans"], "sound": ["undertale_sans.mp3", "megalovania_1.mp3", "megalovania_2.mp3"]},
{"names": ["!what", "!youwhat"], "sound": "you-what-spongebob.mp3"},
{"names": ["!pufferfish", "!augh"], "sound": "pufferfish_augh.mp3"},
{"names": ["!gotcha"], "sound": "Gotcha Bitch2.mp3"},
{"names": ["!palpatine"], "sound": "Palp_doit.mp3"},
{"names": ["!chosenone"], "sound": "ChosenOne.mp3"},
{"names": ["!anakinfun"], "sound": "AnakinFun.mp3"},
{"names": ["!anakinliar"], "sound": "AnakinLiar.mp3"},
{"names": ["!hellothere"], "sound": "HelloThere.mp3"},
{"names": ["!because"], "sound": "BecauseObiWan.mp3"},
{"names": ["!outofhand"], "sound": "OutofHand.mp3"},
{"names": ["!problem"], "sound": "problem.mp3"},
{"names": ["!pizza"], "sound": ["pizza.mp3", "pizza2.mp3"]},
{"names": ["!train"], "sound": ["train.mp3", "train2.mp3", "train4.mp3"]},
{"names": ["!screech"], "sound": "screech.mp3"},
{"names": ["!simp"], "sound": "simp.mp3"},
{"names": ["!tadadada"], "sound": "tadadada.mp3"},
{"names": ["!kroaat"], "sound": "kroaat_rage.mp3"},
{"names": ["!passed"], "sound": "GTA San Andreas - Mission passed sound.mp3"},
{"names": ["!ahshit"], "sound": "GTA San Andreas - Ah shit, here we go again.mp3"},
{"names": ["!no"], "sound": "nononojojo.mp3"},
{"names": ["_amigos"], "sound": "10-60/amigos.mp3"},
{"names": ["_sad"], "sound": "10-60/2sad4me2.mp3"},
{"names": ["_minasplease"], "sound": "10-60/MINASPLEASESONG.mp3"},
{"names": ["_jobs"], "sound": "10-60/Jobs.mp3"},
{"names": ["_praise"], "sound": "10-60/Praise-The-Sun-420Yolo2.mp3"},
{"names": ["_brainpower"], "sound": "10-60/Brain Power V2.mp3"},
{"names": ["_cotton"], "sound": "10-60/Cotton Eye Akbar.mp3"},
{"names": ["_suckmycock", "_stopshooting"], "sound": "10-60/meshlem_arrive.mp3"},
{"names": ["_hacker"], "sound": "10-60/Meshlem_die_good.mp3"},
{"names": ["_how", "_happen"], "sound": "10-60/how_could_this_happen_to_me.mp3"},
{"names": ["_tuturemix"], "sound": "10-60/tuturemix.mp3"},
{"names": ["_pirate"], "sound": "10-60/pirate.wav"},
{"names": ["_legend"], "sound": "10-60/legend.mp3"},
{"names": ["_pirates"], "sound": "10-60/The - best.mp3"},
{"names": ["_pegboard"], "sound": "10-60/Pendulum - Witchcraft Pegboard.mp3"},
{"names": ["_saxoo"], "sound": "10-60/saxo.mp3"},
{"names": ["_no"], "sound": "10-60/NOOOOOOOOOOO.wav"},
{"names": ["_crabrave"], "sound": "10-60/crabrave.mp3"},
{"names": ["_takemeon"], "sound": "10-60/a-ha-take-on-me-cut-mp3.mp3"},
{"names": ["_shake"], "sound": "10-60/ShakeRockstar.ogg"},
{"names": ["_hitormiss"], "sound": "10-60/hom_sans.mp3"},
{"names": ["_johnny"], "sound": "10-60/Johnny Daepp.mp3"},
{"names": ["_theo"], "sound": "10-60/theobald_lower.mp3"},
{"names": ["_chinamotor"], "sound": "10-60/chinesemotorcycle.mp3"},
{"names": ["_ftarflute"], "sound": "10-60/pend_taran_flute.mp3"},
{"names": ["_neon_remix"], "sound": "10-60/neon_remix.mp3"},
{"names": ["_lickies"], "sound": "10-60/lickies.mp3"},
{"names": ["_jenzlem"], "sound": "10-60/jenzlem.m4a"},
{"names": ["_playb"], "sound": "10-60/DontfallbackdefendPlayB.mp3"},
{"names": ["_awaken"], "sound": "10-60/awaken.mp3"},
{"names": ["_dudu"], "sound": "10-60/dudu.mp3"},
{"names": ["_ice"], "sound": "10-60/levshoot.mp3"},
{"names": ["_gunga", "_ginga"], "sound": "10-60/gunga_ginga.mp3"},
{"names": ["_makoplease"], "sound": "10-60/makoplease.mp3"},
{"names": ["_willrip"], "sound": "10-60/fullwilly.mp3"},
{"names": ["_rage"], "sound": "tense1983_rage.mp3"},
{"names": ["_france"], "sound": "france.mp3"},
{"names": ["_cne"], "sound": "Cne_goes_apocalypse_mode.mp3"},
{"names": ["#tempest"], "sound": "60+/tempest.wav"},
{"names": ["#pendulum_knife"], "sound": "60+/Pendulum_KnifeParty.mp3"},
{"names": ["#nostar"], "sound": "60+/minassong.mp3"},
{"names": ["#pika"], "sound": "60+/pika.wav"},
{"names": ["#tripoloski"], "sound": "60+/tripoloski.mp3"},
{"names": ["#tunak"], "sound": "60+/tunak.mp3"},
{"names": ["#crabman"], "sound": "60+/CrabMan.ogg"},
{"names": ["#trump"], "sound": "Donald_Trump_-_Shooting_Stars.mp3"},
{"names": ["#tri_remix"], "sound": "60+/tree_poloskee.mp3"}
]