From 4b3c28c853fb7d23f767a8efea420cd45bd92659 Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 17 Mar 2021 20:46:54 +0100 Subject: [PATCH] initial release --- .../systemd/torchlight_restart.service | 11 + .../systemd/torchlight_restart_mg.service | 11 + .../torchlight3/README.md | 27 + .../torchlight3/Torchlight/AccessManager.py | 43 + .../torchlight3/Torchlight/AsyncClient.py | 96 ++ .../torchlight3/Torchlight/AudioManager.py | 351 +++++++ .../torchlight3/Torchlight/CommandHandler.py | 115 +++ .../Torchlight/CommandHandlermg.py | 116 +++ .../torchlight3/Torchlight/Commands.py | 928 +++++++++++++++++ .../torchlight3/Torchlight/Commandsmg.py | 930 ++++++++++++++++++ .../torchlight3/Torchlight/Config.py | 29 + .../torchlight3/Torchlight/Constants.py | 26 + .../Torchlight/FFmpegAudioPlayer.py | 180 ++++ .../torchlight3/Torchlight/GameEvents.py | 149 +++ .../torchlight3/Torchlight/PlayerManager.py | 201 ++++ .../torchlight3/Torchlight/SourceModAPI.py | 27 + .../Torchlight/SourceRCONServer.py | 106 ++ .../torchlight3/Torchlight/Subscribe.py | 159 +++ .../torchlight3/Torchlight/Torchlight.py | 149 +++ .../torchlight3/Torchlight/Torchlightmg.py | 150 +++ .../torchlight3/Torchlight/Utils.py | 94 ++ .../torchlight3/Torchlight/__init__.py | 2 + .../__pycache__/AccessManager.cpython-37.pyc | Bin 0 -> 2112 bytes .../__pycache__/AsyncClient.cpython-37.pyc | Bin 0 -> 3151 bytes .../__pycache__/AudioManager.cpython-37.pyc | Bin 0 -> 11497 bytes .../__pycache__/CommandHandler.cpython-37.pyc | Bin 0 -> 3088 bytes .../CommandHandlermg.cpython-37.pyc | Bin 0 -> 3114 bytes .../__pycache__/Commands.cpython-37.pyc | Bin 0 -> 25569 bytes .../__pycache__/Commandsmg.cpython-37.pyc | Bin 0 -> 25496 bytes .../__pycache__/Config.cpython-37.pyc | Bin 0 -> 1138 bytes .../__pycache__/Constants.cpython-37.pyc | Bin 0 -> 734 bytes .../FFmpegAudioPlayer.cpython-37.pyc | Bin 0 -> 4999 bytes .../__pycache__/PlayerManager.cpython-37.pyc | Bin 0 -> 10443 bytes .../__pycache__/SourceModAPI.cpython-37.pyc | Bin 0 -> 1035 bytes .../SourceRCONServer.cpython-37.pyc | Bin 0 -> 3272 bytes .../__pycache__/Subscribe.cpython-37.pyc | Bin 0 -> 4098 bytes .../__pycache__/Torchlight.cpython-37.pyc | Bin 0 -> 5222 bytes .../__pycache__/Torchlightmg.cpython-37.pyc | Bin 0 -> 5242 bytes .../__pycache__/Utils.cpython-37.pyc | Bin 0 -> 2426 bytes .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 146 bytes .../torchlight3/_start.sh | 4 + .../torchlight3/_start_mg.sh | 4 + .../torchlight3/access.json | 22 + torchlight_changes_unloze/torchlight3/main.py | 37 + .../torchlight3/mainmg.py | 37 + .../torchlight3/requirements.txt | 23 + torchlight_changes_unloze/torchlight3/run.sh | 5 + .../torchlight3/triggers.json | 405 ++++++++ 48 files changed, 4437 insertions(+) create mode 100644 torchlight_changes_unloze/systemd/torchlight_restart.service create mode 100644 torchlight_changes_unloze/systemd/torchlight_restart_mg.service create mode 100755 torchlight_changes_unloze/torchlight3/README.md create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/AccessManager.py create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/AsyncClient.py create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/AudioManager.py create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/CommandHandler.py create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/CommandHandlermg.py create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/Commands.py create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/Commandsmg.py create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/Config.py create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/Constants.py create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/FFmpegAudioPlayer.py create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/GameEvents.py create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/PlayerManager.py create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/SourceModAPI.py create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/SourceRCONServer.py create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/Subscribe.py create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/Torchlight.py create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/Torchlightmg.py create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/Utils.py create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/__init__.py create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/AccessManager.cpython-37.pyc create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/AsyncClient.cpython-37.pyc create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/AudioManager.cpython-37.pyc create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/CommandHandler.cpython-37.pyc create mode 100644 torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/CommandHandlermg.cpython-37.pyc create mode 100644 torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Commands.cpython-37.pyc create mode 100644 torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Commandsmg.cpython-37.pyc create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Config.cpython-37.pyc create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Constants.cpython-37.pyc create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/FFmpegAudioPlayer.cpython-37.pyc create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/PlayerManager.cpython-37.pyc create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/SourceModAPI.cpython-37.pyc create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/SourceRCONServer.cpython-37.pyc create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Subscribe.cpython-37.pyc create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Torchlight.cpython-37.pyc create mode 100644 torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Torchlightmg.cpython-37.pyc create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Utils.cpython-37.pyc create mode 100755 torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/__init__.cpython-37.pyc create mode 100755 torchlight_changes_unloze/torchlight3/_start.sh create mode 100755 torchlight_changes_unloze/torchlight3/_start_mg.sh create mode 100755 torchlight_changes_unloze/torchlight3/access.json create mode 100755 torchlight_changes_unloze/torchlight3/main.py create mode 100755 torchlight_changes_unloze/torchlight3/mainmg.py create mode 100755 torchlight_changes_unloze/torchlight3/requirements.txt create mode 100755 torchlight_changes_unloze/torchlight3/run.sh create mode 100755 torchlight_changes_unloze/torchlight3/triggers.json diff --git a/torchlight_changes_unloze/systemd/torchlight_restart.service b/torchlight_changes_unloze/systemd/torchlight_restart.service new file mode 100644 index 00000000..fd4e7e7b --- /dev/null +++ b/torchlight_changes_unloze/systemd/torchlight_restart.service @@ -0,0 +1,11 @@ +[Unit] +Description=Restarting torch on ze every 30 minutes + +[Service] +Type=simple +User=gameservers +WorkingDirectory=/home/gameservers/css_ze/torchlight3 +ExecStart=/home/gameservers/css_ze/torchlight3/_start.sh +Restart=always +RuntimeMaxSec=1800 + diff --git a/torchlight_changes_unloze/systemd/torchlight_restart_mg.service b/torchlight_changes_unloze/systemd/torchlight_restart_mg.service new file mode 100644 index 00000000..0a0b1005 --- /dev/null +++ b/torchlight_changes_unloze/systemd/torchlight_restart_mg.service @@ -0,0 +1,11 @@ +[Unit] +Description=Restarting torch on mg every 30 minutes + +[Service] +Type=simple +User=gameservers +WorkingDirectory=/home/gameservers/css_ze/torchlight3 +ExecStart=/home/gameservers/css_ze/torchlight3/_start_mg.sh +Restart=always +RuntimeMaxSec=1800 + diff --git a/torchlight_changes_unloze/torchlight3/README.md b/torchlight_changes_unloze/torchlight3/README.md new file mode 100755 index 00000000..4b7dcaf4 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/README.md @@ -0,0 +1,27 @@ +# Torchlight3 + +## 0. Requirements + * Python3.6 + * FFMPEG + * youtube-dl + * On game server: + * custom sourcemod + * sm-ext-AsyncSocket extension + * smjansson extension + * SMJSONAPI plugin + * sm-ext-Voice extension + +## 1. Install + * Install python3 and python-virtualenv + * Create a virtualenv: `virtualenv venv` + * Activate the virtualenv: `. venv/bin/activate` + * Install all dependencies: `pip install -r requirements.txt` + +## 2. Usage +Set up game server stuff. +Adapt config.json. + +##### Make sure you are in the virtualenv! (`. venv/bin/activate`) +Run: `python main.py` + +Glacius was here. xd diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/AccessManager.py b/torchlight_changes_unloze/torchlight3/Torchlight/AccessManager.py new file mode 100755 index 00000000..9b52f975 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/AccessManager.py @@ -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__() diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/AsyncClient.py b/torchlight_changes_unloze/torchlight3/Torchlight/AsyncClient.py new file mode 100755 index 00000000..16e1baf1 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/AsyncClient.py @@ -0,0 +1,96 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import asyncio +import logging +import json + +class ClientProtocol(asyncio.Protocol): + def __init__(self, loop, master): + self.Loop = loop + self.Master = master + self.Transport = None + self.Buffer = bytearray() + + def connection_made(self, transport): + self.Transport = transport + + def data_received(self, data): + self.Buffer += data + + chunks = self.Buffer.split(b'\0') + if data[-1] == b'\0': + chunks = chunks[:-1] + self.Buffer = bytearray() + else: + self.Buffer = bytearray(chunks[-1]) + chunks = chunks[:-1] + + for chunk in chunks: + self.Master.OnReceive(chunk) + + def connection_lost(self, exc): + self.Transport.close() + self.Transport = None + self.Master.OnDisconnect(exc) + + def Send(self, data): + if self.Transport: + self.Transport.write(data) + +class AsyncClient(): + def __init__(self, loop, host, port, master): + self.Logger = logging.getLogger(__class__.__name__) + self.Loop = loop + self.Host = host + self.Port = port + self.Master = master + + self.Protocol = None + self.SendLock = asyncio.Lock() + self.RecvFuture = None + + async def Connect(self): + while True: + self.Logger.warn("Reconnecting...") + try: + _, self.Protocol = await self.Loop.create_connection(lambda: ClientProtocol(self.Loop, self), host = self.Host, port = self.Port) + break + except: + await asyncio.sleep(1.0) + + def OnReceive(self, data): + Obj = json.loads(data) + + if "method" in Obj and Obj["method"] == "publish": + self.Master.OnPublish(Obj) + else: + if self.RecvFuture: + self.RecvFuture.set_result(Obj) + + def OnDisconnect(self, exc): + self.Protocol = None + if self.RecvFuture: + self.RecvFuture.cancel() + self.Master.OnDisconnect(exc) + + async def Send(self, obj): + if not self.Protocol: + return None + + Data = json.dumps(obj, ensure_ascii = False, separators = (',', ':')).encode("UTF-8") + + with (await self.SendLock): + if not self.Protocol: + return None + + self.RecvFuture = asyncio.Future() + self.Protocol.Send(Data) + await self.RecvFuture + + if self.RecvFuture.done(): + Obj = self.RecvFuture.result() + else: + Obj = None + + self.RecvFuture = None + return Obj diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/AudioManager.py b/torchlight_changes_unloze/torchlight3/Torchlight/AudioManager.py new file mode 100755 index 00000000..f56d97e1 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/AudioManager.py @@ -0,0 +1,351 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import logging +import sys +import io +import math +from .FFmpegAudioPlayer import FFmpegAudioPlayerFactory + +class AudioPlayerFactory(): + AUDIOPLAYER_FFMPEG = 1 + + def __init__(self, master): + self.Logger = logging.getLogger(__class__.__name__) + self.Master = master + self.Torchlight = self.Master.Torchlight + + self.FFmpegAudioPlayerFactory = FFmpegAudioPlayerFactory(self) + + def __del__(self): + self.Logger.info("~AudioPlayerFactory()") + + def NewPlayer(self, _type): + if _type == self.AUDIOPLAYER_FFMPEG: + return self.FFmpegAudioPlayerFactory.NewPlayer() + + +class AntiSpam(): + def __init__(self, master): + self.Logger = logging.getLogger(__class__.__name__) + self.Master = master + self.Torchlight = self.Master.Torchlight + + self.LastClips = dict() + self.DisabledTime = None + self.SaidHint = False + + def CheckAntiSpam(self, player): + if self.DisabledTime and self.DisabledTime > self.Torchlight().Master.Loop.time() and \ + not (player.Access and player.Access["level"] >= self.Torchlight().Config["AntiSpam"]["ImmunityLevel"]): + + self.Torchlight().SayPrivate(player, "Torchlight is currently on cooldown! ({0} seconds left)".format( + math.ceil(self.DisabledTime - self.Torchlight().Master.Loop.time()))) + return False + + return True + + def SpamCheck(self, Delta): + Now = self.Torchlight().Master.Loop.time() + Duration = 0.0 + + for Key, Clip in list(self.LastClips.items()): + if not Clip["timestamp"]: + continue + + if Clip["timestamp"] + Clip["duration"] + self.Torchlight().Config["AntiSpam"]["MaxUsageSpan"] < Now: + if not Clip["active"]: + del self.LastClips[Key] + continue + + Duration += Clip["duration"] + + if Duration > self.Torchlight().Config["AntiSpam"]["MaxUsageTime"]: + self.DisabledTime = self.Torchlight().Master.Loop.time() + self.Torchlight().Config["AntiSpam"]["PunishDelay"] + self.Torchlight().SayChat("Blocked voice commands for the next {0} seconds. Used {1} seconds within {2} seconds.".format( + self.Torchlight().Config["AntiSpam"]["PunishDelay"], self.Torchlight().Config["AntiSpam"]["MaxUsageTime"], self.Torchlight().Config["AntiSpam"]["MaxUsageSpan"])) + + # Make a copy of the list since AudioClip.Stop() will change the list + for AudioClip in self.Master.AudioClips[:]: + if AudioClip.Level < self.Torchlight().Config["AntiSpam"]["ImmunityLevel"]: + AudioClip.Stop() + + self.LastClips.clear() + + def OnPlay(self, clip): + Now = self.Torchlight().Master.Loop.time() + self.LastClips[hash(clip)] = dict({"timestamp": Now, "duration": 0.0, "dominant": False, "active": True}) + + HasDominant = False + for Key, Clip in self.LastClips.items(): + if Clip["dominant"]: + HasDominant = True + break + + self.LastClips[hash(clip)]["dominant"] = not HasDominant + + def OnStop(self, clip): + if hash(clip) not in self.LastClips: + return + + self.LastClips[hash(clip)]["active"] = False + + if self.LastClips[hash(clip)]["dominant"]: + for Key, Clip in self.LastClips.items(): + if Clip["active"]: + Clip["dominant"] = True + break + + self.LastClips[hash(clip)]["dominant"] = False + + def OnUpdate(self, clip, old_position, new_position): + Delta = new_position - old_position + Clip = self.LastClips[hash(clip)] + + if not Clip["dominant"]: + return + + Clip["duration"] += Delta + self.SpamCheck(Delta) + + +class Advertiser(): + def __init__(self, master): + self.Logger = logging.getLogger(__class__.__name__) + self.Master = master + self.Torchlight = self.Master.Torchlight + + self.LastClips = dict() + self.AdStop = 0 + self.NextAdStop = 0 + + def Think(self, Delta): + Now = self.Torchlight().Master.Loop.time() + Duration = 0.0 + + for Key, Clip in list(self.LastClips.items()): + if not Clip["timestamp"]: + continue + + if Clip["timestamp"] + Clip["duration"] + self.Torchlight().Config["Advertiser"]["MaxSpan"] < Now: + if not Clip["active"]: + del self.LastClips[Key] + continue + + Duration += Clip["duration"] + + self.NextAdStop -= Delta + CeilDur = math.ceil(Duration) + if CeilDur > self.AdStop and self.NextAdStop <= 0 and CeilDur % self.Torchlight().Config["Advertiser"]["AdStop"] == 0: + self.Torchlight().SayChat("Hint: Type \x07FF0000!stop(ze) !pls(mg)\x01 to stop all currently playing sounds.") + self.AdStop = CeilDur + self.NextAdStop = 0 + elif CeilDur < self.AdStop: + self.AdStop = 0 + self.NextAdStop = self.Torchlight().Config["Advertiser"]["AdStop"] / 2 + + def OnPlay(self, clip): + Now = self.Torchlight().Master.Loop.time() + self.LastClips[hash(clip)] = dict({"timestamp": Now, "duration": 0.0, "dominant": False, "active": True}) + + HasDominant = False + for Key, Clip in self.LastClips.items(): + if Clip["dominant"]: + HasDominant = True + break + + self.LastClips[hash(clip)]["dominant"] = not HasDominant + + def OnStop(self, clip): + if hash(clip) not in self.LastClips: + return + + self.LastClips[hash(clip)]["active"] = False + + if self.LastClips[hash(clip)]["dominant"]: + for Key, Clip in self.LastClips.items(): + if Clip["active"]: + Clip["dominant"] = True + break + + self.LastClips[hash(clip)]["dominant"] = False + + def OnUpdate(self, clip, old_position, new_position): + Delta = new_position - old_position + Clip = self.LastClips[hash(clip)] + + if not Clip["dominant"]: + return + + Clip["duration"] += Delta + self.Think(Delta) + + +class AudioManager(): + def __init__(self, torchlight): + self.Logger = logging.getLogger(__class__.__name__) + self.Torchlight = torchlight + self.AntiSpam = AntiSpam(self) + self.Advertiser = Advertiser(self) + self.AudioPlayerFactory = AudioPlayerFactory(self) + self.AudioClips = [] + + def __del__(self): + self.Logger.info("~AudioManager()") + + def CheckLimits(self, player): + Level = 0 + if player.Access: + Level = player.Access["level"] + + if str(Level) in self.Torchlight().Config["AudioLimits"]: + if self.Torchlight().Config["AudioLimits"][str(Level)]["Uses"] >= 0 and \ + player.Storage["Audio"]["Uses"] >= self.Torchlight().Config["AudioLimits"][str(Level)]["Uses"]: + + self.Torchlight().SayPrivate(player, "You have used up all of your free uses! ({0} uses)".format( + self.Torchlight().Config["AudioLimits"][str(Level)]["Uses"])) + return False + + if player.Storage["Audio"]["TimeUsed"] >= self.Torchlight().Config["AudioLimits"][str(Level)]["TotalTime"]: + self.Torchlight().SayPrivate(player, "You have used up all of your free time! ({0} seconds)".format( + self.Torchlight().Config["AudioLimits"][str(Level)]["TotalTime"])) + return False + + TimeElapsed = self.Torchlight().Master.Loop.time() - player.Storage["Audio"]["LastUse"] + UseDelay = player.Storage["Audio"]["LastUseLength"] * self.Torchlight().Config["AudioLimits"][str(Level)]["DelayFactor"] + + if TimeElapsed < UseDelay: + self.Torchlight().SayPrivate(player, "You are currently on cooldown! ({0} seconds left)".format( + round(UseDelay - TimeElapsed))) + return False + + return True + + def Stop(self, player, extra): + Level = 0 + if player.Access: + Level = player.Access["level"] + + for AudioClip in self.AudioClips[:]: + if extra and not extra.lower() in AudioClip.Player.Name.lower(): + continue + + if not Level or (Level < AudioClip.Level and Level < self.Torchlight().Config["AntiSpam"]["StopLevel"]): + AudioClip.Stops.add(player.UserID) + + if len(AudioClip.Stops) >= 3: + AudioClip.Stop() + self.Torchlight().SayPrivate(AudioClip.Player, "Your audio clip was stopped.") + if player != AudioClip.Player: + self.Torchlight().SayPrivate(player, "Stopped \"{0}\"({1}) audio clip.".format(AudioClip.Player.Name, AudioClip.Player.UserID)) + else: + self.Torchlight().SayPrivate(player, "This audio clip needs {0} more !stop's.".format(3 - len(AudioClip.Stops))) + else: + AudioClip.Stop() + self.Torchlight().SayPrivate(AudioClip.Player, "Your audio clip was stopped.") + if player != AudioClip.Player: + self.Torchlight().SayPrivate(player, "Stopped \"{0}\"({1}) audio clip.".format(AudioClip.Player.Name, AudioClip.Player.UserID)) + + def AudioClip(self, player, uri, _type = AudioPlayerFactory.AUDIOPLAYER_FFMPEG): + Level = 0 + if player.Access: + Level = player.Access["level"] + + if self.Torchlight().Disabled and self.Torchlight().Disabled > Level: + self.Torchlight().SayPrivate(player, "Torchlight is currently disabled!") + return None + + if not self.AntiSpam.CheckAntiSpam(player): + return None + + if not self.CheckLimits(player): + return None + + Clip = AudioClip(self, player, uri, _type) + self.AudioClips.append(Clip) + + if not player.Access or player.Access["level"] < self.Torchlight().Config["AntiSpam"]["ImmunityLevel"]: + Clip.AudioPlayer.AddCallback("Play", lambda *args: self.AntiSpam.OnPlay(Clip, *args)) + Clip.AudioPlayer.AddCallback("Stop", lambda *args: self.AntiSpam.OnStop(Clip, *args)) + Clip.AudioPlayer.AddCallback("Update", lambda *args: self.AntiSpam.OnUpdate(Clip, *args)) + + Clip.AudioPlayer.AddCallback("Play", lambda *args: self.Advertiser.OnPlay(Clip, *args)) + Clip.AudioPlayer.AddCallback("Stop", lambda *args: self.Advertiser.OnStop(Clip, *args)) + Clip.AudioPlayer.AddCallback("Update", lambda *args: self.Advertiser.OnUpdate(Clip, *args)) + + return Clip + + def OnDisconnect(self, player): + for AudioClip in self.AudioClips[:]: + if AudioClip.Player == player: + AudioClip.Stop() + + +class AudioClip(): + def __init__(self, master, player, uri, _type): + self.Logger = logging.getLogger(__class__.__name__) + self.Master = master + self.Torchlight = self.Master.Torchlight + self.Player = player + self.Type = _type + self.URI = uri + self.LastPosition = None + self.Stops = set() + + self.Level = 0 + if self.Player.Access: + self.Level = self.Player.Access["level"] + + self.AudioPlayer = self.Master.AudioPlayerFactory.NewPlayer(self.Type) + self.AudioPlayer.AddCallback("Play", self.OnPlay) + self.AudioPlayer.AddCallback("Stop", self.OnStop) + self.AudioPlayer.AddCallback("Update", self.OnUpdate) + + def __del__(self): + self.Logger.info("~AudioClip()") + + def Play(self, seconds = None, *args): + return self.AudioPlayer.PlayURI(self.URI, seconds, *args) + + def Stop(self): + return self.AudioPlayer.Stop() + + def OnPlay(self): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + self.URI) + + self.Player.Storage["Audio"]["Uses"] += 1 + self.Player.Storage["Audio"]["LastUse"] = self.Torchlight().Master.Loop.time() + self.Player.Storage["Audio"]["LastUseLength"] = 0.0 + + def OnStop(self): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + self.URI) + self.Master.AudioClips.remove(self) + + if self.AudioPlayer.Playing: + Delta = self.AudioPlayer.Position - self.LastPosition + self.Player.Storage["Audio"]["TimeUsed"] += Delta + self.Player.Storage["Audio"]["LastUseLength"] += Delta + + if str(self.Level) in self.Torchlight().Config["AudioLimits"]: + if self.Player.Storage: + if self.Player.Storage["Audio"]["TimeUsed"] >= self.Torchlight().Config["AudioLimits"][str(self.Level)]["TotalTime"]: + self.Torchlight().SayPrivate(self.Player, "You have used up all of your free time! ({0} seconds)".format( + self.Torchlight().Config["AudioLimits"][str(self.Level)]["TotalTime"])) + elif self.Player.Storage["Audio"]["LastUseLength"] >= self.Torchlight().Config["AudioLimits"][str(self.Level)]["MaxLength"]: + self.Torchlight().SayPrivate(self.Player, "Your audio clip exceeded the maximum length! ({0} seconds)".format( + self.Torchlight().Config["AudioLimits"][str(self.Level)]["MaxLength"])) + + del self.AudioPlayer + + def OnUpdate(self, old_position, new_position): + Delta = new_position - old_position + self.LastPosition = new_position + + self.Player.Storage["Audio"]["TimeUsed"] += Delta + self.Player.Storage["Audio"]["LastUseLength"] += Delta + + if not str(self.Level) in self.Torchlight().Config["AudioLimits"]: + return + + if (self.Player.Storage["Audio"]["TimeUsed"] >= self.Torchlight().Config["AudioLimits"][str(self.Level)]["TotalTime"] or + self.Player.Storage["Audio"]["LastUseLength"] >= self.Torchlight().Config["AudioLimits"][str(self.Level)]["MaxLength"]): + self.Stop() diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/CommandHandler.py b/torchlight_changes_unloze/torchlight3/Torchlight/CommandHandler.py new file mode 100755 index 00000000..e9b73ffa --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/CommandHandler.py @@ -0,0 +1,115 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import logging +import asyncio +import sys +import re +import traceback +import math +from importlib import reload +from . import Commands + +class CommandHandler(): + def __init__(self, Torchlight): + self.Logger = logging.getLogger(__class__.__name__) + self.Torchlight = Torchlight + self.Commands = [] + self.NeedsReload = False + + def Setup(self): + Counter = len(self.Commands) + self.Commands.clear() + if Counter: + self.Logger.info(sys._getframe().f_code.co_name + " Unloaded {0} commands!".format(Counter)) + + Counter = 0 + for subklass in sorted(Commands.BaseCommand.__subclasses__(), key = lambda x: x.Order, reverse = True): + try: + Command = subklass(self.Torchlight) + if hasattr(Command, "_setup"): + Command._setup() + except Exception as e: + self.Logger.error(traceback.format_exc()) + else: + self.Commands.append(Command) + Counter += 1 + + self.Logger.info(sys._getframe().f_code.co_name + " Loaded {0} commands!".format(Counter)) + + def Reload(self): + try: + reload(Commands) + except Exception as e: + self.Logger.error(traceback.format_exc()) + else: + self.Setup() + + async def HandleCommand(self, line, player): + Message = line.split(sep = ' ', maxsplit = 1) + if len(Message) < 2: + Message.append("") + Message[1] = Message[1].strip() + + if Message[1] and self.Torchlight().LastUrl: + Message[1] = Message[1].replace("!last", self.Torchlight().LastUrl) + line = Message[0] + ' ' + Message[1] + + Level = 0 + if player.Access: + Level = player.Access["level"] + + RetMessage = None + Ret = None + for Command in self.Commands: + for Trigger in Command.Triggers: + Match = False + RMatch = None + if isinstance(Trigger, tuple): + if Message[0].lower().startswith(Trigger[0], 0, Trigger[1]): + Match = True + elif isinstance(Trigger, str): + if Message[0].lower() == Trigger.lower(): + Match = True + else: # compiled regex + RMatch = Trigger.search(line) + if RMatch: + Match = True + + if not Match: + continue + + self.Logger.debug(sys._getframe().f_code.co_name + " \"{0}\" Match -> {1} | {2}".format(player.Name, Command.__class__.__name__, Trigger)) + + if Level < Command.Level: + RetMessage = "You do not have access to this command! (You: {0} | Required: {1})".format(Level, Command.Level) + continue + + try: + if RMatch: + Ret = await Command._rfunc(line, RMatch, player) + else: + Ret = await Command._func(Message, player) + except Exception as e: + self.Logger.error(traceback.format_exc()) + self.Torchlight().SayChat("Error: {0}".format(str(e))) + + RetMessage = None + + if isinstance(Ret, str): + Message = Ret.split(sep = ' ', maxsplit = 1) + Ret = None + + if Ret != None and Ret > 0: + break + + if Ret != None and Ret >= 0: + break + + if RetMessage: + self.Torchlight().SayPrivate(player, RetMessage) + + if self.NeedsReload: + self.NeedsReload = False + self.Reload() + + return Ret diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/CommandHandlermg.py b/torchlight_changes_unloze/torchlight3/Torchlight/CommandHandlermg.py new file mode 100755 index 00000000..bf00d8f2 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/CommandHandlermg.py @@ -0,0 +1,116 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import logging +import asyncio +import sys +import re +import traceback +import math +from importlib import reload +from . import Commandsmg + +class CommandHandlermg(): + def __init__(self, Torchlight): + self.Logger = logging.getLogger(__class__.__name__) + self.Torchlight = Torchlight + self.Commands = [] + self.NeedsReload = False + + def Setup(self): + Counter = len(self.Commands) + self.Commands.clear() + if Counter: + self.Logger.info(sys._getframe().f_code.co_name + " Unloaded {0} commands!".format(Counter)) + + Counter = 0 + for subklass in sorted(Commandsmg.BaseCommand.__subclasses__(), key = lambda x: x.Order, reverse = True): + try: + Command = subklass(self.Torchlight) + if hasattr(Command, "_setup"): + Command._setup() + except Exception as e: + self.Logger.error(traceback.format_exc()) + else: + self.Commands.append(Command) + Counter += 1 + + self.Logger.info(sys._getframe().f_code.co_name + " Loaded {0} commands!".format(Counter)) + + def Reload(self): + try: + reload(Commandsmg) + except Exception as e: + self.Logger.error(traceback.format_exc()) + else: + self.Setup() + + async def HandleCommand(self, line, player): + Message = line.split(sep = ' ', maxsplit = 1) + if len(Message) < 2: + Message.append("") + Message[1] = Message[1].strip() + + if Message[1] and self.Torchlight().LastUrl: + Message[1] = Message[1].replace("!last", self.Torchlight().LastUrl) + line = Message[0] + ' ' + Message[1] + + Level = 0 + if player.Access: + Level = player.Access["level"] + + RetMessage = None + Ret = None + for Command in self.Commands: + for Trigger in Command.Triggers: + Match = False + RMatch = None + if isinstance(Trigger, tuple): + if Message[0].lower().startswith(Trigger[0], 0, Trigger[1]): + Match = True + elif isinstance(Trigger, str): + if Message[0].lower() == Trigger.lower(): + Match = True + else: # compiled regex + RMatch = Trigger.search(line) + if RMatch: + Match = True + + if not Match: + continue + + self.Logger.debug(sys._getframe().f_code.co_name + " \"{0}\" Match -> {1} | {2}".format(player.Name, Command.__class__.__name__, Trigger)) + + if Level < Command.Level: + RetMessage = "You do not have access to this command! (You: {0} | Required: {1})".format(Level, Command.Level) + continue + + try: + if RMatch: + Ret = await Command._rfunc(line, RMatch, player) + else: + Ret = await Command._func(Message, player) + except Exception as e: + self.Logger.error(traceback.format_exc()) + self.Torchlight().SayChat("Error: {0}".format(str(e))) + + RetMessage = None + + if isinstance(Ret, str): + Message = Ret.split(sep = ' ', maxsplit = 1) + Ret = None + + if Ret != None and Ret > 0: + break + + if Ret != None and Ret >= 0: + break + + if RetMessage: + self.Torchlight().SayPrivate(player, RetMessage) + + if self.NeedsReload: + self.NeedsReload = False + self.Reload() + + return Ret + diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/Commands.py b/torchlight_changes_unloze/torchlight3/Torchlight/Commands.py new file mode 100755 index 00000000..5f76e5cc --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/Commands.py @@ -0,0 +1,928 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import asyncio +import os +import sys +import logging +import math +from .Utils import Utils, DataHolder +import traceback + +class BaseCommand(): + Order = 0 + def __init__(self, torchlight): + self.Logger = logging.getLogger(__class__.__name__) + self.Torchlight = torchlight + self.Triggers = [] + self.Level = 0 + + def check_chat_cooldown(self, player): + if player.ChatCooldown > self.Torchlight().Master.Loop.time(): + cooldown = player.ChatCooldown - self.Torchlight().Master.Loop.time() + self.Torchlight().SayPrivate(player, "You're on cooldown for the next {0:.1f} seconds.".format(cooldown)) + return True + + def check_disabled(self, player): + Level = 0 + if player.Access: + Level = player.Access["level"] + + Disabled = self.Torchlight().Disabled + if Disabled and (Disabled > Level or Disabled == Level and Level < self.Torchlight().Config["AntiSpam"]["ImmunityLevel"]): + self.Torchlight().SayPrivate(player, "Torchlight is currently disabled!") + return True + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name) + + +class URLFilter(BaseCommand): + Order = 1 + import re + import aiohttp + import magic + import datetime + import json + import io + from bs4 import BeautifulSoup + from PIL import Image + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = [self.re.compile(r'''(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))''', self.re.IGNORECASE)] + self.Level = 10 + self.re_youtube = self.re.compile(r'.*?(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)([a-zA-Z0-9_-]{6,11}).*?') + + async def URLInfo(self, url, yt = False): + Text = None + Info = None + match = self.re_youtube.search(url) + if match or yt: + Temp = DataHolder() + Time = None + + if Temp(url.find("&t=")) != -1 or Temp(url.find("?t=")) != -1 or Temp(url.find("#t=")) != -1: + TimeStr = url[Temp.value + 3:].split('&')[0].split('?')[0].split('#')[0] + if TimeStr: + Time = Utils.ParseTime(TimeStr) + + Proc = await asyncio.create_subprocess_exec("youtube-dl", "--dump-json", "-g", url, + stdout = asyncio.subprocess.PIPE) + Out, _ = await Proc.communicate() + + parts = Out.split(b'\n') + parts.pop() # trailing new line + + Info = parts.pop() + url = parts.pop() + + url = url.strip().decode("ascii") + Info = self.json.loads(Info) + + if Info["extractor_key"] == "Youtube": + self.Torchlight().SayChat("\x07E52D27[YouTube]\x01 {0} | {1} | {2}/5.00 | {3:,}".format( + Info["title"], str(self.datetime.timedelta(seconds = Info["duration"])), round(Info["average_rating"], 2), int(Info["view_count"]))) + else: + match = None + + if Time: + url += "#t={0}".format(Time) + + else: + try: + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get(url), 5) + if Response: + ContentType = Response.headers.get("Content-Type") + ContentLength = Response.headers.get("Content-Length") + Content = await asyncio.wait_for(Response.content.read(65536), 5) + + if not ContentLength: + ContentLength = -1 + + if ContentType.startswith("text"): + if ContentType.startswith("text/plain"): + Text = Content.decode("utf-8", errors = "ignore") + else: + Soup = self.BeautifulSoup(Content.decode("utf-8", errors = "ignore"), "lxml") + if Soup.title: + self.Torchlight().SayChat("[URL] {0}".format(Soup.title.string)) + elif ContentType.startswith("image"): + fp = self.io.BytesIO(Content) + im = self.Image.open(fp) + self.Torchlight().SayChat("[IMAGE] {0} | Width: {1} | Height: {2} | Size: {3}".format(im.format, im.size[0], im.size[1], Utils.HumanSize(ContentLength))) + fp.close() + else: + Filetype = self.magic.from_buffer(bytes(Content)) + self.Torchlight().SayChat("[FILE] {0} | Size: {1}".format(Filetype, Utils.HumanSize(ContentLength))) + + Response.close() + except Exception as e: + self.Torchlight().SayChat("Error: {0}".format(str(e))) + self.Logger.error(traceback.format_exc()) + + self.Torchlight().LastUrl = url + return url, Text + + async def _rfunc(self, line, match, player): + Url = match.groups()[0] + if not Url.startswith("http") and not Url.startswith("ftp"): + Url = "http://" + Url + + if line.startswith("!yt "): + URL, _ = await self.URLInfo(Url, True) + return "!yt " + URL + + if line.startswith("!dec "): + _, text = await self.URLInfo(Url, False) + if text: + return "!dec " + text + + asyncio.ensure_future(self.URLInfo(Url)) + return -1 + + +def FormatAccess(Torchlight, player): + Answer = "#{0} \"{1}\"({2}) is ".format(player.UserID, player.Name, player.UniqueID) + Level = str(0) + if player.Access: + Level = str(player.Access["level"]) + Answer += "level {0!s} as {1}.".format(Level, player.Access["name"]) + else: + Answer += "not authenticated." + + if Level in Torchlight().Config["AudioLimits"]: + Uses = Torchlight().Config["AudioLimits"][Level]["Uses"] + TotalTime = Torchlight().Config["AudioLimits"][Level]["TotalTime"] + + if Uses >= 0: + Answer += " Uses: {0}/{1}".format(player.Storage["Audio"]["Uses"], Uses) + if TotalTime >= 0: + Answer += " Time: {0}/{1}".format(round(player.Storage["Audio"]["TimeUsed"], 2), round(TotalTime, 2)) + + return Answer + +class Access(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!access"] + self.Level = 0 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_chat_cooldown(player): + return -1 + + Count = 0 + if message[0] == "!access": + if message[1]: + return -1 + + self.Torchlight().SayChat(FormatAccess(self.Torchlight, player), player) + + return 0 + +class Who(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!who", "!whois"] + self.Level = 1 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + Count = 0 + if message[0] == "!who": + for Player in self.Torchlight().Players: + if Player.Name.lower().find(message[1].lower()) != -1: + self.Torchlight().SayChat(FormatAccess(self.Torchlight, Player)) + + Count += 1 + if Count >= 3: + break + + elif message[0] == "!whois": + for UniqueID, Access in self.Torchlight().Access: + if Access["name"].lower().find(message[1].lower()) != -1: + Player = self.Torchlight().Players.FindUniqueID(UniqueID) + if Player: + self.Torchlight().SayChat(FormatAccess(self.Torchlight, Player)) + else: + self.Torchlight().SayChat("#? \"{0}\"({1}) is level {2!s} is currently offline.".format(Access["name"], UniqueID, Access["level"])) + + Count += 1 + if Count >= 3: + break + return 0 + + +class WolframAlpha(BaseCommand): + import urllib.parse + import aiohttp + import xml.etree.ElementTree as etree + import re + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!cc"] + self.Level = 10 + + def Clean(self, Text): + return self.re.sub("[ ]{2,}", " ", Text.replace(' | ', ': ').replace('\n', ' | ').replace('~~', ' ≈ ')).strip() + + async def Calculate(self, Params, player): + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get("http://api.wolframalpha.com/v2/query", params=Params), 10) + if not Response: + return 1 + + Data = await asyncio.wait_for(Response.text(), 5) + if not Data: + return 2 + + Root = self.etree.fromstring(Data) + + + # Find all pods with plaintext answers + # Filter out None -answers, strip strings and filter out the empty ones + Pods = list(filter(None, [p.text.strip() for p in Root.findall('.//subpod/plaintext') if p is not None and p.text is not None])) + + # no answer pods found, check if there are didyoumeans-elements + if not Pods: + Didyoumeans = Root.find("didyoumeans") + # no support for future stuff yet, TODO? + if not Didyoumeans: + # If there's no pods, the question clearly wasn't understood + self.Torchlight().SayChat("Sorry, couldn't understand the question.", player) + return 3 + + Options = [] + for Didyoumean in Didyoumeans: + Options.append("\"{0}\"".format(Didyoumean.text)) + Line = " or ".join(Options) + Line = "Did you mean {0}?".format(Line) + self.Torchlight().SayChat(Line, player) + return 0 + + # If there's only one pod with text, it's probably the answer + # example: "integral x²" + if len(Pods) == 1: + Answer = self.Clean(Pods[0]) + self.Torchlight().SayChat(Answer, player) + return 0 + + # If there's multiple pods, first is the question interpretation + Question = self.Clean(Pods[0].replace(' | ', ' ').replace('\n', ' ')) + # and second is the best answer + Answer = self.Clean(Pods[1]) + self.Torchlight().SayChat("{0} = {1}".format(Question, Answer), player) + return 0 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_chat_cooldown(player): + return -1 + + if self.check_disabled(player): + return -1 + + Params = dict({"input": message[1], "appid": self.Torchlight().Config["WolframAPIKey"]}) + Ret = await self.Calculate(Params, player) + return Ret + + +class UrbanDictionary(BaseCommand): + import aiohttp + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!define", "!ud"] + self.Level = 10 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_chat_cooldown(player): + return -1 + + if self.check_disabled(player): + return -1 + + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get("https://api.urbandictionary.com/v0/define?term={0}".format(message[1])), 5) + if not Response: + return 1 + + Data = await asyncio.wait_for(Response.json(), 5) + if not Data: + return 3 + + if not 'list' in Data or not Data["list"]: + self.Torchlight().SayChat("[UB] No definition found for: {}".format(message[1]), player) + return 4 + + def print_item(item): + self.Torchlight().SayChat("[UD] {word} ({thumbs_up}/{thumbs_down}): {definition}\n{example}".format(**item), player) + + print_item(Data["list"][0]) + + +class OpenWeather(BaseCommand): + import aiohttp + import geoip2.database + def __init__(self, torchlight): + super().__init__(torchlight) + self.GeoIP = self.geoip2.database.Reader("/usr/share/GeoIP/GeoLite2-City.mmdb") + self.Triggers = ["!w", "!vv"] + self.Level = 10 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_chat_cooldown(player): + return -1 + + if self.check_disabled(player): + return -1 + + if not message[1]: + # Use GeoIP location + info = self.GeoIP.city(player.Address.split(":")[0]) + Search = "lat={}&lon={}".format(info.location.latitude, info.location.longitude) + else: + Search = "q={}".format(message[1]) + + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get("https://api.openweathermap.org/data/2.5/weather?APPID={0}&units=metric&{1}".format( + self.Torchlight().Config["OpenWeatherAPIKey"], Search)), 5) + if not Response: + return 2 + + Data = await asyncio.wait_for(Response.json(), 5) + if not Data: + return 3 + + if Data["cod"] != 200: + self.Torchlight().SayPrivate(player, "[OW] {0}".format(Data["message"])) + return 5 + + degToCardinal = lambda d: ["N", "NE", "E", "SE", "S", "SW", "W", "NW"][int(((d + 22.5)/45.0) % 8)] + if "deg" in Data["wind"]: + windDir = degToCardinal(Data["wind"]["deg"]) + else: + windDir = "?" + + timezone = "{}{}".format('+' if Data["timezone"] > 0 else '', int(Data["timezone"] / 3600)) + if Data["timezone"] % 3600 != 0: + timezone += ":{}".format((Data["timezone"] % 3600) / 60) + + self.Torchlight().SayChat("[{}, {}](UTC{}) {}°C ({}/{}) {}: {} | Wind {} {}kph | Clouds: {}%% | Humidity: {}%%".format(Data["name"], Data["sys"]["country"], timezone, + Data["main"]["temp"], Data["main"]["temp_min"], Data["main"]["temp_max"], Data["weather"][0]["main"], Data["weather"][0]["description"], + windDir, Data["wind"]["speed"], Data["clouds"]["all"], Data["main"]["humidity"]), player) + + return 0 + +''' +class WUnderground(BaseCommand): + import aiohttp + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!w"] + self.Level = 0 + + async def _func(self, message, player): + if not message[1]: + # Use IP address + Search = "autoip" + Additional = "?geo_ip={0}".format(player.Address.split(":")[0]) + else: + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get("http://autocomplete.wunderground.com/aq?format=JSON&query={0}".format(message[1])), 5) + if not Response: + return 2 + + Data = await asyncio.wait_for(Response.json(), 5) + if not Data: + return 3 + + if not Data["RESULTS"]: + self.Torchlight().SayPrivate(player, "[WU] No cities match your search query.") + return 4 + + Search = Data["RESULTS"][0]["name"] + Additional = "" + + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get("http://api.wunderground.com/api/{0}/conditions/q/{1}.json{2}".format( + self.Torchlight().Config["WundergroundAPIKey"], Search, Additional)), 5) + if not Response: + return 2 + + Data = await asyncio.wait_for(Response.json(), 5) + if not Data: + return 3 + + if "error" in Data["response"]: + self.Torchlight().SayPrivate(player, "[WU] {0}.".format(Data["response"]["error"]["description"])) + return 5 + + if not "current_observation" in Data: + Choices = str() + NumResults = len(Data["response"]["results"]) + for i, Result in enumerate(Data["response"]["results"]): + Choices += "{0}, {1}".format(Result["city"], + Result["state"] if Result["state"] else Result ["country_iso3166"]) + + if i < NumResults - 1: + Choices += " | " + + self.Torchlight().SayPrivate(player, "[WU] Did you mean: {0}".format(Choices)) + return 6 + + Observation = Data["current_observation"] + + self.Torchlight().SayChat("[{0}, {1}] {2}°C ({3}F) {4} | Wind {5} {6}kph ({7}mph) | Humidity: {8}".format(Observation["display_location"]["city"], + Observation["display_location"]["state"] if Observation["display_location"]["state"] else Observation["display_location"]["country_iso3166"], + Observation["temp_c"], Observation["temp_f"], Observation["weather"], + Observation["wind_dir"], Observation["wind_kph"], Observation["wind_mph"], + Observation["relative_humidity"])) + + return 0 +''' + +class VoteDisable(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!votedisable", "!disablevote"] + self.Level = 0 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.Torchlight().Disabled: + self.Torchlight().SayPrivate(player, "Torchlight is already disabled for the duration of this map.") + return + + self.Torchlight().DisableVotes.add(player.UniqueID) + + have = len(self.Torchlight().DisableVotes) + needed = len(self.Torchlight().Players) // 5 + if have >= needed: + self.Torchlight().SayChat("Torchlight has been disabled for the duration of this map.") + self.Torchlight().Disabled = 6 + else: + self.Torchlight().SayPrivate(player, "Torchlight needs {0} more disable votes to be disabled.".format(needed - have)) + + +class VoiceCommands(BaseCommand): + import json + import random + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!random", "!search"] + self.Level = 0 + + def LoadTriggers(self): + try: + with open("triggers.json", "r") as fp: + Triggers = self.json.load(fp) + except ValueError as e: + self.Logger.error(sys._getframe().f_code.co_name + ' ' + str(e)) + self.Torchlight().SayChat(str(e)) + + self.VoiceTriggers = dict() + for Line in Triggers: + for Trigger in Line["names"]: + self.VoiceTriggers[Trigger] = Line["sound"] + + def _setup(self): + self.Logger.debug(sys._getframe().f_code.co_name) + self.LoadTriggers() + for Trigger in self.VoiceTriggers.keys(): + self.Triggers.append(Trigger) + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_disabled(player): + return -1 + + Level = 0 + if player.Access: + Level = player.Access["level"] + + message[0] = message[0].lower() + message[1] = message[1].lower() + if message[0][0] != '!' and Level < 2: + return 1 + + if message[0] == "!search": + res = [] + for key in self.VoiceTriggers.keys(): + if message[1] in key.lower(): + res.append(key) + self.Torchlight().SayPrivate(player, "{} results: {}".format(len(res), ", ".join(res))) + return 0 + elif Level < 2: + return 0 + + if message[0] == "!random": + Trigger = self.random.choice(list(self.VoiceTriggers.values())) + if isinstance(Trigger, list): + Sound = self.random.choice(Trigger) + else: + Sound = Trigger + else: + Sounds = self.VoiceTriggers[message[0]] + + try: + Num = int(message[1]) + except ValueError: + Num = None + + if isinstance(Sounds, list): + if Num and Num > 0 and Num <= len(Sounds): + Sound = Sounds[Num - 1] + + elif message[1]: + searching = message[1].startswith('?') + search = message[1][1:] if searching else message[1] + Sound = None + names = [] + matches = [] + for sound in Sounds: + name = os.path.splitext(os.path.basename(sound))[0] + names.append(name) + + if search and search in name.lower(): + matches.append((name, sound)) + + if matches: + matches.sort(key=lambda t: len(t[0])) + mlist = [t[0] for t in matches] + if searching: + self.Torchlight().SayPrivate(player, "{} results: {}".format(len(mlist), ", ".join(mlist))) + return 0 + + Sound = matches[0][1] + if len(matches) > 1: + self.Torchlight().SayPrivate(player, "Multiple matches: {}".format(", ".join(mlist))) + + if not Sound and not Num: + if not searching: + self.Torchlight().SayPrivate(player, "Couldn't find {} in list of sounds.".format(message[1])) + self.Torchlight().SayPrivate(player, ", ".join(names)) + return 1 + + elif Num: + self.Torchlight().SayPrivate(player, "Number {} is out of bounds, max {}.".format(Num, len(Sounds))) + return 1 + + else: + Sound = self.random.choice(Sounds) + else: + Sound = Sounds + + if not Sound: + return 1 + + Path = os.path.abspath(os.path.join("sounds", Sound)) + AudioClip = self.Torchlight().AudioManager.AudioClip(player, "file://" + Path) + if not AudioClip: + return 1 + + return AudioClip.Play() + + +class YouTube(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!yt"] + self.Level = 3 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_disabled(player): + return -1 + + if self.Torchlight().LastUrl: + message[1] = message[1].replace("!last", self.Torchlight().LastUrl) + + Temp = DataHolder() + Time = None + + if Temp(message[1].find("&t=")) != -1 or Temp(message[1].find("?t=")) != -1 or Temp(message[1].find("#t=")) != -1: + TimeStr = message[1][Temp.value + 3:].split('&')[0].split('?')[0].split('#')[0] + if TimeStr: + Time = Utils.ParseTime(TimeStr) + + AudioClip = self.Torchlight().AudioManager.AudioClip(player, message[1]) + if not AudioClip: + return 1 + + return AudioClip.Play(Time) + +class YouTubeSearch(BaseCommand): + import json + import datetime + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!yts"] + self.Level = 3 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_disabled(player): + return -1 + + Temp = DataHolder() + Time = None + + if Temp(message[1].find("&t=")) != -1 or Temp(message[1].find("?t=")) != -1 or Temp(message[1].find("#t=")) != -1: + TimeStr = message[1][Temp.value + 3:].split('&')[0].split('?')[0].split('#')[0] + if TimeStr: + Time = Utils.ParseTime(TimeStr) + message[1] = message[1][:Temp.value] + + Proc = await asyncio.create_subprocess_exec("youtube-dl", "--dump-json", "-xg", "ytsearch:" + message[1], + stdout = asyncio.subprocess.PIPE) + Out, _ = await Proc.communicate() + + print('out value: ', Out) + url, Info = Out.split(b'\n', maxsplit = 1) + url = url.strip().decode("ascii") + Info = self.json.loads(Info) + + if Info["extractor_key"] == "Youtube": + self.Torchlight().SayChat("\x07E52D27[YouTube]\x01 {0} | {1} | {2}/5.00 | {3:,}".format( + Info["title"], str(self.datetime.timedelta(seconds = Info["duration"])), round(Info["average_rating"] or 0, 2), int(Info["view_count"]))) + AudioClip = self.Torchlight().AudioManager.AudioClip(player, url) + if not AudioClip: + return 1 + self.Torchlight().LastUrl = url + return AudioClip.Play(Time) + + +class Say(BaseCommand): + import gtts + import tempfile + VALID_LANGUAGES = [lang for lang in gtts.lang.tts_langs().keys()] + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = [("!say", 4)] + self.Level = 2 + + async def Say(self, player, language, message): + GTTS = self.gtts.gTTS(text = message, lang = language) + + TempFile = self.tempfile.NamedTemporaryFile(delete = False) + GTTS.write_to_fp(TempFile) + TempFile.close() + + AudioClip = self.Torchlight().AudioManager.AudioClip(player, "file://" + TempFile.name) + if not AudioClip: + os.unlink(TempFile.name) + return 1 + + if AudioClip.Play(): + AudioClip.AudioPlayer.AddCallback("Stop", lambda: os.unlink(TempFile.name)) + return 0 + else: + os.unlink(TempFile.name) + return 1 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_disabled(player): + return -1 + + if not message[1]: + return 1 + + Language = "en" + if len(message[0]) > 4: + Language = message[0][4:] + + if not Language in self.VALID_LANGUAGES: + return 1 + + asyncio.ensure_future(self.Say(player, Language, message[1])) + return 0 + +''' +class DECTalk(BaseCommand): + import tempfile + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!dec"] + self.Level = 0 + + async def Say(self, player, message): + message = "[:phoneme on]" + message + TempFile = self.tempfile.NamedTemporaryFile(delete = False) + TempFile.close() + + Proc = await asyncio.create_subprocess_exec("wine", "say.exe", "-w", TempFile.name, + cwd = "dectalk", stdin = asyncio.subprocess.PIPE) + await Proc.communicate(message.encode('utf-8', errors='ignore')) + + AudioClip = self.Torchlight().AudioManager.AudioClip(player, "file://" + TempFile.name) + if not AudioClip: + os.unlink(TempFile.name) + return 1 + + if AudioClip.Play(None, "-af", "volume=10dB"): + AudioClip.AudioPlayer.AddCallback("Stop", lambda: os.unlink(TempFile.name)) + return 0 + else: + os.unlink(TempFile.name) + return 1 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_disabled(player): + return -1 + + if not message[1]: + return 1 + + asyncio.ensure_future(self.Say(player, message[1])) + return 0 +''' + +class Stop(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!stop"] + self.Level = 0 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + self.Torchlight().AudioManager.Stop(player, message[1]) + return True + + +class EnableDisable(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!enable", "!disable"] + self.Level = 3 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if message[0] == "!enable": + if self.Torchlight().Disabled: + if self.Torchlight().Disabled > player.Access["level"]: + self.Torchlight().SayPrivate(player, "You don't have access to enable torchlight, since it was disabled by a higher level user.") + return 1 + self.Torchlight().SayChat("Torchlight has been enabled for the duration of this map - Type !disable to disable it again.") + + self.Torchlight().Disabled = False + + elif message[0] == "!disable": + if self.Torchlight().Disabled > player.Access["level"]: + self.Torchlight().SayPrivate(player, "You don't have access to disable torchlight, since it was already disabled by a higher level user.") + return 1 + self.Torchlight().SayChat("Torchlight has been disabled for the duration of this map - Type !enable to enable it again.") + self.Torchlight().Disabled = player.Access["level"] + + +class AdminAccess(BaseCommand): + from collections import OrderedDict + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!access"] + self.Level = 4 + + def ReloadValidUsers(self): + self.Torchlight().Access.Load() + for Player in self.Torchlight().Players: + Access = self.Torchlight().Access[Player.UniqueID] + Player.Access = Access + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + if not message[1]: + return -1 + + if message[1].lower() == "reload": + self.ReloadValidUsers() + self.Torchlight().SayChat("Loaded access list with {0} users".format(len(self.Torchlight().Access))) + + elif message[1].lower() == "save": + self.Torchlight().Access.Save() + self.Torchlight().SayChat("Saved access list with {0} users".format(len(self.Torchlight().Access))) + + # Modify access + else: + Player = None + Buf = message[1] + Temp = Buf.find(" as ") + if Temp != -1: + try: + Regname, Level = Buf[Temp + 4:].rsplit(' ', 1) + except ValueError as e: + self.Torchlight().SayChat(str(e)) + return 1 + + Regname = Regname.strip() + Level = Level.strip() + Buf = Buf[:Temp].strip() + else: + try: + Buf, Level = Buf.rsplit(' ', 1) + except ValueError as e: + self.Torchlight().SayChat(str(e)) + return 2 + + Buf = Buf.strip() + Level = Level.strip() + + # Find user by User ID + if Buf[0] == '#' and Buf[1:].isnumeric(): + Player = self.Torchlight().Players.FindUserID(int(Buf[1:])) + # Search user by name + else: + for Player_ in self.Torchlight().Players: + if Player_.Name.lower().find(Buf.lower()) != -1: + Player = Player_ + break + + if not Player: + self.Torchlight().SayChat("Couldn't find user: {0}".format(Buf)) + return 3 + + if Level.isnumeric() or (Level.startswith('-') and Level[1:].isdigit()): + Level = int(Level) + + if Level >= player.Access["level"] and player.Access["level"] < 10: + self.Torchlight().SayChat("Trying to assign level {0}, which is higher or equal than your level ({1})".format(Level, player.Access["level"])) + return 4 + + if Player.Access: + if Player.Access["level"] >= player.Access["level"] and player.Access["level"] < 10: + self.Torchlight().SayChat("Trying to modify level {0}, which is higher or equal than your level ({1})".format(Player.Access["level"], player.Access["level"])) + return 5 + + if "Regname" in locals(): + self.Torchlight().SayChat("Changed \"{0}\"({1}) as {2} level/name from {3} to {4} as {5}".format( + Player.Name, Player.UniqueID, Player.Access["name"], Player.Access["level"], Level, Regname)) + Player.Access["name"] = Regname + else: + self.Torchlight().SayChat("Changed \"{0}\"({1}) as {2} level from {3} to {4}".format( + Player.Name, Player.UniqueID, Player.Access["name"], Player.Access["level"], Level)) + + Player.Access["level"] = Level + self.Torchlight().Access[Player.UniqueID] = Player.Access + else: + if not "Regname" in locals(): + Regname = Player.Name + + self.Torchlight().Access[Player.UniqueID] = self.OrderedDict([("name", Regname), ("level", Level)]) + Player.Access = self.Torchlight().Access[Player.UniqueID] + self.Torchlight().SayChat("Added \"{0}\"({1}) to access list as {2} with level {3}".format(Player.Name, Player.UniqueID, Regname, Level)) + else: + if Level == "revoke" and Player.Access: + if Player.Access["level"] >= player.Access["level"] and player.Access["level"] < 10: + self.Torchlight().SayChat("Trying to revoke level {0}, which is higher or equal than your level ({1})".format(Player.Access["level"], player.Access["level"])) + return 6 + + self.Torchlight().SayChat("Removed \"{0}\"({1}) from access list (was {2} with level {3})".format( + Player.Name, Player.UniqueID, Player.Access["name"], Player.Access["level"])) + del self.Torchlight().Access[Player.UniqueID] + Player.Access = None + return 0 + +class Reload(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!reload"] + self.Level = 4 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + self.Torchlight().Reload() + return 0 + +""" +class Exec(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!exec"] + self.Level = 100 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + try: + Response = eval(message[1]) + except Exception as e: + self.Torchlight().SayChat("Error: {0}".format(str(e))) + return 1 + self.Torchlight().SayChat(str(Response)) + return 0 +""" diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/Commandsmg.py b/torchlight_changes_unloze/torchlight3/Torchlight/Commandsmg.py new file mode 100755 index 00000000..7b954de6 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/Commandsmg.py @@ -0,0 +1,930 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import asyncio +import os +import sys +import logging +import math +from .Utils import Utils, DataHolder +import traceback + +class BaseCommand(): + Order = 0 + def __init__(self, torchlight): + self.Logger = logging.getLogger(__class__.__name__) + self.Torchlight = torchlight + self.Triggers = [] + self.Level = 0 + + def check_chat_cooldown(self, player): + if player.ChatCooldown > self.Torchlight().Master.Loop.time(): + cooldown = player.ChatCooldown - self.Torchlight().Master.Loop.time() + self.Torchlight().SayPrivate(player, "You're on cooldown for the next {0:.1f} seconds.".format(cooldown)) + return True + + def check_disabled(self, player): + Level = 0 + if player.Access: + Level = player.Access["level"] + + Disabled = self.Torchlight().Disabled + if Disabled and (Disabled > Level or Disabled == Level and Level < self.Torchlight().Config["AntiSpam"]["ImmunityLevel"]): + self.Torchlight().SayPrivate(player, "Torchlight is currently disabled!") + return True + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name) + + +class URLFilter(BaseCommand): + Order = 1 + import re + import aiohttp + import magic + import datetime + import json + import io + from bs4 import BeautifulSoup + from PIL import Image + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = [self.re.compile(r'''(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))''', self.re.IGNORECASE)] + self.Level = 10 + self.re_youtube = self.re.compile(r'.*?(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)([a-zA-Z0-9_-]{6,11}).*?') + + async def URLInfo(self, url, yt = False): + Text = None + Info = None + match = self.re_youtube.search(url) + if match or yt: + Temp = DataHolder() + Time = None + + if Temp(url.find("&t=")) != -1 or Temp(url.find("?t=")) != -1 or Temp(url.find("#t=")) != -1: + TimeStr = url[Temp.value + 3:].split('&')[0].split('?')[0].split('#')[0] + if TimeStr: + Time = Utils.ParseTime(TimeStr) + + Proc = await asyncio.create_subprocess_exec("youtube-dl", "--dump-json", "-g", url, + stdout = asyncio.subprocess.PIPE) + Out, _ = await Proc.communicate() + + parts = Out.split(b'\n') + parts.pop() # trailing new line + + Info = parts.pop() + url = parts.pop() + + url = url.strip().decode("ascii") + Info = self.json.loads(Info) + + if Info["extractor_key"] == "Youtube": + self.Torchlight().SayChat("\x07E52D27[YouTube]\x01 {0} | {1} | {2}/5.00 | {3:,}".format( + Info["title"], str(self.datetime.timedelta(seconds = Info["duration"])), round(Info["average_rating"], 2), int(Info["view_count"]))) + else: + match = None + + if Time: + url += "#t={0}".format(Time) + + else: + try: + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get(url), 5) + if Response: + ContentType = Response.headers.get("Content-Type") + ContentLength = Response.headers.get("Content-Length") + Content = await asyncio.wait_for(Response.content.read(65536), 5) + + if not ContentLength: + ContentLength = -1 + + if ContentType.startswith("text"): + if ContentType.startswith("text/plain"): + Text = Content.decode("utf-8", errors = "ignore") + else: + Soup = self.BeautifulSoup(Content.decode("utf-8", errors = "ignore"), "lxml") + if Soup.title: + self.Torchlight().SayChat("[URL] {0}".format(Soup.title.string)) + elif ContentType.startswith("image"): + fp = self.io.BytesIO(Content) + im = self.Image.open(fp) + self.Torchlight().SayChat("[IMAGE] {0} | Width: {1} | Height: {2} | Size: {3}".format(im.format, im.size[0], im.size[1], Utils.HumanSize(ContentLength))) + fp.close() + else: + Filetype = self.magic.from_buffer(bytes(Content)) + self.Torchlight().SayChat("[FILE] {0} | Size: {1}".format(Filetype, Utils.HumanSize(ContentLength))) + + Response.close() + except Exception as e: + self.Torchlight().SayChat("Error: {0}".format(str(e))) + self.Logger.error(traceback.format_exc()) + + self.Torchlight().LastUrl = url + return url, Text + + async def _rfunc(self, line, match, player): + Url = match.groups()[0] + if not Url.startswith("http") and not Url.startswith("ftp"): + Url = "http://" + Url + + if line.startswith("!yt "): + return + #URL, _ = await self.URLInfo(Url, True) + #return "!yt " + URL + + if line.startswith("!dec "): + _, text = await self.URLInfo(Url, False) + if text: + return "!dec " + text + + asyncio.ensure_future(self.URLInfo(Url)) + return -1 + + +def FormatAccess(Torchlight, player): + Answer = "#{0} \"{1}\"({2}) is ".format(player.UserID, player.Name, player.UniqueID) + Level = str(0) + if player.Access: + Level = str(player.Access["level"]) + Answer += "level {0!s} as {1}.".format(Level, player.Access["name"]) + else: + Answer += "not authenticated." + + if Level in Torchlight().Config["AudioLimits"]: + Uses = Torchlight().Config["AudioLimits"][Level]["Uses"] + TotalTime = Torchlight().Config["AudioLimits"][Level]["TotalTime"] + + if Uses >= 0: + Answer += " Uses: {0}/{1}".format(player.Storage["Audio"]["Uses"], Uses) + if TotalTime >= 0: + Answer += " Time: {0}/{1}".format(round(player.Storage["Audio"]["TimeUsed"], 2), round(TotalTime, 2)) + + return Answer + +class Access(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!access"] + self.Level = 0 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_chat_cooldown(player): + return -1 + + Count = 0 + if message[0] == "!access": + if message[1]: + return -1 + + self.Torchlight().SayChat(FormatAccess(self.Torchlight, player), player) + + return 0 + +class Who(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!who", "!whois"] + self.Level = 1 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + Count = 0 + if message[0] == "!who": + for Player in self.Torchlight().Players: + if Player.Name.lower().find(message[1].lower()) != -1: + self.Torchlight().SayChat(FormatAccess(self.Torchlight, Player)) + + Count += 1 + if Count >= 3: + break + + elif message[0] == "!whois": + for UniqueID, Access in self.Torchlight().Access: + if Access["name"].lower().find(message[1].lower()) != -1: + Player = self.Torchlight().Players.FindUniqueID(UniqueID) + if Player: + self.Torchlight().SayChat(FormatAccess(self.Torchlight, Player)) + else: + self.Torchlight().SayChat("#? \"{0}\"({1}) is level {2!s} is currently offline.".format(Access["name"], UniqueID, Access["level"])) + + Count += 1 + if Count >= 3: + break + return 0 + + +class WolframAlpha(BaseCommand): + import urllib.parse + import aiohttp + import xml.etree.ElementTree as etree + import re + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!cc"] + self.Level = 10 + + def Clean(self, Text): + return self.re.sub("[ ]{2,}", " ", Text.replace(' | ', ': ').replace('\n', ' | ').replace('~~', ' ≈ ')).strip() + + async def Calculate(self, Params, player): + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get("http://api.wolframalpha.com/v2/query", params=Params), 10) + if not Response: + return 1 + + Data = await asyncio.wait_for(Response.text(), 5) + if not Data: + return 2 + + Root = self.etree.fromstring(Data) + + + # Find all pods with plaintext answers + # Filter out None -answers, strip strings and filter out the empty ones + Pods = list(filter(None, [p.text.strip() for p in Root.findall('.//subpod/plaintext') if p is not None and p.text is not None])) + + # no answer pods found, check if there are didyoumeans-elements + if not Pods: + Didyoumeans = Root.find("didyoumeans") + # no support for future stuff yet, TODO? + if not Didyoumeans: + # If there's no pods, the question clearly wasn't understood + self.Torchlight().SayChat("Sorry, couldn't understand the question.", player) + return 3 + + Options = [] + for Didyoumean in Didyoumeans: + Options.append("\"{0}\"".format(Didyoumean.text)) + Line = " or ".join(Options) + Line = "Did you mean {0}?".format(Line) + self.Torchlight().SayChat(Line, player) + return 0 + + # If there's only one pod with text, it's probably the answer + # example: "integral x²" + if len(Pods) == 1: + Answer = self.Clean(Pods[0]) + self.Torchlight().SayChat(Answer, player) + return 0 + + # If there's multiple pods, first is the question interpretation + Question = self.Clean(Pods[0].replace(' | ', ' ').replace('\n', ' ')) + # and second is the best answer + Answer = self.Clean(Pods[1]) + self.Torchlight().SayChat("{0} = {1}".format(Question, Answer), player) + return 0 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_chat_cooldown(player): + return -1 + + if self.check_disabled(player): + return -1 + + Params = dict({"input": message[1], "appid": self.Torchlight().Config["WolframAPIKey"]}) + Ret = await self.Calculate(Params, player) + return Ret + + +class UrbanDictionary(BaseCommand): + import aiohttp + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!define", "!ud"] + self.Level = 10 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_chat_cooldown(player): + return -1 + + if self.check_disabled(player): + return -1 + + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get("https://api.urbandictionary.com/v0/define?term={0}".format(message[1])), 5) + if not Response: + return 1 + + Data = await asyncio.wait_for(Response.json(), 5) + if not Data: + return 3 + + if not 'list' in Data or not Data["list"]: + self.Torchlight().SayChat("[UB] No definition found for: {}".format(message[1]), player) + return 4 + + def print_item(item): + self.Torchlight().SayChat("[UD] {word} ({thumbs_up}/{thumbs_down}): {definition}\n{example}".format(**item), player) + + print_item(Data["list"][0]) + + +class OpenWeather(BaseCommand): + import aiohttp + import geoip2.database + def __init__(self, torchlight): + super().__init__(torchlight) + self.GeoIP = self.geoip2.database.Reader("/usr/share/GeoIP/GeoLite2-City.mmdb") + self.Triggers = ["!w", "!vv"] + self.Level = 10 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_chat_cooldown(player): + return -1 + + if self.check_disabled(player): + return -1 + + if not message[1]: + # Use GeoIP location + info = self.GeoIP.city(player.Address.split(":")[0]) + Search = "lat={}&lon={}".format(info.location.latitude, info.location.longitude) + else: + Search = "q={}".format(message[1]) + + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get("https://api.openweathermap.org/data/2.5/weather?APPID={0}&units=metric&{1}".format( + self.Torchlight().Config["OpenWeatherAPIKey"], Search)), 5) + if not Response: + return 2 + + Data = await asyncio.wait_for(Response.json(), 5) + if not Data: + return 3 + + if Data["cod"] != 200: + self.Torchlight().SayPrivate(player, "[OW] {0}".format(Data["message"])) + return 5 + + degToCardinal = lambda d: ["N", "NE", "E", "SE", "S", "SW", "W", "NW"][int(((d + 22.5)/45.0) % 8)] + if "deg" in Data["wind"]: + windDir = degToCardinal(Data["wind"]["deg"]) + else: + windDir = "?" + + timezone = "{}{}".format('+' if Data["timezone"] > 0 else '', int(Data["timezone"] / 3600)) + if Data["timezone"] % 3600 != 0: + timezone += ":{}".format((Data["timezone"] % 3600) / 60) + + self.Torchlight().SayChat("[{}, {}](UTC{}) {}°C ({}/{}) {}: {} | Wind {} {}kph | Clouds: {}%% | Humidity: {}%%".format(Data["name"], Data["sys"]["country"], timezone, + Data["main"]["temp"], Data["main"]["temp_min"], Data["main"]["temp_max"], Data["weather"][0]["main"], Data["weather"][0]["description"], + windDir, Data["wind"]["speed"], Data["clouds"]["all"], Data["main"]["humidity"]), player) + + return 0 + +''' +class WUnderground(BaseCommand): + import aiohttp + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!w"] + self.Level = 0 + + async def _func(self, message, player): + if not message[1]: + # Use IP address + Search = "autoip" + Additional = "?geo_ip={0}".format(player.Address.split(":")[0]) + else: + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get("http://autocomplete.wunderground.com/aq?format=JSON&query={0}".format(message[1])), 5) + if not Response: + return 2 + + Data = await asyncio.wait_for(Response.json(), 5) + if not Data: + return 3 + + if not Data["RESULTS"]: + self.Torchlight().SayPrivate(player, "[WU] No cities match your search query.") + return 4 + + Search = Data["RESULTS"][0]["name"] + Additional = "" + + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get("http://api.wunderground.com/api/{0}/conditions/q/{1}.json{2}".format( + self.Torchlight().Config["WundergroundAPIKey"], Search, Additional)), 5) + if not Response: + return 2 + + Data = await asyncio.wait_for(Response.json(), 5) + if not Data: + return 3 + + if "error" in Data["response"]: + self.Torchlight().SayPrivate(player, "[WU] {0}.".format(Data["response"]["error"]["description"])) + return 5 + + if not "current_observation" in Data: + Choices = str() + NumResults = len(Data["response"]["results"]) + for i, Result in enumerate(Data["response"]["results"]): + Choices += "{0}, {1}".format(Result["city"], + Result["state"] if Result["state"] else Result ["country_iso3166"]) + + if i < NumResults - 1: + Choices += " | " + + self.Torchlight().SayPrivate(player, "[WU] Did you mean: {0}".format(Choices)) + return 6 + + Observation = Data["current_observation"] + + self.Torchlight().SayChat("[{0}, {1}] {2}°C ({3}F) {4} | Wind {5} {6}kph ({7}mph) | Humidity: {8}".format(Observation["display_location"]["city"], + Observation["display_location"]["state"] if Observation["display_location"]["state"] else Observation["display_location"]["country_iso3166"], + Observation["temp_c"], Observation["temp_f"], Observation["weather"], + Observation["wind_dir"], Observation["wind_kph"], Observation["wind_mph"], + Observation["relative_humidity"])) + + return 0 +''' + +class VoteDisable(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!votedisable", "!disablevote"] + self.Level = 0 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.Torchlight().Disabled: + self.Torchlight().SayPrivate(player, "Torchlight is already disabled for the duration of this map.") + return + + self.Torchlight().DisableVotes.add(player.UniqueID) + + have = len(self.Torchlight().DisableVotes) + needed = len(self.Torchlight().Players) // 5 + if have >= needed: + self.Torchlight().SayChat("Torchlight has been disabled for the duration of this map.") + self.Torchlight().Disabled = 6 + else: + self.Torchlight().SayPrivate(player, "Torchlight needs {0} more disable votes to be disabled.".format(needed - have)) + + +class VoiceCommands(BaseCommand): + import json + import random + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!random", "!search"] + self.Level = 0 + + def LoadTriggers(self): + try: + with open("triggers.json", "r") as fp: + Triggers = self.json.load(fp) + except ValueError as e: + self.Logger.error(sys._getframe().f_code.co_name + ' ' + str(e)) + self.Torchlight().SayChat(str(e)) + + self.VoiceTriggers = dict() + for Line in Triggers: + for Trigger in Line["names"]: + self.VoiceTriggers[Trigger] = Line["sound"] + + def _setup(self): + self.Logger.debug(sys._getframe().f_code.co_name) + self.LoadTriggers() + for Trigger in self.VoiceTriggers.keys(): + self.Triggers.append(Trigger) + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_disabled(player): + return -1 + + Level = 0 + if player.Access: + Level = player.Access["level"] + + message[0] = message[0].lower() + message[1] = message[1].lower() + if message[0][0] != '!' and Level < 2: + return 1 + + if message[0] == "!search": + res = [] + for key in self.VoiceTriggers.keys(): + if message[1] in key.lower(): + res.append(key) + self.Torchlight().SayPrivate(player, "{} results: {}".format(len(res), ", ".join(res))) + return 0 + elif Level < 2: + return 0 + + if message[0] == "!random": + Trigger = self.random.choice(list(self.VoiceTriggers.values())) + if isinstance(Trigger, list): + Sound = self.random.choice(Trigger) + else: + Sound = Trigger + else: + Sounds = self.VoiceTriggers[message[0]] + + try: + Num = int(message[1]) + except ValueError: + Num = None + + if isinstance(Sounds, list): + if Num and Num > 0 and Num <= len(Sounds): + Sound = Sounds[Num - 1] + + elif message[1]: + searching = message[1].startswith('?') + search = message[1][1:] if searching else message[1] + Sound = None + names = [] + matches = [] + for sound in Sounds: + name = os.path.splitext(os.path.basename(sound))[0] + names.append(name) + + if search and search in name.lower(): + matches.append((name, sound)) + + if matches: + matches.sort(key=lambda t: len(t[0])) + mlist = [t[0] for t in matches] + if searching: + self.Torchlight().SayPrivate(player, "{} results: {}".format(len(mlist), ", ".join(mlist))) + return 0 + + Sound = matches[0][1] + if len(matches) > 1: + self.Torchlight().SayPrivate(player, "Multiple matches: {}".format(", ".join(mlist))) + + if not Sound and not Num: + if not searching: + self.Torchlight().SayPrivate(player, "Couldn't find {} in list of sounds.".format(message[1])) + self.Torchlight().SayPrivate(player, ", ".join(names)) + return 1 + + elif Num: + self.Torchlight().SayPrivate(player, "Number {} is out of bounds, max {}.".format(Num, len(Sounds))) + return 1 + + else: + Sound = self.random.choice(Sounds) + else: + Sound = Sounds + + if not Sound: + return 1 + + Path = os.path.abspath(os.path.join("sounds", Sound)) + AudioClip = self.Torchlight().AudioManager.AudioClip(player, "file://" + Path) + if not AudioClip: + return 1 + + return AudioClip.Play() + + +class YouTube(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!yt"] + self.Level = 3 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_disabled(player): + return -1 + + if self.Torchlight().LastUrl: + message[1] = message[1].replace("!last", self.Torchlight().LastUrl) + + Temp = DataHolder() + Time = None + + if Temp(message[1].find("&t=")) != -1 or Temp(message[1].find("?t=")) != -1 or Temp(message[1].find("#t=")) != -1: + TimeStr = message[1][Temp.value + 3:].split('&')[0].split('?')[0].split('#')[0] + if TimeStr: + Time = Utils.ParseTime(TimeStr) + + AudioClip = self.Torchlight().AudioManager.AudioClip(player, message[1]) + if not AudioClip: + return 1 + + return AudioClip.Play(Time) + +class YouTubeSearch(BaseCommand): + import json + import datetime + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!yts"] + self.Level = 3 + + async def _func(self, message, player): + return -1 + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_disabled(player): + return -1 + + Temp = DataHolder() + Time = None + + if Temp(message[1].find("&t=")) != -1 or Temp(message[1].find("?t=")) != -1 or Temp(message[1].find("#t=")) != -1: + TimeStr = message[1][Temp.value + 3:].split('&')[0].split('?')[0].split('#')[0] + if TimeStr: + Time = Utils.ParseTime(TimeStr) + message[1] = message[1][:Temp.value] + + Proc = await asyncio.create_subprocess_exec("youtube-dl", "--dump-json", "-xg", "ytsearch:" + message[1], + stdout = asyncio.subprocess.PIPE) + Out, _ = await Proc.communicate() + + print('out value: ', Out) + url, Info = Out.split(b'\n', maxsplit = 1) + url = url.strip().decode("ascii") + Info = self.json.loads(Info) + + if Info["extractor_key"] == "Youtube": + self.Torchlight().SayChat("\x07E52D27[YouTube]\x01 {0} | {1} | {2}/5.00 | {3:,}".format( + Info["title"], str(self.datetime.timedelta(seconds = Info["duration"])), round(Info["average_rating"] or 0, 2), int(Info["view_count"]))) + AudioClip = self.Torchlight().AudioManager.AudioClip(player, url) + if not AudioClip: + return 1 + self.Torchlight().LastUrl = url + return AudioClip.Play(Time) + + +class Say(BaseCommand): + import gtts + import tempfile + VALID_LANGUAGES = [lang for lang in gtts.lang.tts_langs().keys()] + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = [("!say", 4)] + self.Level = 2 + + async def Say(self, player, language, message): + GTTS = self.gtts.gTTS(text = message, lang = language) + + TempFile = self.tempfile.NamedTemporaryFile(delete = False) + GTTS.write_to_fp(TempFile) + TempFile.close() + + AudioClip = self.Torchlight().AudioManager.AudioClip(player, "file://" + TempFile.name) + if not AudioClip: + os.unlink(TempFile.name) + return 1 + + if AudioClip.Play(): + AudioClip.AudioPlayer.AddCallback("Stop", lambda: os.unlink(TempFile.name)) + return 0 + else: + os.unlink(TempFile.name) + return 1 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_disabled(player): + return -1 + + if not message[1]: + return 1 + + Language = "en" + if len(message[0]) > 4: + Language = message[0][4:] + + if not Language in self.VALID_LANGUAGES: + return 1 + + asyncio.ensure_future(self.Say(player, Language, message[1])) + return 0 + +''' +class DECTalk(BaseCommand): + import tempfile + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!dec"] + self.Level = 0 + + async def Say(self, player, message): + message = "[:phoneme on]" + message + TempFile = self.tempfile.NamedTemporaryFile(delete = False) + TempFile.close() + + Proc = await asyncio.create_subprocess_exec("wine", "say.exe", "-w", TempFile.name, + cwd = "dectalk", stdin = asyncio.subprocess.PIPE) + await Proc.communicate(message.encode('utf-8', errors='ignore')) + + AudioClip = self.Torchlight().AudioManager.AudioClip(player, "file://" + TempFile.name) + if not AudioClip: + os.unlink(TempFile.name) + return 1 + + if AudioClip.Play(None, "-af", "volume=10dB"): + AudioClip.AudioPlayer.AddCallback("Stop", lambda: os.unlink(TempFile.name)) + return 0 + else: + os.unlink(TempFile.name) + return 1 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_disabled(player): + return -1 + + if not message[1]: + return 1 + + asyncio.ensure_future(self.Say(player, message[1])) + return 0 +''' + +class Stop(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!pls"] + self.Level = 0 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + self.Torchlight().AudioManager.Stop(player, message[1]) + return True + + +class EnableDisable(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!enable", "!disable"] + self.Level = 3 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if message[0] == "!enable": + if self.Torchlight().Disabled: + if self.Torchlight().Disabled > player.Access["level"]: + self.Torchlight().SayPrivate(player, "You don't have access to enable torchlight, since it was disabled by a higher level user.") + return 1 + self.Torchlight().SayChat("Torchlight has been enabled for the duration of this map - Type !disable to disable it again.") + + self.Torchlight().Disabled = False + + elif message[0] == "!disable": + if self.Torchlight().Disabled > player.Access["level"]: + self.Torchlight().SayPrivate(player, "You don't have access to disable torchlight, since it was already disabled by a higher level user.") + return 1 + self.Torchlight().SayChat("Torchlight has been disabled for the duration of this map - Type !enable to enable it again.") + self.Torchlight().Disabled = player.Access["level"] + + +class AdminAccess(BaseCommand): + from collections import OrderedDict + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!access"] + self.Level = 4 + + def ReloadValidUsers(self): + self.Torchlight().Access.Load() + for Player in self.Torchlight().Players: + Access = self.Torchlight().Access[Player.UniqueID] + Player.Access = Access + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + if not message[1]: + return -1 + + if message[1].lower() == "reload": + self.ReloadValidUsers() + self.Torchlight().SayChat("Loaded access list with {0} users".format(len(self.Torchlight().Access))) + + elif message[1].lower() == "save": + self.Torchlight().Access.Save() + self.Torchlight().SayChat("Saved access list with {0} users".format(len(self.Torchlight().Access))) + + # Modify access + else: + Player = None + Buf = message[1] + Temp = Buf.find(" as ") + if Temp != -1: + try: + Regname, Level = Buf[Temp + 4:].rsplit(' ', 1) + except ValueError as e: + self.Torchlight().SayChat(str(e)) + return 1 + + Regname = Regname.strip() + Level = Level.strip() + Buf = Buf[:Temp].strip() + else: + try: + Buf, Level = Buf.rsplit(' ', 1) + except ValueError as e: + self.Torchlight().SayChat(str(e)) + return 2 + + Buf = Buf.strip() + Level = Level.strip() + + # Find user by User ID + if Buf[0] == '#' and Buf[1:].isnumeric(): + Player = self.Torchlight().Players.FindUserID(int(Buf[1:])) + # Search user by name + else: + for Player_ in self.Torchlight().Players: + if Player_.Name.lower().find(Buf.lower()) != -1: + Player = Player_ + break + + if not Player: + self.Torchlight().SayChat("Couldn't find user: {0}".format(Buf)) + return 3 + + if Level.isnumeric() or (Level.startswith('-') and Level[1:].isdigit()): + Level = int(Level) + + if Level >= player.Access["level"] and player.Access["level"] < 10: + self.Torchlight().SayChat("Trying to assign level {0}, which is higher or equal than your level ({1})".format(Level, player.Access["level"])) + return 4 + + if Player.Access: + if Player.Access["level"] >= player.Access["level"] and player.Access["level"] < 10: + self.Torchlight().SayChat("Trying to modify level {0}, which is higher or equal than your level ({1})".format(Player.Access["level"], player.Access["level"])) + return 5 + + if "Regname" in locals(): + self.Torchlight().SayChat("Changed \"{0}\"({1}) as {2} level/name from {3} to {4} as {5}".format( + Player.Name, Player.UniqueID, Player.Access["name"], Player.Access["level"], Level, Regname)) + Player.Access["name"] = Regname + else: + self.Torchlight().SayChat("Changed \"{0}\"({1}) as {2} level from {3} to {4}".format( + Player.Name, Player.UniqueID, Player.Access["name"], Player.Access["level"], Level)) + + Player.Access["level"] = Level + self.Torchlight().Access[Player.UniqueID] = Player.Access + else: + if not "Regname" in locals(): + Regname = Player.Name + + self.Torchlight().Access[Player.UniqueID] = self.OrderedDict([("name", Regname), ("level", Level)]) + Player.Access = self.Torchlight().Access[Player.UniqueID] + self.Torchlight().SayChat("Added \"{0}\"({1}) to access list as {2} with level {3}".format(Player.Name, Player.UniqueID, Regname, Level)) + else: + if Level == "revoke" and Player.Access: + if Player.Access["level"] >= player.Access["level"] and player.Access["level"] < 10: + self.Torchlight().SayChat("Trying to revoke level {0}, which is higher or equal than your level ({1})".format(Player.Access["level"], player.Access["level"])) + return 6 + + self.Torchlight().SayChat("Removed \"{0}\"({1}) from access list (was {2} with level {3})".format( + Player.Name, Player.UniqueID, Player.Access["name"], Player.Access["level"])) + del self.Torchlight().Access[Player.UniqueID] + Player.Access = None + return 0 + +class Reload(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!reload"] + self.Level = 4 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + self.Torchlight().Reload() + return 0 + +""" +class Exec(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!exec"] + self.Level = 100 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + try: + Response = eval(message[1]) + except Exception as e: + self.Torchlight().SayChat("Error: {0}".format(str(e))) + return 1 + self.Torchlight().SayChat(str(Response)) + return 0 +""" diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/Config.py b/torchlight_changes_unloze/torchlight3/Torchlight/Config.py new file mode 100755 index 00000000..9686bb38 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/Config.py @@ -0,0 +1,29 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import logging +import json +import sys + +class Config(): + def __init__(self): + self.Logger = logging.getLogger(__class__.__name__) + self.Config = dict() + if len(sys.argv) >= 2: + self.ConfigPath = sys.argv[1] + else: + self.ConfigPath = "config.json" + self.Load() + + def Load(self): + try: + with open(self.ConfigPath, "r") as fp: + self.Config = json.load(fp) + except ValueError as e: + self.Logger.error(sys._getframe().f_code.co_name + ' ' + str(e)) + return 1 + return 0 + + def __getitem__(self, key): + if key in self.Config: + return self.Config[key] + return None diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/Constants.py b/torchlight_changes_unloze/torchlight3/Torchlight/Constants.py new file mode 100755 index 00000000..95477aa9 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/Constants.py @@ -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) diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/FFmpegAudioPlayer.py b/torchlight_changes_unloze/torchlight3/Torchlight/FFmpegAudioPlayer.py new file mode 100755 index 00000000..8599f170 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/FFmpegAudioPlayer.py @@ -0,0 +1,180 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import logging +import traceback +import asyncio +import datetime +import time +import socket +import struct +import sys + +SAMPLEBYTES = 2 + +class FFmpegAudioPlayerFactory(): + VALID_CALLBACKS = ["Play", "Stop", "Update"] + + def __init__(self, master): + self.Logger = logging.getLogger(__class__.__name__) + self.Master = master + self.Torchlight = self.Master.Torchlight + + def __del__(self): + self.Master.Logger.info("~FFmpegAudioPlayerFactory()") + self.Quit() + + def NewPlayer(self): + self.Logger.debug(sys._getframe().f_code.co_name) + Player = FFmpegAudioPlayer(self) + return Player + + def Quit(self): + self.Master.Logger.info("FFmpegAudioPlayerFactory->Quit()") + + +class FFmpegAudioPlayer(): + def __init__(self, master): + self.Master = master + self.Torchlight = self.Master.Torchlight + self.Playing = False + + self.Host = ( + self.Torchlight().Config["VoiceServer"]["Host"], + self.Torchlight().Config["VoiceServer"]["Port"] + ) + self.SampleRate = float(self.Torchlight().Config["VoiceServer"]["SampleRate"]) + + self.StartedPlaying = None + self.StoppedPlaying = None + self.Seconds = 0.0 + + self.Writer = None + self.Process = None + + self.Callbacks = [] + + def __del__(self): + self.Master.Logger.debug("~FFmpegAudioPlayer()") + self.Stop() + + def PlayURI(self, uri, position, *args): + if position: + PosStr = str(datetime.timedelta(seconds = position)) + Command = ["/usr/bin/ffmpeg", "-ss", PosStr, "-i", uri, "-acodec", "pcm_s16le", "-ac", "1", "-ar", str(int(self.SampleRate)), "-f", "s16le", "-vn", *args, "-"] + else: + Command = ["/usr/bin/ffmpeg", "-i", uri, "-acodec", "pcm_s16le", "-ac", "1", "-ar", str(int(self.SampleRate)), "-f", "s16le", "-vn", *args, "-"] + + print(Command) + + self.Playing = True + asyncio.ensure_future(self._stream_subprocess(Command)) + return True + + def Stop(self, force = True): + if not self.Playing: + return False + + if self.Process: + try: + self.Process.terminate() + self.Process.kill() + self.Process = None + except ProcessLookupError: + pass + + if self.Writer: + if force: + Socket = self.Writer.transport.get_extra_info("socket") + if Socket: + Socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, + struct.pack("ii", 1, 0)) + + self.Writer.transport.abort() + + self.Writer.close() + + self.Playing = False + + self.Callback("Stop") + del self.Callbacks + + return True + + def AddCallback(self, cbtype, cbfunc): + if not cbtype in FFmpegAudioPlayerFactory.VALID_CALLBACKS: + return False + + self.Callbacks.append((cbtype, cbfunc)) + return True + + def Callback(self, cbtype, *args, **kwargs): + for Callback in self.Callbacks: + if Callback[0] == cbtype: + try: + Callback[1](*args, **kwargs) + except Exception as e: + self.Master.Logger.error(traceback.format_exc()) + + async def _updater(self): + LastSecondsElapsed = 0.0 + + while self.Playing: + SecondsElapsed = time.time() - self.StartedPlaying + + if SecondsElapsed > self.Seconds: + SecondsElapsed = self.Seconds + + self.Callback("Update", LastSecondsElapsed, SecondsElapsed) + + if SecondsElapsed >= self.Seconds: + if not self.StoppedPlaying: + print("BUFFER UNDERRUN!") + self.Stop(False) + return + + LastSecondsElapsed = SecondsElapsed + + await asyncio.sleep(0.1) + + async def _read_stream(self, stream, writer): + Started = False + + while stream and self.Playing: + Data = await stream.read(65536) + + if Data: + writer.write(Data) + await writer.drain() + + Bytes = len(Data) + Samples = Bytes / SAMPLEBYTES + Seconds = Samples / self.SampleRate + + self.Seconds += Seconds + + if not Started: + Started = True + self.Callback("Play") + self.StartedPlaying = time.time() + asyncio.ensure_future(self._updater()) + else: + self.Process = None + break + + self.StoppedPlaying = time.time() + + async def _stream_subprocess(self, cmd): + if not self.Playing: + return + + _, self.Writer = await asyncio.open_connection(self.Host[0], self.Host[1]) + + Process = await asyncio.create_subprocess_exec(*cmd, + stdout = asyncio.subprocess.PIPE, stderr = asyncio.subprocess.DEVNULL) + self.Process = Process + + await self._read_stream(Process.stdout, self.Writer) + await Process.wait() + + if self.Seconds == 0.0: + self.Stop() diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/GameEvents.py b/torchlight_changes_unloze/torchlight3/Torchlight/GameEvents.py new file mode 100755 index 00000000..d7a8828e --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/GameEvents.py @@ -0,0 +1,149 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import asyncio +import logging +import traceback + +class GameEvents(): + def __init__(self, master): + self.Logger = logging.getLogger(__class__.__name__) + self.Torchlight = master + + self.Callbacks = {} + + def __del__(self): + if not len(self.Callbacks) or not self.Torchlight(): + return + + Obj = { + "method": "unsubscribe", + "module": "gameevents", + "events": self.Callbacks.keys() + } + + asyncio.ensure_future(self.Torchlight().Send(Obj)) + + async def _Register(self, events): + if type(events) is not list: + events = [ events ] + + Obj = { + "method": "subscribe", + "module": "gameevents", + "events": events + } + + Res = await self.Torchlight().Send(Obj) + + Ret = [] + for i, ret in enumerate(Res["events"]): + if ret >= 0: + Ret.append(True) + if not events[i] in self.Callbacks: + self.Callbacks[events[i]] = set() + else: + Ret.append(False) + + if len(Ret) == 1: + Ret = Ret[0] + return Ret + + async def _Unregister(self, events): + if type(events) is not list: + events = [ events ] + + Obj = { + "method": "unsubscribe", + "module": "gameevents", + "events": events + } + + Res = await self.Torchlight().Send(Obj) + + Ret = [] + for i, ret in enumerate(Res["events"]): + if ret >= 0: + Ret.append(True) + if events[i] in self.Callbacks: + del self.Callbacks[events[i]] + else: + Ret.append(False) + + if len(Ret) == 1: + Ret = Ret[0] + return Ret + + def HookEx(self, event, callback): + asyncio.ensure_future(self.Hook(event, callback)) + + def UnhookEx(self, event, callback): + asyncio.ensure_future(self.Unhook(event, callback)) + + def ReplayEx(self, events): + asyncio.ensure_future(self.Replay(events)) + + async def Hook(self, event, callback): + if not event in self.Callbacks: + if not await self._Register(event): + return False + + self.Callbacks[event].add(callback) + return True + + async def Unhook(self, event, callback): + if not event in self.Callbacks: + return True + + if not callback in self.Callbacks[event]: + return True + + self.Callbacks[event].discard(callback) + + if len(a) == 0: + return await self._Unregister(event) + + return True + + async def Replay(self, events): + if type(events) is not list: + events = [ events ] + + for event in events[:]: + if not event in self.Callbacks: + events.remove(event) + + Obj = { + "method": "replay", + "module": "gameevents", + "events": events + } + + Res = await self.Torchlight().Send(Obj) + + Ret = [] + for i, ret in enumerate(Res["events"]): + if ret >= 0: + Ret.append(True) + else: + Ret.append(False) + + if len(Ret) == 1: + Ret = Ret[0] + return Ret + + def OnPublish(self, obj): + Event = obj["event"] + + if not Event["name"] in self.Callbacks: + return False + + Callbacks = self.Callbacks[Event["name"]] + + for Callback in Callbacks: + try: + Callback(**Event["data"]) + except Exception as e: + self.Logger.error(traceback.format_exc()) + self.Logger.error(Event) + + return True diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/PlayerManager.py b/torchlight_changes_unloze/torchlight3/Torchlight/PlayerManager.py new file mode 100755 index 00000000..37357c5b --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/PlayerManager.py @@ -0,0 +1,201 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import asyncio +import logging +import numpy +from .Constants import * + +class PlayerManager(): + def __init__(self, master): + self.Logger = logging.getLogger(__class__.__name__) + self.Torchlight = master + + self.Players = numpy.empty(MAXPLAYERS + 1, dtype = object) + self.Storage = self.StorageManager(self) + + self.Torchlight().GameEvents.HookEx("player_connect", self.Event_PlayerConnect) + self.Torchlight().GameEvents.HookEx("player_activate", self.Event_PlayerActivate) + self.Torchlight().Forwards.HookEx("OnClientPostAdminCheck", self.OnClientPostAdminCheck) + self.Torchlight().GameEvents.HookEx("player_info", self.Event_PlayerInfo) + self.Torchlight().GameEvents.HookEx("player_disconnect", self.Event_PlayerDisconnect) + self.Torchlight().GameEvents.HookEx("server_spawn", self.Event_ServerSpawn) + + def Event_PlayerConnect(self, name, index, userid, networkid, address, bot): + index += 1 + self.Logger.info("OnConnect(name={0}, index={1}, userid={2}, networkid={3}, address={4}, bot={5})" + .format(name, index, userid, networkid, address, bot)) + if self.Players[index] != None: + self.Logger.error("!!! Player already exists, overwriting !!!") + + self.Players[index] = self.Player(self, index, userid, networkid, address, name) + self.Players[index].OnConnect() + + def Event_PlayerActivate(self, userid): + self.Logger.info("Pre_OnActivate(userid={0})".format(userid)) + index = self.FindUserID(userid).Index + self.Logger.info("OnActivate(index={0}, userid={1})".format(index, userid)) + + self.Players[index].OnActivate() + + def OnClientPostAdminCheck(self, client): + self.Logger.info("OnClientPostAdminCheck(client={0})".format(client)) + + asyncio.ensure_future(self.Players[client].OnClientPostAdminCheck()) + + def Event_PlayerInfo(self, name, index, userid, networkid, bot): + index += 1 + self.Logger.info("OnInfo(name={0}, index={1}, userid={2}, networkid={3}, bot={4})" + .format(name, index, userid, networkid, bot)) + + # We've connected to the server and receive info events about the already connected players + # Emulate connect message + if not self.Players[index]: + self.Event_PlayerConnect(name, index - 1, userid, networkid, bot) + else: + self.Players[index].OnInfo(name) + + def Event_PlayerDisconnect(self, userid, reason, name, networkid, bot): + index = self.FindUserID(userid).Index + self.Logger.info("OnDisconnect(index={0}, userid={1}, reason={2}, name={3}, networkid={4}, bot={5})" + .format(index, userid, reason, name, networkid, bot)) + + self.Players[index].OnDisconnect(reason) + self.Players[index] = None + + def Event_ServerSpawn(self, hostname, address, ip, port, game, mapname, maxplayers, os, dedicated, password): + self.Logger.info("ServerSpawn(mapname={0})" + .format(mapname)) + + self.Storage.Reset() + + for i in range(1, self.Players.size): + if self.Players[i]: + self.Players[i].OnDisconnect("mapchange") + self.Players[i].OnConnect() + + def FindUniqueID(self, uniqueid): + for Player in self.Players: + if Player and Player.UniqueID == uniqueid: + return Player + + def FindUserID(self, userid): + for Player in self.Players: + if Player and Player.UserID == userid: + return Player + + def FindName(self, name): + for Player in self.Players: + if Player and Player.Name == name: + return Player + + def __len__(self): + Count = 0 + for i in range(1, self.Players.size): + if self.Players[i]: + Count += 1 + return Count + + def __setitem__(self, key, value): + if key > 0 and key <= MAXPLAYERS: + self.Players[key] = value + + def __getitem__(self, key): + if key > 0 and key <= MAXPLAYERS: + return self.Players[key] + + def __iter__(self): + for i in range(1, self.Players.size): + if self.Players[i]: + yield self.Players[i] + + class StorageManager(): + def __init__(self, master): + self.PlayerManager = master + self.Storage = dict() + + def Reset(self): + self.Storage = dict() + + def __getitem__(self, key): + if not key in self.Storage: + self.Storage[key] = dict() + + return self.Storage[key] + + class Admin(): + def __init__(self): + self._FlagBits = 0 + + def FlagBits(self): + return self._FlagBits + + def Reservation(self): return (self._FlagBits & ADMFLAG_RESERVATION) + def Generic(self): return (self._FlagBits & ADMFLAG_GENERIC) + def Kick(self): return (self._FlagBits & ADMFLAG_KICK) + def Ban(self): return (self._FlagBits & ADMFLAG_BAN) + def Unban(self): return (self._FlagBits & ADMFLAG_UNBAN) + def Slay(self): return (self._FlagBits & ADMFLAG_SLAY) + def Changemap(self): return (self._FlagBits & ADMFLAG_CHANGEMAP) + def Convars(self): return (self._FlagBits & ADMFLAG_CONVARS) + def Config(self): return (self._FlagBits & ADMFLAG_CONFIG) + def Chat(self): return (self._FlagBits & ADMFLAG_CHAT) + def Vote(self): return (self._FlagBits & ADMFLAG_VOTE) + def Password(self): return (self._FlagBits & ADMFLAG_PASSWORD) + def RCON(self): return (self._FlagBits & ADMFLAG_RCON) + def Cheats(self): return (self._FlagBits & ADMFLAG_CHEATS) + def Root(self): return (self._FlagBits & ADMFLAG_ROOT) + def Custom1(self): return (self._FlagBits & ADMFLAG_CUSTOM1) + def Custom2(self): return (self._FlagBits & ADMFLAG_CUSTOM2) + def Custom3(self): return (self._FlagBits & ADMFLAG_CUSTOM3) + def Custom4(self): return (self._FlagBits & ADMFLAG_CUSTOM4) + def Custom5(self): return (self._FlagBits & ADMFLAG_CUSTOM5) + def Custom6(self): return (self._FlagBits & ADMFLAG_CUSTOM6) + + class Player(): + def __init__(self, master, index, userid, uniqueid, address, name): + self.PlayerManager = master + self.Torchlight = self.PlayerManager.Torchlight + self.Index = index + self.UserID = userid + self.UniqueID = uniqueid + self.Address = address + self.Name = name + self.Access = None + self.Admin = self.PlayerManager.Admin() + self.Storage = None + self.Active = False + self.ChatCooldown = 0 + + def OnConnect(self): + self.Storage = self.PlayerManager.Storage[self.UniqueID] + + if not "Audio" in self.Storage: + self.Storage["Audio"] = dict({"Uses": 0, "LastUse": 0.0, "LastUseLength": 0.0, "TimeUsed": 0.0}) + + self.Access = self.Torchlight().Access[self.UniqueID] + + def OnActivate(self): + self.Active = True + + async def OnClientPostAdminCheck(self): + self.Admin._FlagBits = (await self.Torchlight().API.GetUserFlagBits(self.Index))["result"] + self.PlayerManager.Logger.info("#{0} \"{1}\"({2}) FlagBits: {3}".format(self.UserID, self.Name, self.UniqueID, self.Admin._FlagBits)) + if not self.Access: + if self.Admin.RCON(): + self.Access = dict({"level": 6, "name": "SAdmin"}) + elif self.Admin.Generic(): + self.Access = dict({"level": 3, "name": "Admin"}) + elif self.Admin.Custom1(): + self.Access = dict({"level": 1, "name": "VIP"}) + + if self.PlayerManager.Torchlight().Config["DefaultLevel"]: + if self.Access and self.Access["level"] < self.PlayerManager.Torchlight().Config["DefaultLevel"]: + self.Access = dict({"level": self.PlayerManager.Torchlight().Config["DefaultLevel"], "name": "Default"}) + + def OnInfo(self, name): + self.Name = name + + def OnDisconnect(self, message): + self.Active = False + self.Storage = None + self.Torchlight().AudioManager.OnDisconnect(self) diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/SourceModAPI.py b/torchlight_changes_unloze/torchlight3/Torchlight/SourceModAPI.py new file mode 100755 index 00000000..64f83792 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/SourceModAPI.py @@ -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 diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/SourceRCONServer.py b/torchlight_changes_unloze/torchlight3/Torchlight/SourceRCONServer.py new file mode 100755 index 00000000..882c7687 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/SourceRCONServer.py @@ -0,0 +1,106 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import logging +import asyncio +import sys +import socket +import struct +import time +import traceback +from importlib import reload +from .PlayerManager import PlayerManager + +class SourceRCONServer(): + class SourceRCONClient(): + def __init__(self, Server, Socket, Name): + self.Loop = Server.Loop + self.Server = Server + self._sock = Socket + self.Name = Name + self.Authenticated = False + asyncio.Task(self._peer_handler()) + + def send(self, data): + return self.Loop.sock_sendall(self._sock, data) + + @asyncio.coroutine + def _peer_handler(self): + try: + yield from self._peer_loop() + except IOError: + pass + finally: + self.Server.Remove(self) + + @asyncio.coroutine + def _peer_loop(self): + while True: + Data = yield from self.Loop.sock_recv(self._sock, 1024) + if Data == b'': + break + + while Data: + p_size = struct.unpack("= 0: + Ret.append(True) + if not events[i] in self.Callbacks: + self.Callbacks[events[i]] = set() + else: + Ret.append(False) + + if len(Ret) == 1: + Ret = Ret[0] + return Ret + + async def _Unregister(self, events): + if type(events) is not list: + events = [ events ] + + Obj = { + "method": "unsubscribe", + "module": self.Module, + "events": events + } + + Res = await self.Torchlight().Send(Obj) + + Ret = [] + for i, ret in enumerate(Res["events"]): + if ret >= 0: + Ret.append(True) + if events[i] in self.Callbacks: + del self.Callbacks[events[i]] + else: + Ret.append(False) + + if len(Ret) == 1: + Ret = Ret[0] + return Ret + + def HookEx(self, event, callback): + asyncio.ensure_future(self.Hook(event, callback)) + + def UnhookEx(self, event, callback): + asyncio.ensure_future(self.Unhook(event, callback)) + + def ReplayEx(self, events): + asyncio.ensure_future(self.Replay(events)) + + async def Hook(self, event, callback): + if not event in self.Callbacks: + if not await self._Register(event): + return False + + self.Callbacks[event].add(callback) + return True + + async def Unhook(self, event, callback): + if not event in self.Callbacks: + return True + + if not callback in self.Callbacks[event]: + return True + + self.Callbacks[event].discard(callback) + + if len(a) == 0: + return await self._Unregister(event) + + return True + + async def Replay(self, events): + if type(events) is not list: + events = [ events ] + + for event in events[:]: + if not event in self.Callbacks: + events.remove(event) + + Obj = { + "method": "replay", + "module": self.Module, + "events": events + } + + Res = await self.Torchlight().Send(Obj) + + Ret = [] + for i, ret in enumerate(Res["events"]): + if ret >= 0: + Ret.append(True) + else: + Ret.append(False) + + if len(Ret) == 1: + Ret = Ret[0] + return Ret + + def OnPublish(self, obj): + Event = obj["event"] + + if not Event["name"] in self.Callbacks: + return False + + Callbacks = self.Callbacks[Event["name"]] + + for Callback in Callbacks: + try: + Callback(**Event["data"]) + except Exception as e: + self.Logger.error(traceback.format_exc()) + self.Logger.error(Event) + + return True + + +class GameEvents(SubscribeBase): + def __init__(self, master): + super().__init__(master, "gameevents") + +class Forwards(SubscribeBase): + def __init__(self, master): + super().__init__(master, "forwards") diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/Torchlight.py b/torchlight_changes_unloze/torchlight3/Torchlight/Torchlight.py new file mode 100755 index 00000000..0096204c --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/Torchlight.py @@ -0,0 +1,149 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import logging +import asyncio +import sys +import json +import time +import weakref +import traceback +import textwrap + +from .AsyncClient import AsyncClient + +from .SourceModAPI import SourceModAPI +from .Subscribe import GameEvents, Forwards + +from .Utils import Utils +from .Config import Config +from .CommandHandler import CommandHandler +from .AccessManager import AccessManager +from .PlayerManager import PlayerManager +from .AudioManager import AudioManager + +class Torchlight(): + def __init__(self, master): + self.Logger = logging.getLogger(__class__.__name__) + self.Master = master + self.Config = self.Master.Config + self.WeakSelf = weakref.ref(self) + + self.API = SourceModAPI(self.WeakSelf) + self.GameEvents = GameEvents(self.WeakSelf) + self.Forwards = Forwards(self.WeakSelf) + + self.DisableVotes = set() + self.Disabled = 0 + self.LastUrl = None + + def InitModules(self): + self.Access = AccessManager() + self.Access.Load() + + self.Players = PlayerManager(self.WeakSelf) + + self.AudioManager = AudioManager(self.WeakSelf) + + self.CommandHandler = CommandHandler(self.WeakSelf) + self.CommandHandler.Setup() + + self.GameEvents.HookEx("server_spawn", self.Event_ServerSpawn) + self.GameEvents.HookEx("player_say", self.Event_PlayerSay) + + def SayChat(self, message, player=None): + message = "\x0700FFFA[Torchlight]: \x01{0}".format(message) + if len(message) > 976: + message = message[:973] + "..." + lines = textwrap.wrap(message, 244, break_long_words = True) + for line in lines: + asyncio.ensure_future(self.API.PrintToChatAll(line)) + + if player: + Level = 0 + if player.Access: + Level = player.Access["level"] + + if Level < self.Config["AntiSpam"]["ImmunityLevel"]: + cooldown = len(lines) * self.Config["AntiSpam"]["ChatCooldown"] + if player.ChatCooldown > self.Master.Loop.time(): + player.ChatCooldown += cooldown + else: + player.ChatCooldown = self.Master.Loop.time() + cooldown + + def SayPrivate(self, player, message): + message = "\x0700FFFA[Torchlight]: \x01{0}".format(message) + if len(message) > 976: + message = message[:973] + "..." + lines = textwrap.wrap(message, 244, break_long_words = True) + for line in lines: + asyncio.ensure_future(self.API.PrintToChat(player.Index, line)) + + def Reload(self): + self.Config.Load() + self.CommandHandler.NeedsReload = True + + async def Send(self, data): + return await self.Master.Send(data) + + def OnPublish(self, obj): + if obj["module"] == "gameevents": + self.GameEvents.OnPublish(obj) + elif obj["module"] == "forwards": + self.Forwards.OnPublish(obj) + + def Event_ServerSpawn(self, hostname, address, ip, port, game, mapname, maxplayers, os, dedicated, password): + self.DisableVotes = set() + self.Disabled = 0 + + def Event_PlayerSay(self, userid, text): + if userid == 0: + return + + Player = self.Players.FindUserID(userid) + asyncio.ensure_future(self.CommandHandler.HandleCommand(text, Player)) + + def __del__(self): + self.Logger.debug("~Torchlight()") + + +class TorchlightHandler(): + def __init__(self, loop): + self.Logger = logging.getLogger(__class__.__name__) + self.Loop = loop if loop else asyncio.get_event_loop() + self._Client = None + self.Torchlight = None + self.Config = Config() + + asyncio.ensure_future(self._Connect(), loop = self.Loop) + + async def _Connect(self): + # Connect to API + self._Client = AsyncClient(self.Loop, self.Config["SMAPIServer"]["Host"], self.Config["SMAPIServer"]["Port"], self) + await self._Client.Connect() + + self.Torchlight = Torchlight(self) + + # Pre Hook for late load + await self.Torchlight.GameEvents._Register(["player_connect", "player_activate"]) + await self.Torchlight.Forwards._Register(["OnClientPostAdminCheck"]) + + self.Torchlight.InitModules() + + # Late load + await self.Torchlight.GameEvents.Replay(["player_connect", "player_activate"]) + await self.Torchlight.Forwards.Replay(["OnClientPostAdminCheck"]) + + async def Send(self, data): + return await self._Client.Send(data) + + def OnPublish(self, obj): + self.Torchlight.OnPublish(obj) + + def OnDisconnect(self, exc): + self.Logger.info("OnDisconnect({0})".format(exc)) + self.Torchlight = None + + asyncio.ensure_future(self._Connect(), loop = self.Loop) + + def __del__(self): + self.Logger.debug("~TorchlightHandler()") diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/Torchlightmg.py b/torchlight_changes_unloze/torchlight3/Torchlight/Torchlightmg.py new file mode 100755 index 00000000..f1c21372 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/Torchlightmg.py @@ -0,0 +1,150 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import logging +import asyncio +import sys +import json +import time +import weakref +import traceback +import textwrap + +from .AsyncClient import AsyncClient + +from .SourceModAPI import SourceModAPI +from .Subscribe import GameEvents, Forwards + +from .Utils import Utils +from .Config import Config +from .CommandHandlermg import CommandHandlermg +from .AccessManager import AccessManager +from .PlayerManager import PlayerManager +from .AudioManager import AudioManager + +class Torchlight(): + def __init__(self, master): + self.Logger = logging.getLogger(__class__.__name__) + self.Master = master + self.Config = self.Master.Config + self.WeakSelf = weakref.ref(self) + + self.API = SourceModAPI(self.WeakSelf) + self.GameEvents = GameEvents(self.WeakSelf) + self.Forwards = Forwards(self.WeakSelf) + + self.DisableVotes = set() + self.Disabled = 0 + self.LastUrl = None + + def InitModules(self): + self.Access = AccessManager() + self.Access.Load() + + self.Players = PlayerManager(self.WeakSelf) + + self.AudioManager = AudioManager(self.WeakSelf) + + self.CommandHandler = CommandHandlermg(self.WeakSelf) + self.CommandHandler.Setup() + + self.GameEvents.HookEx("server_spawn", self.Event_ServerSpawn) + self.GameEvents.HookEx("player_say", self.Event_PlayerSay) + + def SayChat(self, message, player=None): + message = "\x0700FFFA[Torchlight]: \x01{0}".format(message) + if len(message) > 976: + message = message[:973] + "..." + lines = textwrap.wrap(message, 244, break_long_words = True) + for line in lines: + asyncio.ensure_future(self.API.PrintToChatAll(line)) + + if player: + Level = 0 + if player.Access: + Level = player.Access["level"] + + if Level < self.Config["AntiSpam"]["ImmunityLevel"]: + cooldown = len(lines) * self.Config["AntiSpam"]["ChatCooldown"] + if player.ChatCooldown > self.Master.Loop.time(): + player.ChatCooldown += cooldown + else: + player.ChatCooldown = self.Master.Loop.time() + cooldown + + def SayPrivate(self, player, message): + message = "\x0700FFFA[Torchlight]: \x01{0}".format(message) + if len(message) > 976: + message = message[:973] + "..." + lines = textwrap.wrap(message, 244, break_long_words = True) + for line in lines: + asyncio.ensure_future(self.API.PrintToChat(player.Index, line)) + + def Reload(self): + self.Config.Load() + self.CommandHandler.NeedsReload = True + + async def Send(self, data): + return await self.Master.Send(data) + + def OnPublish(self, obj): + if obj["module"] == "gameevents": + self.GameEvents.OnPublish(obj) + elif obj["module"] == "forwards": + self.Forwards.OnPublish(obj) + + def Event_ServerSpawn(self, hostname, address, ip, port, game, mapname, maxplayers, os, dedicated, password): + self.DisableVotes = set() + self.Disabled = 0 + + def Event_PlayerSay(self, userid, text): + if userid == 0: + return + + Player = self.Players.FindUserID(userid) + asyncio.ensure_future(self.CommandHandler.HandleCommand(text, Player)) + + def __del__(self): + self.Logger.debug("~Torchlight()") + + +class TorchlightHandler(): + def __init__(self, loop): + self.Logger = logging.getLogger(__class__.__name__) + self.Loop = loop if loop else asyncio.get_event_loop() + self._Client = None + self.Torchlight = None + self.Config = Config() + + asyncio.ensure_future(self._Connect(), loop = self.Loop) + + async def _Connect(self): + # Connect to API + self._Client = AsyncClient(self.Loop, self.Config["SMAPIServer"]["Host"], self.Config["SMAPIServer"]["Port"], self) + await self._Client.Connect() + + self.Torchlight = Torchlight(self) + + # Pre Hook for late load + await self.Torchlight.GameEvents._Register(["player_connect", "player_activate"]) + await self.Torchlight.Forwards._Register(["OnClientPostAdminCheck"]) + + self.Torchlight.InitModules() + + # Late load + await self.Torchlight.GameEvents.Replay(["player_connect", "player_activate"]) + await self.Torchlight.Forwards.Replay(["OnClientPostAdminCheck"]) + + async def Send(self, data): + return await self._Client.Send(data) + + def OnPublish(self, obj): + self.Torchlight.OnPublish(obj) + + def OnDisconnect(self, exc): + self.Logger.info("OnDisconnect({0})".format(exc)) + self.Torchlight = None + + asyncio.ensure_future(self._Connect(), loop = self.Loop) + + def __del__(self): + self.Logger.debug("~TorchlightHandler()") + diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/Utils.py b/torchlight_changes_unloze/torchlight3/Torchlight/Utils.py new file mode 100755 index 00000000..f335768e --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/Utils.py @@ -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) diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__init__.py b/torchlight_changes_unloze/torchlight3/Torchlight/__init__.py new file mode 100755 index 00000000..f9664561 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/AccessManager.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/AccessManager.cpython-37.pyc new file mode 100755 index 0000000000000000000000000000000000000000..ebc20f2674e165976caabf4e4d47b39fbcc962c4 GIT binary patch literal 2112 zcmZ`)&2Jk;6rb5IufIYQLfW7rBJpWJF%s7hrG$czLJ+tTl`q3~XYANn?}nL8pvFBV zQn^HM=Mo?J7x*(c&XrTo+7TK#F9TVD4rC~+&=pyeb?8tw zWD~k7m*gwZHF--eL)V`&k98XWvSOR};#e8;b(BVZrN{NCc-};Xs@=zKqx6Z=rX6ExtlD{|dQ5i@(m^gnl{`Qs;nNZyuU$91GMf2K0BOfAhS`CbupvB`{KCP^o$@K?Eca+G zom1zxwI@9HhrxvB73rSCMuguZCB9|fv4_xL$J-y@-pe8hZZ=A$H+ni7ZT#}_6U#Lm zU(Oy4Rh)~XXrPTa$g;y$RabDe^#*A#vq3M@qbRrad%L^$J00=GS9|xZmmR6pdK5G3 zB{;sIR)^Tuax{5ho!*hIV^WGnaUcO{VrBve##bFsvT~ggGFeQ=57@Gpj@-xh;G!}` z4IoXC_tHOiPbpP`JS@lxK-L|QRUOvIcEV*vhUci2Il8L*=Ud0{3AeAc3TlFG#`WBAV%OqAv6p0_0Oy^1# zQL_FZS0kferPWmk>&fHM(Pi-T8hxIR9zh~|Q?w{V&*66$M3BsG&{ZTf?aL{K{FHhU z3K!laDlPMd7kM*(YefiH2yvG#CN~%#zjmFfON+Qzg#l?A#hrIBnIIE}>@d))q$dbh zrz{R2;4dewegoF?2)1>`N5UD0s^#>{FlB^2BlB`YPIPv_y3Rs4U1NR^a?yh}LB_O^(S zncd=qC~87Q>q|@#4SRi$;G0N=1L2JKv7$n<3xXiZyykb%hb5pV9VW2O)baP zgcxP=IH8|HLx>-bqon*rlx0G_NmEJccSyWP;(Zcyh5nEPU1*z^KZ{r;i4ZR^0VL(P zroNyl?9@jMRba?H-tfX8tcGsr(J0niz9#a@3arOjlAsk2vecCQl#Y0DdO4%BZMaps Ou}v05Hw}5n8_s`2TFS)$ literal 0 HcmV?d00001 diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/AsyncClient.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/AsyncClient.cpython-37.pyc new file mode 100755 index 0000000000000000000000000000000000000000..221f74bd30212789d5871e34e21a8cc8c74c562b GIT binary patch literal 3151 zcmai0TW=dh6rP#Ac zGHE`TbLeIjL^8>9)~QF{;qF~@#3xJ&>3zqfr`Wc)?{$34_%gsuurE4+xemEW;`?)L1sJ-;x$fV&>9t!$M$YGpIsQAX+e zN}H9;m}H<=s$A%5rblMoH2wx>b@yBt8+wL-!1xJgo5Y zWMxL$QloLIIk~phe4-`S0p?<%=~UaSCOSI7Ytp7KN3yagR91D%BH2l$TEe7pcksar zb27{=or*%?L)|%{0yD(xHUxSP62ckaM_lp)z9)uo`_l-pyF#xH1wu=FkM})M3FjnM zJW~nGV{U)!$VY1)JT(*-+1E$+Je=r_{GoTiIlFiJCPEdi_16F5xSRd{H(DI%26!8o zUf!*A2wszjwt1zvrZUyNuVTNpjW5$GwPCi^FYX!}xN+V}i?k-u^+x#^4l>6;7+(@? zPESiL@wRC3!Ei7>6z{fZ4Vfc|+KYX6nH8ol7BMULx9@ zHtj3L#jY9qW#Vj4J;>%2m~}I@S1;mDLsntDg@F4nSJU!;7yOB_;QClC{$X*&Sln-Q zw^C!-^a*U4m(9Pf3i%2?7}AL{U~qiSinL-qjwoRYBwLc$R+8+LvY!**PLg~5G_Swt zMY`e$ktc~fOXPVXl(I8~!e5gkC{PS#P{gA!@}i|EaPt|arj5Ks%|YY@#C~9R5Z3^* z=>LK22joO2f+rX3!VC;#l%~{Zq)Vuqb08Ix=0}9V0VM)LfiTSBS};TcB;n8pZMblW zs^+M5K!LO1-rJLqS+tG3+}!LIo3^>Bs>|S&)`XKb9KX#V|1Ziy} zCCuuUgqqo18<8s<*FqlMztpe#S|QsAW^~h?wdKwtK-FZ`nA>KOWO)i?Oltc()03ZK zbaNC0nKVYeP=pA{!HF3gJpw(2$UWJX0f@v zyqw|gX(Lk}M>nKj#c&^P*cWy73KvtF);J@?ytbK(N(aT!YM$QiQ)a#L4XXw~BK{jb0V}E`WH#Oyy^}kJZ;jXohWm=_`N+ueVg?<%F z^eG~*5qX_<1SVIiH$f9#`)nevle94wg$lZ%iov)52nv3>E?&p~MeHerkOin;KsV<> zD9{Ny_V80M6n8^?1{Hv#N(fYmGk_LCLZks_1p$q(*NBdSdTxyUHr!FwRw+A;UVkI+ znyomr{nM}=c5E8#EY@UOJ@M-WH_eaXZX-=^XXu#?_T-UxqfuAIXW zb+Y_?+3CQ{S+_bEg$|DzbiydAT?M`|NiE_>%>$-;jstq_c?JH__B=|qFW5D9tF}~Q z=pliE^-HmM$j?0F@5jQnRbc=-Noulgmz>y3b&6juGYG!Cap}#o@j`96_n?9eWPhh; zY^aK?luDn*Tpj0O4PGu1T!pZ{EDL4BaX^g!54>_?yW?NP4}kuF_C6{jlnZX-WiaMB z5Gr22cmh8IG`8s1fT(bRa&*@I1fxh%f?`d@Sf3=~YS+8OQM5Tf*OD!c5fmYiPf(Nu zL0T@;N+-%o;3$K~#jL#$HSvU+=Ht*QtzV|qWS81P6rJw-j9N}Xm*>%HU1QfMUtKVv KMDj=+d+8t1ldG5j literal 0 HcmV?d00001 diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/AudioManager.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/AudioManager.cpython-37.pyc new file mode 100755 index 0000000000000000000000000000000000000000..1be52bd1fdb456d42d1a9ec239e846685a1eeac5 GIT binary patch literal 11497 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 Hm)!paNzD;b literal 0 HcmV?d00001 diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/CommandHandler.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/CommandHandler.cpython-37.pyc new file mode 100755 index 0000000000000000000000000000000000000000..a90e10e7203146942dac74a00942df3425dcccd2 GIT binary patch literal 3088 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 bY=(I5bK06+JEn8EPTXIR%ID8tIuHL3ywwG* literal 0 HcmV?d00001 diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Commands.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Commands.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8b8bd5ae8f44a75c6d15bad441e8af6e228cb61d GIT binary patch literal 25569 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 literal 0 HcmV?d00001 diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Config.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Config.cpython-37.pyc new file mode 100755 index 0000000000000000000000000000000000000000..b0394d689d9336d87948e6c85a29b11507152476 GIT binary patch literal 1138 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 z zzD1-;gq$bhJ4C)qTfkzzlYLN-Xt zC2JQ+k9^BGGo|py&DD$iMGKE4pRL|S&2N9U!ug=kne5+Vs9fJ2Cmn$0xq?`zPWQCc zhg(Cl>3$QZd|YqPwLUI?Y`W{zZ20IwP4d^WJy*YjdJ8Jvq$8SJXKjx z9-O@N6NpiCr9AM9l$rwxGG%pRDQ;5EEZ6I}zLd<7=Xff?RlF1AX>p!yw_Ci83w3qj zsN4+F5r2VRI8S7Th;$DYDJ2O_mz0`+M8Z1>qA?R*7PIG{m+8*H?o4qm`DjKbGRZ=M z9x<;8&mb58=Nn2GoC8<<6n#b_|Hfy=$FiJDM}hnjg1&B0&!i>O(iI5S4sWz2>qsEw zuVe%U9JF3Fa|;*fNL5F}#4X3&@E-)$dSlB$IfpmEz$212fpqTHXB3Ub?i~g6W z4(}4j^`y(7kR9>O?{EXrcTT3Yk$ek7x_%o~|3Y1pP{J!S;HHl|jc+d*wZo5M3*p}p zy#EEED=2(jLN_J2X5VK^IH}*nnVsgOX+VJGJqd0@^gVo)(!^IOO}Z_~BN)x}z7gK7 zjC!9@$zEX}cEKOR+~Gr=&vcqQd(?eL){Ud?Pf=&$)18C5)3WXe>OMoAh0l57I@FjF z-BF%{5AlDHA61m&uBN|s6ZQUBe;Lmoyh}>TXoc>kyq%WcIdcwIt;MtWI^gUyJ_{%n zql5h)7V%lY=On3Bb4}W-k{+a6&LgKKmE`jjs+77B^Q)=5ODomqRy0VlVOwI2_=Sk%AO-~o`}?ZQWzzgB#_DylZ#B5a%qXl zVh<6Q466PWAynidJ1>>_C={B)IBwjEtn-!uWJ<2jh4n`)e zROHA@GBVfjjFiSulm~WOF$#H;D%;=zifJBZ5xgQuq&#FuAksQST3*7FcuFjKWucUo zDy7t%ENhULW+NQ1ymt$c;!X`lmA>Vom-m=Nx|{noLg;oRb2#{|MU431qOwxTCa%bD z(#uJg>m)MBNq#1!LqP;j=||jHKk*TJ)A(5AoHzypgU|q zcdmji{lo$hT8YIHkq?Pfh>&+xtPq(XA_bIs2}Z+Dkm^pW1utolYKcJbUZ=D~O2(9o zjCYn2EMKs4R>n$Oc_1D^O#ckrrG}L*6bfgg)MIik(+jdh3Lw=iPPU$3!m=b~xNg=)VDJ>De3r literal 0 HcmV?d00001 diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/SourceModAPI.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/SourceModAPI.cpython-37.pyc new file mode 100755 index 0000000000000000000000000000000000000000..7f15ffe8a6a1ce048797cdbdfaea518a61fbdc4a GIT binary patch literal 1035 zcmZ8f&2G~`5T0HC#0hCB0xIAMu!I`^j;PR974*=82nUqQ%JpuVv`!p$H$YS8LM!zp zI3!12frkKz<6b%O3Y?g6NN8DW#v9Mfmv6qA&84Lp;Q99BtLSoopY&sXJiI(ZGk4HQ zkSv21JO|08cLvfsmGD$ z%$;vk)^=epF;?j>phK+q{j|}^d#Z7e^psKhz0#(U8Y4!kVHdTJ8vk$_7n!ULj$I@~ zx8Jow5S%HgvC+*-S@Vob#3M{6Nz7w($14^rl0W4WUa%=j=!5l+YI=3`i7Ar6#HtkpueYcxNEO!IvTc;$7U+7@bhKrueSge;jNof)P0|zGZ2LJ#7 literal 0 HcmV?d00001 diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/SourceRCONServer.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/SourceRCONServer.cpython-37.pyc new file mode 100755 index 0000000000000000000000000000000000000000..3294fce3299fa5b6b96da29ebacbada9aab122f0 GIT binary patch literal 3272 zcmai0-EZ8+5#L?%c)U-`h8xvMKV;Vpe1&cG6DV3Ebs{-c5cecV3hWXESc$HJ7(7HPc0eSPJY_-E^MKpA2!!pOz_Gm>xVEnaHQRfEhhC)hLqG62 zla6%L#$h99aQ0g!YtlPq(t9C-rsP|!S3ko(tl#6t(<;p)dB!ovw6l>$$4dV$%A!4` zV_J3oxU)fV7X!seuRuUHmjbc{WINJ-$#%K)Wc{=exG$N@f*N*fn1!wUNXKe(Wxc

qy7W&OX1;95Ce}1$OSaKBWe2-k*J z_5Pf(k|ruE<8z~^r?!Gt{1jx&2W-qj^q_n!K*iVrbtG}zvNrL?Y*L@HDTlimHWdAj zd%kfW=lRfhm6@g%7I}PN+QQl=#Yt}5?Wi~~op7j>4)>!> zrb_o*;}$C2)paNXagd@)-N59ZOfR7OYGrvpKUB+mkX%@w7Rzx_gd??F<~rU_lfC`& zXUp3c54mGn?j&nSS(1Oz5VEdh#IwyLbpPx_f*L$H=Da5e{(;3!bp-y30y_(A0N0=A8YW zW#TFLk}MGufPjwtX?5tZ#Wl_9=u$fQ`X}p;w9d6XxWL;~hxv2W(xjdzp{BgBuBs?4-D zB3-Br%vA{hRWMlvOpG@Si)5r;ckesc<_BoSbr2>5e+SqW!sFlO3!G-7yK}$(Km5#( zHYT;{si9F=uAmjGASLjPv;hCkMq(-!+1Qx~DW-f8*?5tixbU1)y)WId`vR-S@ajeS zaQoz&QSGzskMI4m0yyi{^m~wRT! zfDL$n_yeGXviRQ-N3uTU(kHY4qi*r#*p&@n@=Hp&17v%Q|0SE0Z0)>0@yo`fIc0;^ zpe_v-`LaXrvbArJ)_t2ac?ET7CVQ|jxjIE&u-IPc zkK6}onx0YaJfpNUJ3-?mds(hkPZ-~xTaxV=Pf=_K-S}Y57zvP}too=Js^}5h{Qm9y~GNu@#hkA-R!b>p(p?LcnqG?K(5#;UJm83RBB!sw_47^Dl`QAnb#(r5F zG|-yJM>amdYvDTM?m_~^3+8R<(0Gp|bnJzvJFa1T#p2xU+k~x2gELa4w_xfiU_r2M z01FjF^sn$r(*>$Z>>ed{RR&F@9sRtz47CbX?xf(;r%g;~O+GA5b8G!^xV66W>7#Aa z-dYbgAAPp<=)=|3P2=t)8DN+uMX9o&wt zrExcMT~;eN;2Xv%;2nnZ>EK@dnP$FF+_sbb$Id zL>_`b5$la#6XO99d&-0n;MyaD%?NH%tB!@$oohCb)a+9W4-ASp5f89DHDLMv4NYmws*D G7GDEAZx8(d literal 0 HcmV?d00001 diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Subscribe.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Subscribe.cpython-37.pyc new file mode 100755 index 0000000000000000000000000000000000000000..032d56bf2c092b04e38088ea4c2d00358578c108 GIT binary patch literal 4098 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 HwD0^M_Dzwk literal 0 HcmV?d00001 diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Utils.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Utils.cpython-37.pyc new file mode 100755 index 0000000000000000000000000000000000000000..d01d4834c72ecea4839cf25051cb666e9ffc696d GIT binary patch literal 2426 zcmaJ?&2Jnv6t_Je`;ko=3Q{S8GOCc;t-_{NqXJTes%_O&YP*VP0BHn_CTnMR=Y%;Vz ze)}bOUSsS}8qAvo<_7-cB8X&?$INHzObTfoFlp@zpJQZ82O|d~A^8K=c8{@|_4-8T zt&o+^xwpvyakeqlm#qhhdkv+oWpPU#CvU#H-pd^9gM?~%B@71ww2q7inlfG_Z!vZ zk4ysz_TuB!;bf#%2Uwpdy{mMx+E0>TrdG{F_lI#b7@DiAn-#zMxrySW^XN%l4}xeM znILeX4Dqei+9= z(7-4u)V^-AoPWh6IagqJ8jOL_lzj{2_AL(MbL473i{R0;`hR>Vd|V*&Wz0)_wsaz9 zXz6pzQlq{Kw}rvn_P(gB4o+I-W#p8Z6G2;8vjovR-jn7q)fT-1t9Fgxq09#X>L3_R zZJK&2`0N#}~U{KGUqM$0sNL>XP>6JX=d)HESi+$b6L@Ey15z}7E zQ}GeY_)atB`r-ydFQj-VG{*CpFU#awy_~(^AGUd3ixL?PBAf}|-c)-gw?0!Q=l$1< z%w1QeHyu&2Bt)37d6NSIZsV)-mYB85V^A)voTsv_YVCIn$ayI;vG;Sz^m9Y!l^yZS zO0DGF1~VeH_RG=A21_jj!1TQow~l1riL)%jIWK~`C+&>G$5SH9eSZ3`!OC4E*tY_dxY#I zAZyDe^n7DD>Okxxo0&6S#>m3$Zp(%5A}@?UDgk3A5MCHgN8$K_w;jc*Sh3=%&Y)8+TJi37SG@c9*YR)S-{?YPZ(@{ZhEQ6l zTD{x#9!GKPZ7ENyM-y$7Bs-6X6X+aIM_YI%ysfE$CNCUIFWj2!s&dOP1UJwsG)jAq zwCYDmG#SH2;l!1~z>2qab?(m^Sn_GDbG7hmtpfo$hieqO^xHsu=9c^Lyg7w~MftVy z7T@hw?EP-BmZa<5W4gSE-R$ZQ{$V#tc7P$;4>!u4+^>y`OCg!LPcLUruVif}cedk6 zh{8?q8oZ0exuYl3vCQ2Pdem%_un!7n{H0{Ny&dhTBrxGtjIsqnxmybTMumaDRPF|t z0qy=*E#INUJ;2Oum`o^eR;!~zmr=hl+NYU$)ARP8u1i>(WVtZnIYG*6J0NBci^=k@F5bMq-( M-o?vU7fY?b0dU9@0{{R3 literal 0 HcmV?d00001 diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/__init__.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/__init__.cpython-37.pyc new file mode 100755 index 0000000000000000000000000000000000000000..5f225dfbb2a500269c1ce370812b75864aa37ed3 GIT binary patch literal 146 zcmZ?b<>g`k0**`4aoRxoF^B^Lj6jA15Erumi4=xl22Do4l?+87VFdBZT0bK{H&s79 zF*mh1wWut$s8~O_xH!HlRlg*^C^;i1Gd-ikSU&{L){l?R%*!l^kJl@xyv1RYo1ape NlWGSt^fM4M008jvBXj@& literal 0 HcmV?d00001 diff --git a/torchlight_changes_unloze/torchlight3/_start.sh b/torchlight_changes_unloze/torchlight3/_start.sh new file mode 100755 index 00000000..9403c544 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/_start.sh @@ -0,0 +1,4 @@ +#!/bin/bash +cd "$(dirname "$0")" +source venv/bin/activate +python3 main.py diff --git a/torchlight_changes_unloze/torchlight3/_start_mg.sh b/torchlight_changes_unloze/torchlight3/_start_mg.sh new file mode 100755 index 00000000..76a3fdd3 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/_start_mg.sh @@ -0,0 +1,4 @@ +#!/bin/bash +cd "$(dirname "$0")" +source venv/bin/activate +python3 mainmg.py config-mg.json diff --git a/torchlight_changes_unloze/torchlight3/access.json b/torchlight_changes_unloze/torchlight3/access.json new file mode 100755 index 00000000..66379d74 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/access.json @@ -0,0 +1,22 @@ +{ + "[U:1:69566635]": { + "level": 100, + "name": "Jenz" + }, + "[U:1:64494019]": { + "level": 100, + "name": "Neon" + }, + "[U:1:126555404]": { + "level": 3, + "name": "Migza" + }, + "[U:1:20383465]": { + "name": "WASD", + "level": 3 + }, + "[U:1:28627906]": { + "name": "Berry", + "level": 1337 + } +} diff --git a/torchlight_changes_unloze/torchlight3/main.py b/torchlight_changes_unloze/torchlight3/main.py new file mode 100755 index 00000000..b8f6daf4 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/main.py @@ -0,0 +1,37 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import logging +import asyncio +import os +import sys +import threading +import traceback +import gc +from importlib import reload + +global TorchMaster + +import Torchlight.Torchlight +from Torchlight.SourceRCONServer import SourceRCONServer + +if __name__ == '__main__': + logging.basicConfig( + level = logging.DEBUG, + format = "[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s", + datefmt = "%H:%M:%S" + ) + + Loop = asyncio.get_event_loop() + + global TorchMaster + TorchMaster = Torchlight.Torchlight.TorchlightHandler(Loop) + + # Handles new connections on 0.0.0.0:27015 + RCONConfig = TorchMaster.Config["TorchRCON"] + """RCONServer = SourceRCONServer(Loop, TorchMaster, + Host = RCONConfig["Host"], + Port = RCONConfig["Port"], + Password = RCONConfig["Password"])""" + + # Run! + Loop.run_forever() diff --git a/torchlight_changes_unloze/torchlight3/mainmg.py b/torchlight_changes_unloze/torchlight3/mainmg.py new file mode 100755 index 00000000..27a8b3f0 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/mainmg.py @@ -0,0 +1,37 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import logging +import asyncio +import os +import sys +import threading +import traceback +import gc +from importlib import reload + +global TorchMaster + +import Torchlight.Torchlightmg +from Torchlight.SourceRCONServer import SourceRCONServer + +if __name__ == '__main__': + logging.basicConfig( + level = logging.DEBUG, + format = "[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s", + datefmt = "%H:%M:%S" + ) + + Loop = asyncio.get_event_loop() + + global TorchMaster + TorchMaster = Torchlight.Torchlightmg.TorchlightHandler(Loop) + + # Handles new connections on 0.0.0.0:27015 + RCONConfig = TorchMaster.Config["TorchRCON"] + """RCONServer = SourceRCONServer(Loop, TorchMaster, + Host = RCONConfig["Host"], + Port = RCONConfig["Port"], + Password = RCONConfig["Password"])""" + + # Run! + Loop.run_forever() diff --git a/torchlight_changes_unloze/torchlight3/requirements.txt b/torchlight_changes_unloze/torchlight3/requirements.txt new file mode 100755 index 00000000..f69a03d7 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/requirements.txt @@ -0,0 +1,23 @@ +aiohttp +appdirs +async-timeout +beautifulsoup4 +certifi +chardet +gTTS +gTTS-token +idna +lxml +multidict +numpy +olefile +packaging +Pillow +pyparsing +python-magic +requests +six +urllib3 +yarl +cython +geoip2 diff --git a/torchlight_changes_unloze/torchlight3/run.sh b/torchlight_changes_unloze/torchlight3/run.sh new file mode 100755 index 00000000..7553eb37 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/run.sh @@ -0,0 +1,5 @@ +#!/bin/bash +screen kill torchlight +screen kill torchlight-noct +screen -L -A -m -d -S torchlight ./_start.sh +screen -L -A -m -d -S torchlight-mg ./_start_mg.sh diff --git a/torchlight_changes_unloze/torchlight3/triggers.json b/torchlight_changes_unloze/torchlight3/triggers.json new file mode 100755 index 00000000..a2986e12 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/triggers.json @@ -0,0 +1,405 @@ +[ + {"names": ["!pyramid"], "sound": "ascream7.mp3"}, + {"names": ["!applause"], "sound": "applause.wav"}, + {"names": ["!seeyou", "!seeyouagain"], "sound": "sephiseeyou.mp3"}, + {"names": ["!chosen"], "sound": "sephonlythechosen.mp3"}, + {"names": ["!laser"], "sound": "bladeout.wav"}, + {"names": ["!prepare", "!battle"], "sound": "prepareforbattle.mp3"}, + {"names": ["!tuturu"], "sound": "tuturu.mp3"}, + {"names": ["!baka"], "sound": "baka.wav"}, + {"names": ["!omae"], "sound": "Omae.mp3"}, + {"names": ["!poi"], "sound": "poi.wav"}, + {"names": ["!shit"], "sound": "shit.wav"}, + {"names": ["!sugoi"], "sound": "sugoi.wav"}, + {"names": ["!allahu", "!akbar"], "sound": "allahu_akbar1.mp3"}, + {"names": ["!allahu2", "!akbar2"], "sound": "AllahuAkbarDubstep.mp3"}, + {"names": ["!quack"], "sound": "quack.mp3"}, + {"names": ["!english"], "sound": "english.wav"}, + {"names": ["!squad", "!suicidesquad"], "sound": "suicidesquad.mp3"}, + {"names": ["!cena"], "sound": "john.mp3"}, + {"names": ["!solo"], "sound": "solo.mp3"}, + {"names": ["!popopo"], "sound": "popopoguys1.mp3"}, + {"names": ["!tageule"], "sound": "tagueule.wav"}, + {"names": ["!idiot"], "sound": "omguidiot.mp3"}, + {"names": ["!game"], "sound": "isonly1.mp3"}, + {"names": ["!wah"], "sound": "wah wah sound effect.mp3"}, + {"names": ["!yes"], "sound": ["ohyes.mp3", "yes.mp3", "m-bison-yes-yes.mp3", "yesyesyesjojo.mp3"]}, + {"names": ["!overconfidence"], "sound": "Monster_Kill.wav"}, + {"names": ["!beer", "!pussy"], "sound": "grababeer.mp3"}, + {"names": ["!nein"], "sound": "Nein nein nein2.mp3"}, + {"names": ["!ayy", "!lmao"], "sound": "AyyyLmao_louer.mp3"}, + {"names": ["!blyat"], "sound": "BLYAT_louder.mp3"}, + {"names": ["!monkey", "!language"], "sound": "NomnkyLanguage_louder.mp3"}, + {"names": ["!wanker"], "sound": "OIYAWANKER_louder.mp3"}, + {"names": ["!vortex"], "sound": "WAAAAhByVortexx_louder.mp3"}, + {"names": ["!slutty"], "sound": "sluttyrainbow.wav"}, + {"names": ["!hello"], "sound": "Hello2.mp3"}, + {"names": ["!george"], "sound": "George.mp3"}, + {"names": ["!george2"], "sound": "George2.mp3"}, + {"names": ["!erebus"], "sound": "erebus.wav"}, + {"names": ["!kabzor", "!neon"], "sound": "kabzorrage.wav"}, + {"names": ["!otominas"], "sound": "Otominas.mp3"}, + {"names": ["!airhorn"], "sound": "AIRHORN.mp3"}, + {"names": ["!reee", "!reeee"], "sound": "reeeeeeee_v2.mp3"}, + {"names": ["!wombo"], "sound": "wombo combo2.mp3"}, + {"names": ["!vaperwave"], "sound": "Vaperwave.mp3"}, + {"names": ["!roasted"], "sound": "roasted2.mp3"}, + {"names": ["!ogre"], "sound": "its all ogre now.mp3"}, + {"names": ["!failed"], "sound": "Mission Failed.mp3"}, + {"names": ["!youmad"], "sound": "meshlem_arrive2_good.mp3"}, + {"names": ["!daniel"], "sound": "daniel.mp3"}, + {"names": ["!action"], "sound": "Action is Coming.mp3"}, + {"names": ["!brucelee"], "sound": "Ugandan Bruce Lee.mp3"}, + {"names": ["!gwegwe"], "sound": "Gwe Gwe Gwe.mp3"}, + {"names": ["!bruceu"], "sound": "We Call Him Bruce U.mp3"}, + {"names": ["!commando"], "sound": "COMMAND COMMANDO PagChomp.wav"}, + {"names": ["!nuffin"], "sound": "DINDU NUFFIN.wav"}, + {"names": ["!dinosaurs"], "sound": "DINOSAURS ZULUL.wav"}, + {"names": ["!deadlyactions"], "sound": "NON STOP DEADLY ACTIONS PagChomp.wav"}, + {"names": ["!gimme"], "sound": "GIMME GIMME GIMME GIMME.wav"}, + {"names": ["!skicker"], "sound": "SUPA KICKER.wav"}, + {"names": ["!sbiker"], "sound": "SUPA BIKER.wav"}, + {"names": ["!sfighter"], "sound": "SUPA FIGHTER.wav"}, + {"names": ["!smafia"], "sound": "SUPA MAFIA.wav"}, + {"names": ["!tigercage"], "sound": "THE TIGER IS IN THE CAGE.wav"}, + {"names": ["!serious"], "sound": "THIS IS SERIOUS.wav"}, + {"names": ["!hehehe"], "sound": "HE HE HE.wav"}, + {"names": ["!tigermafia"], "sound": "TIGER MAFIA.wav"}, + {"names": ["!cheeki"], "sound": "AH NU CHEEKI BREEKI IV DAMKE.mp3"}, + {"names": ["_legend"], "sound": "legend.mp3"}, + {"names": ["!admun_pls"], "sound": "Admun Please Electro (Short).mp3"}, + {"names": ["!dolphin"], "sound": "Dolfin - Song.mp3"}, + {"names": ["!omg_remix"], "sound": "O my Gah (Remix #3).mp3"}, + {"names": ["!rainbow_panda"], "sound": "ITS RAINBOW PANDA.mp3"}, + {"names": ["!dong"], "sound": "Ding Dong.mp3"}, + {"names": ["!ching_club"], "sound": "Ching Chong (Remix #9) Club.mp3"}, + {"names": ["!tonguetwister"], "sound": "tongue-twister.mp3"}, + {"names": ["!cooler"], "sound": "cooler.mp3"}, + {"names": ["!fatality"], "sound": "fatality.mp3"}, + {"names": ["!inception"], "sound": "inceptionbutton.mp3"}, + {"names": ["!leroy"], "sound": "leroy.mp3"}, + {"names": ["!ecchi"], "sound": "ecchidarling.mp3"}, + {"names": ["!moe"], "sound": "moe.mp3"}, + {"names": ["!nants"], "sound": "nants_ingonyama_bagithi_baba_-_the_circle_of_life-mp3cut.mp3"}, + {"names": ["!nyanpasu"], "sound": "nyanpass_2.mp3"}, + {"names": ["!oww"], "sound": "oww.mp3"}, + {"names": ["!wololo"], "sound": "wololo.mp3"}, + {"names": ["!yuuta"], "sound": "yuuuta_itaiiii.mp3"}, + {"names": ["!zawarudo"], "sound": "za-warudo-stop-time-sound.mp3"}, + {"names": ["!slow"], "sound": "seph_slow.wav"}, + {"names": ["!tuturu"], "sound": "Tutturuu_v1.wav"}, + {"names": ["!baka"], "sound": ["baka.wav", "Baka.mp3"]}, + {"names": ["!buhi"], "sound": "buhi.wav"}, + {"names": ["!nyan"], "sound": "nyanpass.wav"}, + {"names": ["!trial"], "sound": "trial.wav"}, + {"names": ["!mail"], "sound": "yuu_new_mail.wav"}, + {"names": ["!balrog"], "sound": "balrog_scream.wav"}, + {"names": ["!bluescreen", "!error"], "sound": "error.wav"}, + {"names": ["!hallelujah"], "sound": "hallelujah.wav"}, + {"names": ["!trap"], "sound": "itsatrap.wav"}, + {"names": ["!whores"], "sound": "whores.wav"}, + {"names": ["!gandalf"], "sound": "vozyoucannotpass.wav"}, + {"names": ["!cat"], "sound": "gato.wav"}, + {"names": ["!turtle", "!turtles"], "sound": ["iliketurtles.wav", "wherearetheturtles.mp3"]}, + {"names": ["!laser"], "sound": "blade_out.wav"}, + {"names": ["!king", "!kingoftheworld"], "sound": "vulcan.wav"}, + {"names": ["!seeyou", "!seeyouagain"], "sound": "seph_iseeyou.wav"}, + {"names": ["!chosen"], "sound": "seph_onlythechosen.wav"}, + {"names": ["!goodbye", "!saygoodbye"], "sound": "seph_saygoodbye.wav"}, + {"names": ["!slow"], "sound": "seph_slow.wav"}, + {"names": ["!late", "!toolate"], "sound": "seph_toolate.wav"}, + {"names": ["!growl"], "sound": "bahamut_growl2.wav"}, + {"names": ["!buyspins", "!buy_spins"], "sound": "BUYSPINS1.wav"}, + {"names": ["!death", "!deathscream"], "sound": "d_death_scream.wav"}, + {"names": ["!attack", "!meetyourend"], "sound": "z_godend.wav"}, + {"names": ["!die", "!guardian"], "sound": "z_guardianend.wav"}, + {"names": ["!applause"], "sound": "applause.wav"}, + {"names": ["!retreat"], "sound": "stage_x_gandalf_retreat-1-0.wav"}, + {"names": ["!idiot"], "sound": "idiot.wav"}, + {"names": ["!duck", "!quack"], "sound": "quack.wav"}, + {"names": ["!triple"], "sound": "triple.wav"}, + {"names": ["!solo"], "sound": "solo.wav"}, + {"names": ["!oniichan", "!onii-chan"], "sound": "Onii-chan.wav"}, + {"names": ["!gameover"], "sound": "Gameover.wav"}, + {"names": ["!wombo"], "sound": "WomboCombo.wav"}, + {"names": ["!camera"], "sound": "MOM GET THE CAMERA.wav"}, + {"names": ["!hihi"], "sound": "skullz.wav"}, + {"names": ["!yatta"], "sound": "Hiro Yatta.wav"}, + {"names": ["!over"], "sound": "over.wav"}, + {"names": ["!cena"], "sound": "cena.wav"}, + {"names": ["!victory"], "sound": "ffvii_victory.wav"}, + {"names": ["!2012"], "sound": ["2012_1.wav", "2012_2.wav", "2012_3.wav", "2012_4.wav"]}, + {"names": ["!nein"], "sound": "hitler_nein.wav"}, + {"names": ["!allahu", "!akbar"], "sound": "allahu_akbar.wav"}, + {"names": ["!chopper"], "sound": "gettothechopper.wav"}, + {"names": ["!payback"], "sound": "blain - payback time.wav"}, + {"names": ["!scotland"], "sound": "scotland.wav"}, + {"names": ["!happening"], "sound": "happening.wav"}, + {"names": ["!topkek"], "sound": "topkek.wav"}, + {"names": ["!welcome", "!ricefields"], "sound": "welcome_to_the_ricefields.wav"}, + {"names": ["!inception"], "sound": "inception.wav"}, + {"names": ["!x"], "sound": "xfiles.wav"}, + {"names": ["!stfu"], "sound": ["stfu.wav", "stfu2.mp3"]}, + {"names": ["!goat"], "sound": ["goat1.wav", "goat2.wav", "goat3.wav", "goat4.wav", "goat5.wav", "goat6.wav"]}, + {"names": ["!doit"], "sound": "doit.wav"}, + {"names": ["!benis"], "sound": "grossenbenis.wav"}, + {"names": ["!omg"], "sound": "omg.wav"}, + {"names": ["!wow"], "sound": ["wow.wav", "rexy_wow.wav"]}, + {"names": ["!prepare", "!battle"], "sound": "stage_2_preparaos-0-0.wav"}, + {"names": ["!meme"], "sound": "nicememe.wav"}, + {"names": ["!nyaa"], "sound": ["nyaa_1.wav", "nyaa_2.wav", "nyaa_3.wav", "nyaa_4.wav", "nyaa_5.wav", "nyaa_6.wav", "nyaa-3.mp3", "nyaa4.mp3"]}, + {"names": ["!ah"], "sound": "FFVII_Cry.wav"}, + {"names": ["!feelsbad"], "sound": "feelsbadman.wav"}, + {"names": ["!admun"], "sound": "admun_please.wav"}, + {"names": ["!population"], "sound": "population.wav"}, + {"names": ["!why", "!immigration"], "sound": "immigration.wav"}, + {"names": ["!tutury"], "sound": "tutury.wav"}, + {"names": ["!lew"], "sound": "Bewlewlewlew.wav"}, + {"names": ["!pudi"], "sound": "Pudi Pudi (Short).wav"}, + {"names": ["!hey", "!listen"], "sound": "Hey Listen.wav"}, + {"names": ["!comeon"], "sound": "Come on.mp3"}, + {"names": ["!stopp"], "sound": "itstimetostop.mp3"}, + {"names": ["!cancer"], "sound": "cancer.mp3"}, + {"names": ["!heil"], "sound": "heil.mp3"}, + {"names": ["!nico"], "sound": "niconiconi.mp3"}, + {"names": ["!cock"], "sound": ["cock1.mp3", "cock2.mp3", "cock3.mp3", "cock4.mp3", "cock5.mp3", "cock6.mp3"]}, + {"names": ["!rock"], "sound": "LetsRock.mp3"}, + {"names": ["!fmlaugh"], "sound": "fmlaugh.mp3"}, + {"names": ["!monopoly"], "sound": "monopoly.mp3"}, + {"names": ["!kiddin", "!kidding"], "sound": "rexy r u kiddin me.mp3"}, + {"names": ["!noice"], "sound": "Noice.mp3"}, + {"names": ["!rexy"], "sound": "rexywaah.mp3"}, + {"names": ["!kaboom"], "sound": "kaboom.mp3"}, + {"names": ["!honk"], "sound": "honk.wav"}, + {"names": ["!spam"], "sound": "No_Spammerino_In_The_Chatterino.mp3"}, + {"names": ["!ohoho"], "sound": ["ohoho_1.mp3", "ohoho_3.mp3", "ohoho_4.mp3", "ohoho_5.mp3", "ohoho_6.mp3", "ohoho_7.mp3", "ohoho_8.mp3", "ohoho_9.mp3", "ohoho_10.mp3", "ohoho_11.mp3", "ohoho_12.mp3", "ohoho_13.mp3", "ohoho_14.mp3", "ohoho_15.mp3", "ohoho_16.mp3", "ohoho_17.mp3", "ohoho_18.mp3", "ohoho_19.mp3", "ohoho_20.mp3", "ohoho_21.mp3", "ohoho_22.mp3", "ohoho_23.mp3", "ohoho_24.mp3", "ohoho_25.mp3", "ohoho_26.mp3", "ohoho_27.mp3", "ohoho_28.mp3", "ohoho_29.mp3", "ohoho_30.mp3", "ohoho_31.mp3", "ohoho_32.mp3", "ohoho_33.mp3", "ohoho_34.mp3", "ohoho_35.mp3", "ohoho_36.mp3", "ohoho_37.mp3", "ohoho_38.mp3", "ohoho_39.mp3", "ohoho_40.mp3", "ohoho_41.mp3", "ohoho_42.mp3", "ohoho_43.mp3", "ohoho_44.mp3", "ohoho_45.mp3", "ohoho_46.mp3", "ohoho_47.mp3", "ohoho_48.mp3", "ohoho_49.mp3", "ohoho_50.mp3", "ohoho_51.mp3", "ohoho_52.mp3", "ohoho_53.mp3", "ohoho_54.mp3", "ohoho_55.mp3", "ohoho_56.mp3", "ohoho_57.mp3", "ohoho_58.mp3", "ohoho_59.mp3", "ohoho_60.mp3", "ohoho_61.mp3", "ohoho_62.mp3", "ohoho_63.mp3", "ohoho_64.mp3", "ohoho_65.mp3", "ohoho_66.mp3", "ohoho_67.mp3", "ohoho_68.mp3", "ohoho_69.mp3", "ohoho_70.mp3", "ohoho_71.mp3", "ohoho_72.mp3", "ohoho_73.mp3", "ohoho_74.mp3", "ohoho_75.mp3", "ohoho_76.mp3", "ohoho_77.mp3", "ohoho_78.mp3", "ohoho_79.mp3", "ohoho_80.mp3", "ohoho_81.mp3", "ohoho_82.mp3", "ohoho_83.mp3", "ohoho_84.mp3", "ohoho_85.mp3", "ohoho_86.mp3", "ohoho_87.mp3", "ohoho_88.mp3", "ohoho_89.mp3"]}, + {"names": ["!sugoi"], "sound": "sugoi_sugoi.mp3"}, + {"names": ["!cry"], "sound": ["cry1.wav", "cry2.wav", "cry3.wav", "cry4.wav", "cry5.wav", "cry6.wav", "cry7.wav"]}, + {"names": ["!hehe", "!giggle"], "sound": "Giggle.mp3"}, + {"names": ["!monkey"], "sound": ["chimp1.wav", "chimp2.wav"]}, + {"names": ["!ka", "!kaka"], "sound": ["Nisemonogatari-Shinobu-Kaka.ogg", "Nisemonogatari-Shinobu-K-ka.ogg"]}, + {"names": ["!jodel"], "sound": "jodel.mp3"}, + {"names": ["!nyaaa"], "sound": "nyaaa.mp3"}, + {"names": ["!run"], "sound": "run.wav"}, + {"names": ["!goodbye"], "sound": "goodbye.wav"}, + {"names": ["!noo"], "sound": "LOTR_Noooooo.wav"}, + {"names": ["!dayum"], "sound": "daaamn.mp3"}, + {"names": ["!goddammit", "!goddamnit"], "sound": "goddammit.mp3"}, + {"names": ["!surprise"], "sound": "surprisemotherfucker1.mp3"}, + {"names": ["!csi"], "sound": "yeeaah.mp3"}, + {"names": ["!nope"], "sound": "engineer_no01.mp3"}, + {"names": ["!joke"], "sound": "rimshot.mp3"}, + {"names": ["!weed"], "sound": "smokeweederryday.mp3"}, + {"names": ["!toasty"], "sound": "toasty.mp3"}, + {"names": ["!damn"], "sound": "wheredyoufindthis.mp3"}, + {"names": ["!nuts"], "sound": "suckmynuts.mp3"}, + {"names": ["!wake"], "sound": "wakemeup.mp3"}, + {"names": ["!bye"], "sound": "bye.mp3"}, + {"names": ["!ilikeit"], "sound": "ilikeit.mp3"}, + {"names": ["!milk"], "sound": "milk.mp3"}, + {"names": ["!pussy"], "sound": "pussy.mp3"}, + {"names": ["!retard"], "sound": ["retard.mp3", "retard2.mp3"]}, + {"names": ["!sorry"], "sound": "sry.mp3"}, + {"names": ["!wtf"], "sound": "wtf.mp3"}, + {"names": ["!brb"], "sound": "brb.mp3"}, + {"names": ["!cricket"], "sound": "cricket.mp3"}, + {"names": ["!hax"], "sound": "hax.mp3"}, + {"names": ["!hi"], "sound": "hi.mp3"}, + {"names": ["!moo"], "sound": "moo.mp3"}, + {"names": ["!rape"], "sound": "rape.mp3"}, + {"names": ["!tada"], "sound": "tada.mp3"}, + {"names": ["!yay"], "sound": "yay.mp3"}, + {"names": ["!cyka"], "sound": "cyka.mp3"}, + {"names": ["!racist"], "sound": "racist.mp3"}, + {"names": ["!roger"], "sound": "roger.mp3"}, + {"names": ["!tooslow"], "sound": "tooslow.mp3"}, + {"names": ["!steam"], "sound": "steam.wav"}, + {"names": ["!good"], "sound": "that_s_pretty_good.mp3"}, + {"names": ["!crawling"], "sound": "crawling.mp3"}, + {"names": ["!nukyun"], "sound": ["nukyun1.mp3", "nukyun2.mp3", "nukyun3.mp3", "nukyun4.mp3", "nukyun5.mp3", "nukyun6.mp3", "nukyun7.mp3", "nukyun8.mp3", "nukyun9.mp3", "nukyun10.mp3", "nukyun11.mp3", "nukyun12.mp3", "nukyun13.mp3", "nukyun14.mp3", "nukyun15.mp3", "nukyun16.mp3", "nukyun17.mp3", "nukyun18.mp3", "nukyun19.mp3", "nukyun20.mp3", "nukyun21.mp3", "nukyun22.mp3", "nukyun23.mp3", "nukyun24.mp3", "nukyun25.mp3", "nukyun26.mp3", "nukyun27.mp3"]}, + {"names": ["!harambe"], "sound": "harambe.mp3"}, + {"names": ["!horn"], "sound": "vu_horn_quick.wav"}, + {"names": ["!hood"], "sound": "hood.mp3"}, + {"names": ["!gtfo"], "sound": ["gtfo.mp3", "gtfo2.mp3"]}, + {"names": ["!pomf"], "sound": "pomf.mp3"}, + {"names": ["!gay"], "sound": "putingay.mp3"}, + {"names": ["!pedo"], "sound": "pedobear.mp3"}, + {"names": ["!kys"], "sound": "kys.mp3"}, + {"names": ["!english"], "sound": "englishonly.mp3"}, + {"names": ["!knowledge"], "sound": "Knowledge.m4a"}, + {"names": ["!mana"], "sound": "mana.mp3"}, + {"names": ["!dodge"], "sound": ["dodge1.mp3", "dodge2.mp3"]}, + {"names": ["!love"], "sound": "sheep.mp3"}, + {"names": ["!timotei"], "sound": "Timotei.wav"}, + {"names": ["!daniel"], "sound": "daniel.mp3"}, + {"names": ["!cne"], "sound": ["it_is_may.mp3", "cnelaugh.mp3"]}, + {"names": ["!avocados"], "sound": "avocados.mp3"}, + {"names": ["!tutu", "!papa"], "sound": "papatutuwawa.mp3"}, + {"names": ["!nani"], "sound": "nani.mp3"}, + {"names": ["!squee"], "sound": "squee.mp3"}, + {"names": ["!ptb"], "sound": "07ptb.wav"}, + {"names": ["!wall"], "sound": "wall.mp3"}, + {"names": ["!bomb"], "sound": "bomb.mp3"}, + {"names": ["!wrong"], "sound": "wrong.mp3"}, + {"names": ["!china"], "sound": ["china1.mp3", "china2.mp3", "china3.mp3", "china4.mp3", "china5.mp3", "china6.mp3", "china7.mp3"]}, + {"names": ["!oof"], "sound": "oof.mp3"}, + {"names": ["!pan"], "sound": "panpakapan.mp3"}, + {"names": ["!shoulder"], "sound": "oh_my_shoulder.mp3"}, + {"names": ["!kizuna"], "sound": ["kizuna_fucku.mp3", "kizuna_fucku2.mp3", "kizuna_omg.mp3"]}, + {"names": ["!zegawa"], "sound": ["moaning_leBuP3p.mp3","1caae40.mp3","ara_ara.mp3","bndarling.mp3","watashi_hotto_jaate.mp3"]}, + {"names": ["!screenshot"], "sound": "camera1.wav"}, + {"names": ["!badro"], "sound": "vip.mp3"}, + {"names": ["!omarlaser"], "sound": "omarlaser.mp3"}, + {"names": ["!omarlenny"], "sound": "omarlenny.mp3"}, + {"names": ["!silent"], "sound": ["silent_scream.mp3" , "silent_laught.mp3"]}, + {"names": ["!willy"], "sound": "rippr_lotr_plz.mp3"}, + {"names": ["!anone"], "sound": "anone.mp3"}, + {"names": ["!jayz"], "sound": "jayz.wav"}, + {"names": ["!succ"], "sound": "succ.mp3"}, + {"names": ["!uguu"], "sound": "uguu_1.mp3"}, + {"names": ["!doot"], "sound": "skullsound2.mp3"}, + {"names": ["!widow"], "sound": "widowmaker_-_americans-1.mp3"}, + {"names": ["!winston"], "sound": "i-wanna-be-winston.mp3"}, + {"names": ["!try"], "sound": "try.mp3"}, + {"names": ["!smug"], "sound": "smug.mp3"}, + {"names": ["!skullz_tw"], "sound": "tw_skullzrage.mp3"}, + {"names": ["!genesis"], "sound": "genesis.mp3"}, + {"names": ["!borealis"], "sound": ["borealis1.mp3", "borealis2.mp3"]}, + {"names": ["!clams"], "sound": ["clams1.mp3", "clams2.mp3", "clams3.mp3"]}, + {"names": ["!hams"], "sound": ["hams1.mp3", "hams2.mp3"]}, + {"names": ["!expression"], "sound": "expression.mp3"}, + {"names": ["!albany"], "sound": "expression.mp3"}, + {"names": ["!steamedyes"], "sound": "steamedyes.mp3"}, + {"names": ["!steamedno"], "sound": "steamedno.mp3"}, + {"names": ["!luncheon"], "sound": "luncheon.mp3"}, + {"names": ["!devilish"], "sound": "devilish.mp3"}, + {"names": ["!roast"], "sound": "roast.mp3"}, + {"names": ["!goodlord"], "sound": "lord_unloze.mp3"}, + {"names": ["!time"], "sound": "time.mp3"}, + {"names": ["!dialect"], "sound": "dialect.mp3"}, + {"names": ["!attention"], "sound": "attention.mp3"}, + {"names": ["!great"], "sound": "great.mp3"}, + {"names": ["!epicsolo"], "sound": "epicsolo.mp3"}, + {"names": ["!everyonertv"], "sound": "everyonertv.mp3"}, + {"names": ["!esk"], "sound": "esk.mp3"}, + {"names": ["!epc"], "sound": "epc.mp3"}, + {"names": ["!ges"], "sound": "ges.mp3"}, + {"names": ["!eze"], "sound": "eze.mp3"}, + {"names": ["!eod"], "sound": "eod.mp3"}, + {"names": ["!tce"], "sound": "tce.mp3"}, + {"names": ["!trouble"], "sound": "trouble.mp3"}, + {"names": ["!teamwin"], "sound": "teamwin.mp3"}, + {"names": ["!nide"], "sound": ["nide.mp3", "nide2.mp3"]}, + {"names": ["!heaven"], "sound": "heaven.mp3"}, + {"names": ["!unskilled"], "sound": "unskilled.mp3"}, + {"names": ["!russia"], "sound": "russia.mp3"}, + {"names": ["!gg"], "sound": "gg.mp3"}, + {"names": ["!notrigger"], "sound": "notrig.wav"}, + {"names": ["!enemy"], "sound": ["enemee1.wav", "enemee2.wav", "enemee3.wav", "enemee4.wav", "enemee5.wav"]}, + {"names": ["!glacius"], "sound": "Glacius.m4a"}, + {"names": ["!ayaya"], "sound": ["ayayakaren1.mp3", "ayayakaren2.mp3"]}, + {"names": ["!dogan"], "sound": "dogan_im_going_to_fucking_fuck_you.mp3"}, + {"names": ["!destroy"], "sound": "dickstodestroy.mp3"}, + {"names": ["!boi"], "sound": "ainsley_harriott_and_his_spicy_meatconverttoaudio.mp3"}, + {"names": ["!gunga", "!ginga"], "sound": "gunga_ginga.mp3"}, + {"names": ["!leaders"], "sound": "leaders.mp3"}, + {"names": ["!never"], "sound": "never.mp3"}, + {"names": ["!hl1", "!blackmesa"], "sound": ["hl1_whatyoudoing.wav", "hl1_otis_talkmuch.wav", "hl1_pain3.wav", "hl1_otis_virgin.wav", "hl1_pain4.wav", "hl1_otis_die.wav", "hl1_pain1.wav", "hl1_otis_mom.wav", "hl1_pain2.wav", "hl1_scream05.wav", "hl1_iwounded.wav", "hl1_fear12.wav", "hl1_fear14.wav", "hl1_fear15.wav", "hl1_fear6.wav", "hl1_fear7.wav", "hl1_fear8.wav", "hl1_fear11.wav", "hl1_scream07.wav", "hl1_scream01.wav"]}, + {"names": ["!lügen"], "sound": "lügen.mp3"}, + {"names": ["!lachen"], "sound": "lachen.mp3"}, + {"names": ["!incoming"], "sound": "phalanx_incoming.mp3"}, + {"names": ["!address"], "sound": "humanz_send_me_your_address.wav"}, + {"names": ["!abuse"], "sound": "humanz_abuse.wav"}, + {"names": ["!killme"], "sound": "doit_comeon_killme.mp3"}, + {"names": ["!sickening"], "sound": "sickening.mp3"}, + {"names": ["!disgusting"], "sound": "disgusting_scuffed.mp3"}, + {"names": ["!icq"], "sound": "icq_old_sound.wav"}, + {"names": ["!discord"], "sound": "discord-notification.mp3"}, + {"names": ["!law", "!order"], "sound": "Law_&_Order_Sound.mp3"}, + {"names": ["!deutschland"], "sound": "VorUnsLiegtDeutschland.mp3"}, + {"names": ["!qwerpi"], "sound": ["qwerpi_aotsu/answer_the_question1.mp3", "qwerpi_aotsu/answer_the_question2.mp3", "qwerpi_aotsu/answer_the_question3.mp3", "qwerpi_aotsu/answer_the_question4.mp3", "qwerpi_aotsu/answer_the_question5.mp3", "qwerpi_aotsu/answer_the_question6.mp3", "qwerpi_aotsu/answer_the_question7.mp3", "qwerpi_aotsu/aotsuki_autism.mp3", "qwerpi_aotsu/aotsuki_come_on_stop.mp3", "qwerpi_aotsu/aotsuki_kouya.mp3", "qwerpi_aotsu/aotsuki_pls.mp3", "qwerpi_aotsu/aotsukitsukitsuki1.mp3", "qwerpi_aotsu/aotsukitsukitsuki2.mp3", "qwerpi_aotsu/aotsukyaotsukyaotsuky1.mp3", "qwerpi_aotsu/aotsukyaotsukyaotsuky2.mp3", "qwerpi_aotsu/can_you_pls_tell_me_your_opinion_takeshima.mp3", "qwerpi_aotsu/come_on_aotsukyyy.mp3", "qwerpi_aotsu/faking_faking_faking.mp3", "qwerpi_aotsu/i_changed_my_name_he_can_hear_me.mp3", "qwerpi_aotsu/japan_or_america1.mp3", "qwerpi_aotsu/japan_or_america10.mp3", "qwerpi_aotsu/japan_or_america11.mp3", "qwerpi_aotsu/japan_or_america12.mp3", "qwerpi_aotsu/japan_or_america2.mp3", "qwerpi_aotsu/japan_or_america3.mp3", "qwerpi_aotsu/japan_or_america4.mp3", "qwerpi_aotsu/japan_or_america5.mp3", "qwerpi_aotsu/japan_or_america6.mp3", "qwerpi_aotsu/japan_or_america7.mp3", "qwerpi_aotsu/japan_or_america8.mp3", "qwerpi_aotsu/japan_or_america9.mp3", "qwerpi_aotsu/onegaishimasu.mp3", "qwerpi_aotsu/pls_win_round_aotsuki.mp3", "qwerpi_aotsu/takeshima1.mp3", "qwerpi_aotsu/takeshima2.mp3", "qwerpi_aotsu/takeshima3.mp3", "qwerpi_aotsu/takeshima4.mp3", "qwerpi_aotsu/takeshima5.mp3", "qwerpi_aotsu/takeshima6.mp3", "qwerpi_aotsu/takeshima7.mp3", "qwerpi_aotsu/unnamed.mp3", "qwerpi_aotsu/why_do_you_do_this_to_me.mp3", "qwerpi_aotsu/why_dont_you_just_answer1.mp3", "qwerpi_aotsu/why_dont_you_just_answer2.mp3", "qwerpi_aotsu/why_dont_you_just_answer3.mp3", "qwerpi_aotsu/why_dont_you_just_answer4.mp3", "qwerpi_aotsu/why_dont_you_just_answer5.mp3", "qwerpi_aotsu/why_dont_you_just_answer6.mp3", "qwerpi_aotsu/you_not_japanese1.mp3", "qwerpi_aotsu/you_not_japanese2.mp3", "qwerpi_aotsu/you_not_japanese3.mp3", "qwerpi_aotsu/you_will_never_get_rid_of_me_aotsuki.mp3"]}, + {"names": ["!panic"], "sound": "greta_panic.mp3"}, + {"names": ["!gachi"], "sound": ["gachi/300.mp3", "gachi/ah.mp3", "gachi/amazing.mp3", "gachi/anal.mp3", "gachi/artist.mp3", "gachi/asswecan.mp3", "gachi/attention.mp3", "gachi/bigger.mp3", "gachi/boss.mp3", "gachi/boynextdoor.mp3", "gachi/collegeboy.mp3", "gachi/comeon.mp3", "gachi/daddy.mp3", "gachi/deep.mp3", "gachi/door.mp3", "gachi/doyoulike.mp3", "gachi/dungeon.mp3", "gachi/embarrassing.mp3", "gachi/fantasies.mp3", "gachi/fuckyou.mp3", "gachi/fuckyou2.mp3", "gachi/fuckyou3.mp3", "gachi/gangingup.mp3", "gachi/happy.mp3", "gachi/interruption.mp3", "gachi/jabroni.mp3", "gachi/lash.mp3", "gachi/likethat.mp3", "gachi/loads.mp3", "gachi/lube.mp3", "gachi/mmh.mp3", "gachi/pants.mp3", "gachi/power.mp3", "gachi/rip.mp3", "gachi/slaves.mp3", "gachi/sorry.mp3", "gachi/sorry2.mp3", "gachi/spank.mp3", "gachi/suction.mp3", "gachi/takeit.mp3", "gachi/website.mp3", "gachi/woo.mp3", "gachi/wth.mp3"]}, + {"names": ["!ali", "!alia"], "sound": "AliA.mp3"}, + {"names": ["!dropit"], "sound": "dropit.mp3"}, + {"names": ["!murica"], "sound": "MURICA.mp3"}, + {"names": ["!ocean", "!oceanman"], "sound": "oceanman.mp3"}, + {"names": ["!thanos"], "sound": "thanos_snaps_fingers_sfx.mp3"}, + {"names": ["!greta"], "sound": ["greta_1.mp3", "greta_2.mp3", "greta_3.mp3"]}, + {"names": ["!playb"], "sound": ["PlayB_-_back_back_back.mp3", "PlayB_-_back_doorhug.mp3", "PlayB_-_dont_stay_fkng_nubs.mp3", "PlayB_-_not_FF"]}, + {"names": ["!alarm"], "sound": "iphone_alarm.mp3"}, + {"names": ["!iphone", "!ringtone"], "sound": "iphone_ringtone.mp3"}, + {"names": ["!gman"], "sound": ["gman_choose1.wav", "gman_mono34.wav", "Gman_wellseeaboutthat.ogg", "gman_choose1.wav", "gman_choose2.wav", "gman_wisely_done.wav", "gman_wisely_done_freeman.wav", "gman_well_shit.wav"]}, + {"names": ["!steamedjenz"], "sound": "jenz_yes.mp3"}, + {"names": ["!sinaknife"], "sound": "sinaknife.mp3"}, + {"names": ["!sans"], "sound": ["undertale_sans.mp3", "megalovania_1.mp3", "megalovania_2.mp3"]}, + {"names": ["!what", "!youwhat"], "sound": "you-what-spongebob.mp3"}, + {"names": ["!pufferfish", "!augh"], "sound": "pufferfish_augh.mp3"}, + {"names": ["!gotcha"], "sound": "Gotcha Bitch2.mp3"}, + {"names": ["!palpatine"], "sound": "Palp_doit.mp3"}, + {"names": ["!chosenone"], "sound": "ChosenOne.mp3"}, + {"names": ["!anakinfun"], "sound": "AnakinFun.mp3"}, + {"names": ["!anakinliar"], "sound": "AnakinLiar.mp3"}, + {"names": ["!hellothere"], "sound": "HelloThere.mp3"}, + {"names": ["!because"], "sound": "BecauseObiWan.mp3"}, + {"names": ["!outofhand"], "sound": "OutofHand.mp3"}, + {"names": ["!problem"], "sound": "problem.mp3"}, + {"names": ["!pizza"], "sound": ["pizza.mp3", "pizza2.mp3"]}, + {"names": ["!train"], "sound": ["train.mp3", "train2.mp3", "train4.mp3"]}, + {"names": ["!screech"], "sound": "screech.mp3"}, + {"names": ["!simp"], "sound": "simp.mp3"}, + {"names": ["!tadadada"], "sound": "tadadada.mp3"}, + {"names": ["!kroaat"], "sound": "kroaat_rage.mp3"}, + {"names": ["!passed"], "sound": "GTA San Andreas - Mission passed sound.mp3"}, + {"names": ["!ahshit"], "sound": "GTA San Andreas - Ah shit, here we go again.mp3"}, + {"names": ["!no"], "sound": "nononojojo.mp3"}, + + {"names": ["_amigos"], "sound": "10-60/amigos.mp3"}, + {"names": ["_sad"], "sound": "10-60/2sad4me2.mp3"}, + {"names": ["_minasplease"], "sound": "10-60/MINASPLEASESONG.mp3"}, + {"names": ["_jobs"], "sound": "10-60/Jobs.mp3"}, + {"names": ["_praise"], "sound": "10-60/Praise-The-Sun-420Yolo2.mp3"}, + {"names": ["_brainpower"], "sound": "10-60/Brain Power V2.mp3"}, + {"names": ["_cotton"], "sound": "10-60/Cotton Eye Akbar.mp3"}, + {"names": ["_suckmycock", "_stopshooting"], "sound": "10-60/meshlem_arrive.mp3"}, + {"names": ["_hacker"], "sound": "10-60/Meshlem_die_good.mp3"}, + {"names": ["_how", "_happen"], "sound": "10-60/how_could_this_happen_to_me.mp3"}, + {"names": ["_tuturemix"], "sound": "10-60/tuturemix.mp3"}, + {"names": ["_pirate"], "sound": "10-60/pirate.wav"}, + {"names": ["_legend"], "sound": "10-60/legend.mp3"}, + {"names": ["_pirates"], "sound": "10-60/The - best.mp3"}, + {"names": ["_pegboard"], "sound": "10-60/Pendulum - Witchcraft Pegboard.mp3"}, + {"names": ["_saxoo"], "sound": "10-60/saxo.mp3"}, + {"names": ["_no"], "sound": "10-60/NOOOOOOOOOOO.wav"}, + {"names": ["_crabrave"], "sound": "10-60/crabrave.mp3"}, + {"names": ["_takemeon"], "sound": "10-60/a-ha-take-on-me-cut-mp3.mp3"}, + {"names": ["_shake"], "sound": "10-60/ShakeRockstar.ogg"}, + {"names": ["_hitormiss"], "sound": "10-60/hom_sans.mp3"}, + {"names": ["_johnny"], "sound": "10-60/Johnny Daepp.mp3"}, + {"names": ["_theo"], "sound": "10-60/theobald_lower.mp3"}, + {"names": ["_chinamotor"], "sound": "10-60/chinesemotorcycle.mp3"}, + {"names": ["_ftarflute"], "sound": "10-60/pend_taran_flute.mp3"}, + {"names": ["_neon_remix"], "sound": "10-60/neon_remix.mp3"}, + {"names": ["_lickies"], "sound": "10-60/lickies.mp3"}, + {"names": ["_jenzlem"], "sound": "10-60/jenzlem.m4a"}, + {"names": ["_playb"], "sound": "10-60/DontfallbackdefendPlayB.mp3"}, + {"names": ["_awaken"], "sound": "10-60/awaken.mp3"}, + {"names": ["_dudu"], "sound": "10-60/dudu.mp3"}, + {"names": ["_ice"], "sound": "10-60/levshoot.mp3"}, + {"names": ["_gunga", "_ginga"], "sound": "10-60/gunga_ginga.mp3"}, + {"names": ["_makoplease"], "sound": "10-60/makoplease.mp3"}, + {"names": ["_willrip"], "sound": "10-60/fullwilly.mp3"}, + {"names": ["_rage"], "sound": "tense1983_rage.mp3"}, + {"names": ["_france"], "sound": "france.mp3"}, + {"names": ["_cne"], "sound": "Cne_goes_apocalypse_mode.mp3"}, + + {"names": ["#tempest"], "sound": "60+/tempest.wav"}, + {"names": ["#pendulum_knife"], "sound": "60+/Pendulum_KnifeParty.mp3"}, + {"names": ["#nostar"], "sound": "60+/minassong.mp3"}, + {"names": ["#pika"], "sound": "60+/pika.wav"}, + {"names": ["#tripoloski"], "sound": "60+/tripoloski.mp3"}, + {"names": ["#tunak"], "sound": "60+/tunak.mp3"}, + {"names": ["#crabman"], "sound": "60+/CrabMan.ogg"}, + {"names": ["#trump"], "sound": "Donald_Trump_-_Shooting_Stars.mp3"}, + {"names": ["#tri_remix"], "sound": "60+/tree_poloskee.mp3"} +]