Initial commit.

This commit is contained in:
BotoX 2017-08-02 23:41:02 +02:00
commit 02cce55db1
23 changed files with 2302 additions and 0 deletions

3
.gitignore vendored Executable file
View File

@ -0,0 +1,3 @@
__pycache__
*.pyc
venv

25
README.md Normal file
View File

@ -0,0 +1,25 @@
# 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`

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__()

97
Torchlight/AsyncClient.py Normal file
View File

@ -0,0 +1,97 @@
#!/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("Connecting...")
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

271
Torchlight/AudioManager.py Normal file
View File

@ -0,0 +1,271 @@
#!/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
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 RegisterClip(self, clip):
self.LastClips[hash(clip)] = dict({"timestamp": None, "duration": 0.0, "dominant": False, "active": True})
def SpamCheck(self):
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"] + 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):
self.LastClips[hash(clip)]["timestamp"] = self.Torchlight().Master.Loop.time()
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):
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()
class AudioManager():
def __init__(self, torchlight):
self.Logger = logging.getLogger(__class__.__name__)
self.Torchlight = torchlight
self.AntiSpam = AntiSpam(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:
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"]:
self.AntiSpam.RegisterClip(Clip)
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))
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):
return self.AudioPlayer.PlayURI(self.URI, seconds)
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["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,111 @@
#!/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()
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

730
Torchlight/Commands.py Normal file
View File

@ -0,0 +1,730 @@
#!/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
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name)
### FILTER COMMANDS ###
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 = -1
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):
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", "-xg", url,
stdout = asyncio.subprocess.PIPE)
Out, _ = await Proc.communicate()
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"], 2), int(Info["view_count"])))
else:
match = None
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") and not ContentType.startswith("text/plain"):
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
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
asyncio.ensure_future(self.URLInfo(Url))
return -1
### FILTER COMMANDS ###
### LEVEL 0 COMMANDS ###
class Access(BaseCommand):
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!access", "!who", "!whois"]
self.Level = 0
def FormatAccess(self, 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 self.Torchlight().Config["AudioLimits"]:
Uses = self.Torchlight().Config["AudioLimits"][Level]["Uses"]
TotalTime = self.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
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
Count = 0
if message[0] == "!access":
if message[1]:
return -1
self.Torchlight().SayChat(self.FormatAccess(player))
elif message[0] == "!who":
for Player in self.Torchlight().Players:
if Player.Name.lower().find(message[1].lower()) != -1:
self.Torchlight().SayChat(self.FormatAccess(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(self.FormatAccess(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 Calculate(BaseCommand):
import urllib.parse
import aiohttp
import json
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!c"]
self.Level = 0
async def Calculate(self, Params):
async with self.aiohttp.ClientSession() as session:
Response = await asyncio.wait_for(session.get("http://math.leftforliving.com/query", params=Params), 5)
if not Response:
return 1
Data = await asyncio.wait_for(Response.json(content_type = "text/json"), 5)
if not Data:
return 2
if not Data["error"]:
self.Torchlight().SayChat(Data["answer"])
return 0
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
Params = dict({"question": message[1]})
Ret = await self.Calculate(Params)
return Ret
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 = 0
def Clean(self, Text):
return self.re.sub("[ ]{2,}", " ", Text.replace(' | ', ': ').replace('\n', ' | ').replace('~~', '')).strip()
async def Calculate(self, Params):
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.")
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)
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)
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))
return 0
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
Params = dict({"input": message[1], "appid": self.Torchlight().Config["WolframAPIKey"]})
Ret = await self.Calculate(Params)
return Ret
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
### LEVEL 0 COMMANDS ###
### LIMITED LEVEL 0 COMMANDS ###
class VoiceCommands(BaseCommand):
import json
import random
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!random"]
self.Level = 0
def LoadTriggers(self):
try:
with open("triggers.json", "r") as fp:
self.VoiceTriggers = self.json.load(fp)
except ValueError as e:
self.Logger.error(sys._getframe().f_code.co_name + ' ' + str(e))
self.Torchlight().SayChat(str(e))
def _setup(self):
self.Logger.debug(sys._getframe().f_code.co_name)
self.LoadTriggers()
for Triggers in self.VoiceTriggers:
for Trigger in Triggers["names"]:
self.Triggers.append(Trigger)
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
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 1
if message[0][0] == '_' and Level < 2:
return 1
if message[0].lower() == "!random":
Trigger = self.random.choice(self.VoiceTriggers)
if isinstance(Trigger["sound"], list):
Sound = self.random.choice(Trigger["sound"])
else:
Sound = Trigger["sound"]
else:
for Trigger in self.VoiceTriggers:
for Name in Trigger["names"]:
if message[0].lower() == Name:
Num = Utils.GetNum(message[1])
if Num:
Num = int(Num)
if isinstance(Trigger["sound"], list):
if Num and Num > 0 and Num <= len(Trigger["sound"]):
Sound = Trigger["sound"][Num - 1]
else:
Sound = self.random.choice(Trigger["sound"])
else:
Sound = Trigger["sound"]
break
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 = 0
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
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 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 = 0
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
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 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()
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"], 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.gTTS.LANGUAGES.keys()]
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = [("!say", 4)]
self.Level = 0
async def Say(self, player, language, message):
GTTS = self.gtts.gTTS(text = message, lang = language, debug = False)
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))
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 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 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
### LIMITED LEVEL 0 COMMANDS ###
### LEVEL 1 COMMANDS ###
### LEVEL 1 COMMANDS ###
### LEVEL 2 COMMANDS ###
### LEVEL 2 COMMANDS ###
### LEVEL 3 COMMANDS ###
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 not self.Torchlight().Disabled:
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"]
### LEVEL 3 COMMANDS ###
### LEVEL 4 COMMANDS ###
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
### LEVEL 4 COMMANDS ###
### LEVEL X COMMANDS ###
class Exec(BaseCommand):
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!exec"]
self.Level = 9
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
class Reload(BaseCommand):
def __init__(self, torchlight):
super().__init__(torchlight)
self.Triggers = ["!reload"]
self.Level = 6
async def _func(self, message, player):
self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message))
self.Torchlight().Reload()
return 0
### LEVEL X COMMANDS ###

24
Torchlight/Config.py Normal file
View File

@ -0,0 +1,24 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import logging
import json
class Config():
def __init__(self):
self.Logger = logging.getLogger(__class__.__name__)
self.Config = dict()
self.Load()
def Load(self):
try:
with open("config.json", "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

26
Torchlight/Constants.py Normal file
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,178 @@
#!/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 = None):
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", "-"]
else:
Command = ["/usr/bin/ffmpeg", "-i", uri, "-acodec", "pcm_s16le", "-ac", "1", "-ar", str(int(self.SampleRate)), "-f", "s16le", "-"]
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()

142
Torchlight/GameEvents.py Normal file
View File

@ -0,0 +1,142 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import asyncio
class GameEvents():
def __init__(self, master):
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:
Callback(**Event["data"])
return True

188
Torchlight/PlayerManager.py Normal file
View File

@ -0,0 +1,188 @@
#!/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().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))
assert self.Players[index] == None
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 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
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
asyncio.ensure_future(self.OnPostActivate())
async def OnPostActivate(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.Generic():
self.Access = dict({"level": 3, "name": "Admin"})
elif self.Admin.Custom1():
self.Access = dict({"level": 1, "name": "VIP"})
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,105 @@
#!/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))

125
Torchlight/Torchlight.py Normal file
View File

@ -0,0 +1,125 @@
#!/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 .GameEvents import GameEvents
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.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):
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))
def SayPrivate(self, player, message):
asyncio.ensure_future(self.API.PrintToChat(player.Index, "\x0700FFFA[Torchlight]: \x01{0}".format(message)))
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)
def Event_ServerSpawn(self, hostname, address, ip, port, game, mapname, maxplayers, os, dedicated, password):
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"])
self.Torchlight.InitModules()
# Late load
await self.Torchlight.GameEvents.Replay(["player_connect", "player_activate"])
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()")

94
Torchlight/Utils.py Normal file
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)

2
Torchlight/__init__.py Normal file
View File

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

6
access.json Normal file
View File

@ -0,0 +1,6 @@
{
"[U:1:51174697]": {
"name": "BotoX",
"level": 10
}
}

48
config.json Normal file
View File

@ -0,0 +1,48 @@
{
"VoiceServer":
{
"Host": "10.0.0.101",
"Port": 27020,
"SampleRate": 48000
},
"SMAPIServer":
{
"Host": "10.0.0.101",
"Port": 27021
},
"AudioLimits":
{
"0":
{
"Uses": -1,
"TotalTime": 12.5,
"MaxLength": 5.0,
"DelayFactor": 10.0
},
"1":
{
"Uses": -1,
"TotalTime": 17.5,
"MaxLength": 5.0,
"DelayFactor": 5.0
}
},
"AntiSpam":
{
"MaxUsageSpan": 60,
"MaxUsageTime": 10,
"PunishDelay": 60,
"ImmunityLevel": 4
},
"TorchRCON":
{
"Host": "0.0.0.0",
"Port": 27015,
"Password": "***"
},
"WolframAPIKey": "***",
"WundergroundAPIKey": "***"
}

33
main.py Executable file
View File

@ -0,0 +1,33 @@
#!/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)
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()

21
requirements.txt Normal file
View File

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

BIN
sounds/Tutturuu_v1.wav Normal file

Binary file not shown.

3
triggers.json Normal file
View File

@ -0,0 +1,3 @@
[
{"names": ["!tuturu"], "sound": "Tutturuu_v1.wav"}
]