Initial commit.
This commit is contained in:
commit
02cce55db1
3
.gitignore
vendored
Executable file
3
.gitignore
vendored
Executable file
@ -0,0 +1,3 @@
|
||||
__pycache__
|
||||
*.pyc
|
||||
venv
|
25
README.md
Normal file
25
README.md
Normal 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`
|
43
Torchlight/AccessManager.py
Normal file
43
Torchlight/AccessManager.py
Normal 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
97
Torchlight/AsyncClient.py
Normal 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
271
Torchlight/AudioManager.py
Normal 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()
|
111
Torchlight/CommandHandler.py
Normal file
111
Torchlight/CommandHandler.py
Normal 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
730
Torchlight/Commands.py
Normal 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
24
Torchlight/Config.py
Normal 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
26
Torchlight/Constants.py
Normal 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)
|
178
Torchlight/FFmpegAudioPlayer.py
Normal file
178
Torchlight/FFmpegAudioPlayer.py
Normal 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
142
Torchlight/GameEvents.py
Normal 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
188
Torchlight/PlayerManager.py
Normal 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)
|
27
Torchlight/SourceModAPI.py
Normal file
27
Torchlight/SourceModAPI.py
Normal 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
|
105
Torchlight/SourceRCONServer.py
Normal file
105
Torchlight/SourceRCONServer.py
Normal 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
125
Torchlight/Torchlight.py
Normal 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
94
Torchlight/Utils.py
Normal 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
2
Torchlight/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
6
access.json
Normal file
6
access.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"[U:1:51174697]": {
|
||||
"name": "BotoX",
|
||||
"level": 10
|
||||
}
|
||||
}
|
48
config.json
Normal file
48
config.json
Normal 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
33
main.py
Executable 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
21
requirements.txt
Normal 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
BIN
sounds/Tutturuu_v1.wav
Normal file
Binary file not shown.
3
triggers.json
Normal file
3
triggers.json
Normal file
@ -0,0 +1,3 @@
|
||||
[
|
||||
{"names": ["!tuturu"], "sound": "Tutturuu_v1.wav"}
|
||||
]
|
Loading…
Reference in New Issue
Block a user