From d94fe1204f5cfd8a9b36589c6f054f04236b8cf4 Mon Sep 17 00:00:00 2001 From: BotoX Date: Mon, 30 Dec 2019 17:43:48 +0100 Subject: [PATCH] update --- Torchlight/AccessManager.py | 0 Torchlight/AsyncClient.py | 2 - Torchlight/AudioManager.py | 97 ++++++- Torchlight/CommandHandler.py | 0 Torchlight/Commands.py | 454 ++++++++++++++++++++------------ Torchlight/Config.py | 7 +- Torchlight/Constants.py | 0 Torchlight/FFmpegAudioPlayer.py | 4 +- Torchlight/GameEvents.py | 1 + Torchlight/PlayerManager.py | 21 +- Torchlight/SourceModAPI.py | 0 Torchlight/SourceRCONServer.py | 0 Torchlight/Subscribe.py | 159 +++++++++++ Torchlight/Torchlight.py | 28 +- Torchlight/Utils.py | 0 Torchlight/__init__.py | 0 access.json | 2 +- config.json | 9 +- main.py | 6 +- 19 files changed, 594 insertions(+), 196 deletions(-) mode change 100644 => 100755 Torchlight/AccessManager.py mode change 100644 => 100755 Torchlight/AsyncClient.py mode change 100644 => 100755 Torchlight/AudioManager.py mode change 100644 => 100755 Torchlight/CommandHandler.py mode change 100644 => 100755 Torchlight/Commands.py mode change 100644 => 100755 Torchlight/Config.py mode change 100644 => 100755 Torchlight/Constants.py mode change 100644 => 100755 Torchlight/FFmpegAudioPlayer.py mode change 100644 => 100755 Torchlight/PlayerManager.py mode change 100644 => 100755 Torchlight/SourceModAPI.py mode change 100644 => 100755 Torchlight/SourceRCONServer.py create mode 100755 Torchlight/Subscribe.py mode change 100644 => 100755 Torchlight/Torchlight.py mode change 100644 => 100755 Torchlight/Utils.py mode change 100644 => 100755 Torchlight/__init__.py diff --git a/Torchlight/AccessManager.py b/Torchlight/AccessManager.py old mode 100644 new mode 100755 diff --git a/Torchlight/AsyncClient.py b/Torchlight/AsyncClient.py old mode 100644 new mode 100755 index f245497..7461b18 --- a/Torchlight/AsyncClient.py +++ b/Torchlight/AsyncClient.py @@ -61,7 +61,6 @@ class AsyncClient(): def OnReceive(self, data): Obj = json.loads(data) - print(Obj) if "method" in Obj and Obj["method"] == "publish": self.Master.OnPublish(Obj) @@ -80,7 +79,6 @@ class AsyncClient(): return None Data = json.dumps(obj, ensure_ascii = False, separators = (',', ':')).encode("UTF-8") - print(obj) with (await self.SendLock): if not self.Protocol: diff --git a/Torchlight/AudioManager.py b/Torchlight/AudioManager.py old mode 100644 new mode 100755 index ca5ddd7..c7f7b16 --- a/Torchlight/AudioManager.py +++ b/Torchlight/AudioManager.py @@ -23,6 +23,7 @@ class AudioPlayerFactory(): if _type == self.AUDIOPLAYER_FFMPEG: return self.FFmpegAudioPlayerFactory.NewPlayer() + class AntiSpam(): def __init__(self, master): self.Logger = logging.getLogger(__class__.__name__) @@ -31,6 +32,7 @@ class AntiSpam(): 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 \ @@ -42,10 +44,7 @@ class AntiSpam(): return True - def RegisterClip(self, clip): - self.LastClips[hash(clip)] = dict({"timestamp": None, "duration": 0.0, "dominant": False, "active": True}) - - def SpamCheck(self): + def SpamCheck(self, Delta): Now = self.Torchlight().Master.Loop.time() Duration = 0.0 @@ -53,7 +52,7 @@ class AntiSpam(): if not Clip["timestamp"]: continue - if Clip["timestamp"] + self.Torchlight().Config["AntiSpam"]["MaxUsageSpan"] < Now: + if Clip["timestamp"] + Clip["duration"] + self.Torchlight().Config["AntiSpam"]["MaxUsageSpan"] < Now: if not Clip["active"]: del self.LastClips[Key] continue @@ -73,7 +72,8 @@ class AntiSpam(): self.LastClips.clear() def OnPlay(self, clip): - self.LastClips[hash(clip)]["timestamp"] = self.Torchlight().Master.Loop.time() + 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(): @@ -84,6 +84,9 @@ class AntiSpam(): 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"]: @@ -102,7 +105,79 @@ class AntiSpam(): return Clip["duration"] += Delta - self.SpamCheck() + 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\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(): @@ -110,6 +185,7 @@ class AudioManager(): self.Logger = logging.getLogger(__class__.__name__) self.Torchlight = torchlight self.AntiSpam = AntiSpam(self) + self.Advertiser = Advertiser(self) self.AudioPlayerFactory = AudioPlayerFactory(self) self.AudioClips = [] @@ -153,7 +229,7 @@ class AudioManager(): if extra and not extra.lower() in AudioClip.Player.Name.lower(): continue - if not Level or Level < AudioClip.Level: + if not Level or (Level < AudioClip.Level and Level < self.Torchlight().Config["AntiSpam"]["StopLevel"]): AudioClip.Stops.add(player.UserID) if len(AudioClip.Stops) >= 3: @@ -188,11 +264,14 @@ class AudioManager(): self.AudioClips.append(Clip) if not player.Access or player.Access["level"] < self.Torchlight().Config["AntiSpam"]["ImmunityLevel"]: - self.AntiSpam.RegisterClip(Clip) Clip.AudioPlayer.AddCallback("Play", lambda *args: self.AntiSpam.OnPlay(Clip, *args)) Clip.AudioPlayer.AddCallback("Stop", lambda *args: self.AntiSpam.OnStop(Clip, *args)) Clip.AudioPlayer.AddCallback("Update", lambda *args: self.AntiSpam.OnUpdate(Clip, *args)) + 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): diff --git a/Torchlight/CommandHandler.py b/Torchlight/CommandHandler.py old mode 100644 new mode 100755 diff --git a/Torchlight/Commands.py b/Torchlight/Commands.py old mode 100644 new mode 100755 index b3c5896..0aa3c47 --- a/Torchlight/Commands.py +++ b/Torchlight/Commands.py @@ -16,10 +16,26 @@ class BaseCommand(): 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) -### FILTER COMMANDS ### + class URLFilter(BaseCommand): Order = 1 import re @@ -49,11 +65,16 @@ class URLFilter(BaseCommand): if TimeStr: Time = Utils.ParseTime(TimeStr) - Proc = await asyncio.create_subprocess_exec("youtube-dl", "--dump-json", "-xg", url, + Proc = await asyncio.create_subprocess_exec("youtube-dl", "--dump-json", "-g", url, stdout = asyncio.subprocess.PIPE) Out, _ = await Proc.communicate() - url, Info = Out.split(b'\n', maxsplit = 1) + 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) @@ -119,49 +140,62 @@ class URLFilter(BaseCommand): asyncio.ensure_future(self.URLInfo(Url)) return -1 -### FILTER COMMANDS ### -### LEVEL 0 COMMANDS ### +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"] #, "!who", "!whois"] + self.Triggers = ["!access"] self.Level = 0 - def FormatAccess(self, player): - Answer = "#{0} \"{1}\"({2}) is ".format(player.UserID, player.Name, player.UniqueID) - Level = str(0) - if player.Access: - Level = str(player.Access["level"]) - Answer += "level {0!s} as {1}.".format(Level, player.Access["name"]) - else: - Answer += "not authenticated." - - if Level in self.Torchlight().Config["AudioLimits"]: - Uses = self.Torchlight().Config["AudioLimits"][Level]["Uses"] - TotalTime = self.Torchlight().Config["AudioLimits"][Level]["TotalTime"] - - if Uses >= 0: - Answer += " Uses: {0}/{1}".format(player.Storage["Audio"]["Uses"], Uses) - if TotalTime >= 0: - Answer += " Time: {0}/{1}".format(round(player.Storage["Audio"]["TimeUsed"], 2), round(TotalTime, 2)) - - return Answer - async def _func(self, message, player): self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + if self.check_chat_cooldown(player): + return -1 + Count = 0 if message[0] == "!access": if message[1]: return -1 - self.Torchlight().SayChat(self.FormatAccess(player)) + self.Torchlight().SayChat(FormatAccess(self.Torchlight, player), player) - elif message[0] == "!who": + 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(self.FormatAccess(Player)) + self.Torchlight().SayChat(FormatAccess(self.Torchlight, Player)) Count += 1 if Count >= 3: @@ -172,7 +206,7 @@ class Access(BaseCommand): if Access["name"].lower().find(message[1].lower()) != -1: Player = self.Torchlight().Players.FindUniqueID(UniqueID) if Player: - self.Torchlight().SayChat(self.FormatAccess(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"])) @@ -181,34 +215,6 @@ class Access(BaseCommand): break return 0 -class Calculate(BaseCommand): - import urllib.parse - import aiohttp - import json - def __init__(self, torchlight): - super().__init__(torchlight) - self.Triggers = ["!c"] - self.Level = 0 - - async def Calculate(self, Params): - async with self.aiohttp.ClientSession() as session: - Response = await asyncio.wait_for(session.get("http://math.leftforliving.com/query", params=Params), 5) - if not Response: - return 1 - - Data = await asyncio.wait_for(Response.json(content_type = "text/json"), 5) - if not Data: - return 2 - - if not Data["error"]: - self.Torchlight().SayChat(Data["answer"]) - return 0 - - async def _func(self, message, player): - self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) - Params = dict({"question": message[1]}) - Ret = await self.Calculate(Params) - return Ret class WolframAlpha(BaseCommand): import urllib.parse @@ -218,12 +224,12 @@ class WolframAlpha(BaseCommand): def __init__(self, torchlight): super().__init__(torchlight) self.Triggers = ["!cc"] - self.Level = 0 + self.Level = 3 def Clean(self, Text): return self.re.sub("[ ]{2,}", " ", Text.replace(' | ', ': ').replace('\n', ' | ').replace('~~', ' ≈ ')).strip() - async def Calculate(self, Params): + 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: @@ -246,7 +252,7 @@ class WolframAlpha(BaseCommand): # 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.") + self.Torchlight().SayChat("Sorry, couldn't understand the question.", player) return 3 Options = [] @@ -254,39 +260,128 @@ class WolframAlpha(BaseCommand): Options.append("\"{0}\"".format(Didyoumean.text)) Line = " or ".join(Options) Line = "Did you mean {0}?".format(Line) - self.Torchlight().SayChat(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) + 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)) + 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)) - Level = 0 - if player.Access: - Level = player.Access["level"] + if self.check_chat_cooldown(player): + return -1 - Disabled = self.Torchlight().Disabled - if Disabled and (Disabled > Level or Disabled == Level and Level < self.Torchlight().Config["AntiSpam"]["ImmunityLevel"]): - self.Torchlight().SayPrivate(player, "Torchlight is currently disabled!") - return 1 + if self.check_disabled(player): + return -1 Params = dict({"input": message[1], "appid": self.Torchlight().Config["WolframAPIKey"]}) - Ret = await self.Calculate(Params) + 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 = 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 + + 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 = 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 + + 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): @@ -352,6 +447,7 @@ class WUnderground(BaseCommand): Observation["relative_humidity"])) return 0 +''' class VoteDisable(BaseCommand): def __init__(self, torchlight): @@ -361,6 +457,7 @@ class VoteDisable(BaseCommand): 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 @@ -375,70 +472,118 @@ class VoteDisable(BaseCommand): else: self.Torchlight().SayPrivate(player, "Torchlight needs {0} more disable votes to be disabled.".format(needed - have)) -### LEVEL 0 COMMANDS ### -### LIMITED LEVEL 0 COMMANDS ### class VoiceCommands(BaseCommand): import json import random def __init__(self, torchlight): super().__init__(torchlight) - self.Triggers = ["!random"] + self.Triggers = ["!random", "!search"] self.Level = 0 def LoadTriggers(self): try: with open("triggers.json", "r") as fp: - self.VoiceTriggers = self.json.load(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 Triggers in self.VoiceTriggers: - for Trigger in Triggers["names"]: - self.Triggers.append(Trigger) + 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"] - 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!") + message[0] = message[0].lower() + message[1] = message[1].lower() + if message[0][0] != '!' and Level < 2: return 1 - 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].lower() == "!random": - Trigger = self.random.choice(self.VoiceTriggers) - if isinstance(Trigger["sound"], list): - Sound = self.random.choice(Trigger["sound"]) + if message[0] == "!random": + Trigger = self.random.choice(list(self.VoiceTriggers.values())) + if isinstance(Trigger, list): + Sound = self.random.choice(Trigger) else: - Sound = Trigger["sound"] + Sound = Trigger else: - for Trigger in self.VoiceTriggers: - for Name in Trigger["names"]: - if message[0].lower() == Name: - Num = Utils.GetNum(message[1]) - if Num: - Num = int(Num) + Sounds = self.VoiceTriggers[message[0]] - if isinstance(Trigger["sound"], list): - if Num and Num > 0 and Num <= len(Trigger["sound"]): - Sound = Trigger["sound"][Num - 1] - else: - Sound = self.random.choice(Trigger["sound"]) - else: - Sound = Trigger["sound"] + try: + Num = int(message[1]) + except ValueError: + Num = None - break + 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) @@ -447,23 +592,21 @@ class VoiceCommands(BaseCommand): return AudioClip.Play() + class YouTube(BaseCommand): def __init__(self, torchlight): super().__init__(torchlight) self.Triggers = ["!yt"] - self.Level = 2 + self.Level = 3 async def _func(self, message, player): self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) - Level = 0 - if player.Access: - Level = player.Access["level"] + if self.check_disabled(player): + return -1 - Disabled = self.Torchlight().Disabled - if Disabled and (Disabled > Level or Disabled == Level and Level < self.Torchlight().Config["AntiSpam"]["ImmunityLevel"]): - self.Torchlight().SayPrivate(player, "Torchlight is currently disabled!") - return 1 + if self.Torchlight().LastUrl: + message[1] = message[1].replace("!last", self.Torchlight().LastUrl) Temp = DataHolder() Time = None @@ -485,19 +628,13 @@ class YouTubeSearch(BaseCommand): def __init__(self, torchlight): super().__init__(torchlight) self.Triggers = ["!yts"] - self.Level = 2 + self.Level = 3 async def _func(self, message, player): self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) - Level = 0 - if player.Access: - Level = player.Access["level"] - - Disabled = self.Torchlight().Disabled - if Disabled and (Disabled > Level or Disabled == Level and Level < self.Torchlight().Config["AntiSpam"]["ImmunityLevel"]): - self.Torchlight().SayPrivate(player, "Torchlight is currently disabled!") - return 1 + if self.check_disabled(player): + return -1 Temp = DataHolder() Time = None @@ -518,7 +655,7 @@ class YouTubeSearch(BaseCommand): 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"]))) + 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: @@ -528,6 +665,7 @@ class YouTubeSearch(BaseCommand): return AudioClip.Play(Time) + class Say(BaseCommand): import gtts import tempfile @@ -535,7 +673,7 @@ class Say(BaseCommand): def __init__(self, torchlight): super().__init__(torchlight) self.Triggers = [("!say", 4)] - self.Level = 0 + self.Level = 2 async def Say(self, player, language, message): GTTS = self.gtts.gTTS(text = message, lang = language) @@ -559,14 +697,8 @@ class Say(BaseCommand): async def _func(self, message, player): self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) - Level = 0 - if player.Access: - Level = player.Access["level"] - - Disabled = self.Torchlight().Disabled - if Disabled and (Disabled > Level or Disabled == Level and Level < self.Torchlight().Config["AntiSpam"]["ImmunityLevel"]): - self.Torchlight().SayPrivate(player, "Torchlight is currently disabled!") - return 1 + if self.check_disabled(player): + return -1 if not message[1]: return 1 @@ -581,6 +713,7 @@ class Say(BaseCommand): asyncio.ensure_future(self.Say(player, Language, message[1])) return 0 +''' class DECTalk(BaseCommand): import tempfile def __init__(self, torchlight): @@ -612,20 +745,15 @@ class DECTalk(BaseCommand): async def _func(self, message, player): self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) - Level = 0 - if player.Access: - Level = player.Access["level"] - - Disabled = self.Torchlight().Disabled - if Disabled and (Disabled > Level or Disabled == Level and Level < self.Torchlight().Config["AntiSpam"]["ImmunityLevel"]): - self.Torchlight().SayPrivate(player, "Torchlight is currently disabled!") - return 1 + if self.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): @@ -639,18 +767,7 @@ class Stop(BaseCommand): self.Torchlight().AudioManager.Stop(player, message[1]) return True -### LIMITED LEVEL 0 COMMANDS ### - -### LEVEL 1 COMMANDS ### -### LEVEL 1 COMMANDS ### - - -### LEVEL 2 COMMANDS ### -### LEVEL 2 COMMANDS ### - - -### LEVEL 3 COMMANDS ### class EnableDisable(BaseCommand): def __init__(self, torchlight): super().__init__(torchlight) @@ -659,24 +776,24 @@ class EnableDisable(BaseCommand): 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.") + self.Torchlight().SayPrivate(player, "You don't have access to enable torchlight, since it was disabled by a higher level user.") return 1 self.Torchlight().SayChat("Torchlight has been enabled for the duration of this map - Type !disable to disable it again.") self.Torchlight().Disabled = False elif message[0] == "!disable": - if not self.Torchlight().Disabled: - self.Torchlight().SayChat("Torchlight has been disabled for the duration of this map - Type !enable to enable it again.") - + 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"] -### LEVEL 3 COMMANDS ### -### LEVEL 4 COMMANDS ### class AdminAccess(BaseCommand): from collections import OrderedDict def __init__(self, torchlight): @@ -782,15 +899,24 @@ class AdminAccess(BaseCommand): del self.Torchlight().Access[Player.UniqueID] Player.Access = None return 0 -### LEVEL 4 COMMANDS ### + +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 -### LEVEL X COMMANDS ### class Exec(BaseCommand): def __init__(self, torchlight): super().__init__(torchlight) self.Triggers = ["!exec"] - self.Level = 9 + self.Level = 100 async def _func(self, message, player): self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) @@ -801,15 +927,3 @@ class Exec(BaseCommand): return 1 self.Torchlight().SayChat(str(Response)) return 0 - -class Reload(BaseCommand): - def __init__(self, torchlight): - super().__init__(torchlight) - self.Triggers = ["!reload"] - self.Level = 6 - - async def _func(self, message, player): - self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) - self.Torchlight().Reload() - return 0 -### LEVEL X COMMANDS ### diff --git a/Torchlight/Config.py b/Torchlight/Config.py old mode 100644 new mode 100755 index dc0fbd7..620a4d8 --- a/Torchlight/Config.py +++ b/Torchlight/Config.py @@ -2,16 +2,21 @@ # -*- 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("config.json", "r") as fp: + 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)) diff --git a/Torchlight/Constants.py b/Torchlight/Constants.py old mode 100644 new mode 100755 diff --git a/Torchlight/FFmpegAudioPlayer.py b/Torchlight/FFmpegAudioPlayer.py old mode 100644 new mode 100755 index 8841777..e214315 --- a/Torchlight/FFmpegAudioPlayer.py +++ b/Torchlight/FFmpegAudioPlayer.py @@ -60,9 +60,9 @@ class FFmpegAudioPlayer(): 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", *args, "-"] + 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", *args, "-"] + Command = ["/usr/bin/ffmpeg", "-i", uri, "-acodec", "pcm_s16le", "-ac", "1", "-ar", str(int(self.SampleRate)), "-f", "s16le", "-vn", *args, "-"] print(Command) diff --git a/Torchlight/GameEvents.py b/Torchlight/GameEvents.py index 81aefea..7919e14 100644 --- a/Torchlight/GameEvents.py +++ b/Torchlight/GameEvents.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- import asyncio import logging +import traceback class GameEvents(): def __init__(self, master): diff --git a/Torchlight/PlayerManager.py b/Torchlight/PlayerManager.py old mode 100644 new mode 100755 index 033fd2e..7e000f8 --- a/Torchlight/PlayerManager.py +++ b/Torchlight/PlayerManager.py @@ -15,6 +15,7 @@ class PlayerManager(): 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) @@ -23,7 +24,8 @@ class PlayerManager(): index += 1 self.Logger.info("OnConnect(name={0}, index={1}, userid={2}, networkid={3}, address={4}, bot={5})" .format(name, index, userid, networkid, address, bot)) - assert self.Players[index] == None + 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() @@ -35,6 +37,11 @@ class PlayerManager(): 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})" @@ -157,6 +164,7 @@ class PlayerManager(): self.Admin = self.PlayerManager.Admin() self.Storage = None self.Active = False + self.ChatCooldown = 0 def OnConnect(self): self.Storage = self.PlayerManager.Storage[self.UniqueID] @@ -168,17 +176,22 @@ class PlayerManager(): def OnActivate(self): self.Active = True - asyncio.ensure_future(self.OnPostActivate()) - async def OnPostActivate(self): + 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.Generic(): + 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 diff --git a/Torchlight/SourceModAPI.py b/Torchlight/SourceModAPI.py old mode 100644 new mode 100755 diff --git a/Torchlight/SourceRCONServer.py b/Torchlight/SourceRCONServer.py old mode 100644 new mode 100755 diff --git a/Torchlight/Subscribe.py b/Torchlight/Subscribe.py new file mode 100755 index 0000000..8cd152c --- /dev/null +++ b/Torchlight/Subscribe.py @@ -0,0 +1,159 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import asyncio +import logging +import traceback + +class SubscribeBase(): + def __init__(self, master, module): + self.Logger = logging.getLogger(__class__.__name__) + self.Torchlight = master + self.Module = module + + self.Callbacks = {} + + def __del__(self): + if not len(self.Callbacks) or not self.Torchlight(): + return + + Obj = { + "method": "unsubscribe", + "module": self.Module, + "events": self.Callbacks.keys() + } + + asyncio.ensure_future(self.Torchlight().Send(Obj)) + + async def _Register(self, events): + if type(events) is not list: + events = [ events ] + + Obj = { + "method": "subscribe", + "module": self.Module, + "events": events + } + + Res = await self.Torchlight().Send(Obj) + + Ret = [] + for i, ret in enumerate(Res["events"]): + if ret >= 0: + Ret.append(True) + if not events[i] in self.Callbacks: + self.Callbacks[events[i]] = set() + else: + Ret.append(False) + + if len(Ret) == 1: + Ret = Ret[0] + return Ret + + async def _Unregister(self, events): + if type(events) is not list: + events = [ events ] + + Obj = { + "method": "unsubscribe", + "module": self.Module, + "events": events + } + + Res = await self.Torchlight().Send(Obj) + + Ret = [] + for i, ret in enumerate(Res["events"]): + if ret >= 0: + Ret.append(True) + if events[i] in self.Callbacks: + del self.Callbacks[events[i]] + else: + Ret.append(False) + + if len(Ret) == 1: + Ret = Ret[0] + return Ret + + def HookEx(self, event, callback): + asyncio.ensure_future(self.Hook(event, callback)) + + def UnhookEx(self, event, callback): + asyncio.ensure_future(self.Unhook(event, callback)) + + def ReplayEx(self, events): + asyncio.ensure_future(self.Replay(events)) + + async def Hook(self, event, callback): + if not event in self.Callbacks: + if not await self._Register(event): + return False + + self.Callbacks[event].add(callback) + return True + + async def Unhook(self, event, callback): + if not event in self.Callbacks: + return True + + if not callback in self.Callbacks[event]: + return True + + self.Callbacks[event].discard(callback) + + if len(a) == 0: + return await self._Unregister(event) + + return True + + async def Replay(self, events): + if type(events) is not list: + events = [ events ] + + for event in events[:]: + if not event in self.Callbacks: + events.remove(event) + + Obj = { + "method": "replay", + "module": self.Module, + "events": events + } + + Res = await self.Torchlight().Send(Obj) + + Ret = [] + for i, ret in enumerate(Res["events"]): + if ret >= 0: + Ret.append(True) + else: + Ret.append(False) + + if len(Ret) == 1: + Ret = Ret[0] + return Ret + + def OnPublish(self, obj): + Event = obj["event"] + + if not Event["name"] in self.Callbacks: + return False + + Callbacks = self.Callbacks[Event["name"]] + + for Callback in Callbacks: + try: + Callback(**Event["data"]) + except Exception as e: + self.Logger.error(traceback.format_exc()) + self.Logger.error(Event) + + return True + + +class GameEvents(SubscribeBase): + def __init__(self, master): + super().__init__(master, "gameevents") + +class Forwards(SubscribeBase): + def __init__(self, master): + super().__init__(master, "forwards") diff --git a/Torchlight/Torchlight.py b/Torchlight/Torchlight.py old mode 100644 new mode 100755 index 114b976..090d9e5 --- a/Torchlight/Torchlight.py +++ b/Torchlight/Torchlight.py @@ -12,7 +12,7 @@ import textwrap from .AsyncClient import AsyncClient from .SourceModAPI import SourceModAPI -from .GameEvents import GameEvents +from .Subscribe import GameEvents, Forwards from .Utils import Utils from .Config import Config @@ -30,6 +30,7 @@ class Torchlight(): self.API = SourceModAPI(self.WeakSelf) self.GameEvents = GameEvents(self.WeakSelf) + self.Forwards = Forwards(self.WeakSelf) self.DisableVotes = set() self.Disabled = 0 @@ -49,7 +50,7 @@ class Torchlight(): self.GameEvents.HookEx("server_spawn", self.Event_ServerSpawn) self.GameEvents.HookEx("player_say", self.Event_PlayerSay) - def SayChat(self, message): + def SayChat(self, message, player=None): message = "\x0700FFFA[Torchlight]: \x01{0}".format(message) if len(message) > 976: message = message[:973] + "..." @@ -57,8 +58,25 @@ class Torchlight(): 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): - asyncio.ensure_future(self.API.PrintToChat(player.Index, "\x0700FFFA[Torchlight]: \x01{0}".format(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() @@ -70,6 +88,8 @@ class Torchlight(): 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() @@ -105,11 +125,13 @@ class TorchlightHandler(): # 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) diff --git a/Torchlight/Utils.py b/Torchlight/Utils.py old mode 100644 new mode 100755 diff --git a/Torchlight/__init__.py b/Torchlight/__init__.py old mode 100644 new mode 100755 diff --git a/access.json b/access.json index 2119d42..668221d 100644 --- a/access.json +++ b/access.json @@ -1,6 +1,6 @@ { "[U:1:51174697]": { "name": "BotoX", - "level": 10 + "level": 100 } } \ No newline at end of file diff --git a/config.json b/config.json index 0df485e..c138e91 100644 --- a/config.json +++ b/config.json @@ -3,7 +3,7 @@ { "Host": "10.0.0.101", "Port": 27020, - "SampleRate": 48000 + "SampleRate": 22050 }, "SMAPIServer": { @@ -30,10 +30,12 @@ }, "AntiSpam": { + "ImmunityLevel": 5, "MaxUsageSpan": 60, "MaxUsageTime": 10, "PunishDelay": 60, - "ImmunityLevel": 4 + "StopLevel": 3, + "ChatCooldown": 15 }, "TorchRCON": @@ -44,5 +46,6 @@ }, "WolframAPIKey": "***", - "WundergroundAPIKey": "***" + "WundergroundAPIKey": "***", + "OpenWeatherAPIKey": "***" } diff --git a/main.py b/main.py index c02cd05..9d48ca6 100755 --- a/main.py +++ b/main.py @@ -15,7 +15,11 @@ import Torchlight.Torchlight from Torchlight.SourceRCONServer import SourceRCONServer if __name__ == '__main__': - logging.basicConfig(level = logging.DEBUG) + 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()