From 77efbd134e3bc5121cadddb16fb4b3bf0f2d779e Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 23 Jun 2019 09:22:20 +0200 Subject: [PATCH] Starting to implement grapewine support --- evennia/accounts/bots.py | 90 ++++++++++++++++++++ evennia/commands/default/cmdset_account.py | 1 + evennia/commands/default/comms.py | 97 ++++++++++++++++++++-- evennia/server/portal/webclient.py | 4 +- evennia/server/server.py | 5 ++ evennia/settings_default.py | 16 ++++ evennia/utils/create.py | 11 ++- 7 files changed, 212 insertions(+), 12 deletions(-) diff --git a/evennia/accounts/bots.py b/evennia/accounts/bots.py index 6cce22bcb3..302f9db8b4 100644 --- a/evennia/accounts/bots.py +++ b/evennia/accounts/bots.py @@ -15,6 +15,8 @@ _IDLE_TIMEOUT = settings.IDLE_TIMEOUT _IRC_ENABLED = settings.IRC_ENABLED _RSS_ENABLED = settings.RSS_ENABLED +_GRAPEWINE_ENABLED = settings.GRAPEWINE_ENABLED + _SESSIONS = None @@ -424,3 +426,91 @@ class RSSBot(Bot): self.ndb.ev_channel = self.db.ev_channel if self.ndb.ev_channel: self.ndb.ev_channel.msg(txt, senders=self.id) + + +# Grapewine bot + +class GrapewineBot(Bot): + """ + A Grapewine (https://grapewine.haus) relayer. The channel to connect to is the first + name in the settings.GRAPEWINE_CHANNELS list. + + """ + factory_path = "evennia.server.portal.grapewine.RestartingWebsocketServerFactory" + + def start(self, ev_channel=None, grapewine_channel=None): + """ + Start by telling the portal to connect to the grapewine network. + + """ + if not _GRAPEWINE_ENABLED: + self.delete() + return + + global _SESSIONS + if not _SESSIONS: + from evennia.server.sessionhandler import SESSIONS as _SESSIONS + + # connect to Evennia channel + if ev_channel: + # connect to Evennia channel + channel = search.channel_search(ev_channel) + if not channel: + raise RuntimeError("Evennia Channel '%s' not found." % ev_channel) + channel = channel[0] + channel.connect(self) + self.db.ev_channel = channel + + if grapewine_channel: + self.db.grapewine_channel = grapewine_channel + + # these will be made available as properties on the protocol factory + configdict = {"uid": self.dbid, + "grapewine_channel": self.db.grapewine_channel} + + _SESSIONS.start_bot_session(self.factory_path, configdict) + + def at_msg_send(self, **kwargs): + "Shortcut here or we can end up in infinite loop" + pass + + def msg(self, text=None, **kwargs): + """ + Takes text from connected channel (only). + + Args: + text (str, optional): Incoming text from channel. + + Kwargs: + options (dict): Options dict with the following allowed keys: + - from_channel (str): dbid of a channel this text originated from. + - from_obj (list): list of objects sending this text. + + """ + from_obj = kwargs.get("from_obj", None) + options = kwargs.get("options", None) or {} + if not self.ndb.ev_channel and self.db.ev_channel: + # cache channel lookup + self.ndb.ev_channel = self.db.ev_channel + if ("from_channel" in options and text and + self.ndb.ev_channel.dbid == options["from_channel"]): + if not from_obj or from_obj != [self]: + # send outputfunc text(msg, chan, sender) + super().msg(text=(text, self.db.grapewine_channel, from_obj.key)) + + def execute_cmd(self, txt=None, session=None, event=None, grapewine_channel=None, + sender=None, game=None, **kwargs): + """ + Take incoming data from protocol and send it to connected channel. This is + triggered by the bot_data_in Inputfunc. + """ + if event == "channels/broadcast": + # A private message to the bot - a command. + + text = f"{sender}@{game}: {txt}" + + if not self.ndb.ev_channel and self.db.ev_channel: + # simple cache of channel lookup + self.ndb.ev_channel = self.db.ev_channel + if self.ndb.ev_channel: + self.ndb.ev_channel.msg(text, senders=self) diff --git a/evennia/commands/default/cmdset_account.py b/evennia/commands/default/cmdset_account.py index c36a659844..d48c140d2c 100644 --- a/evennia/commands/default/cmdset_account.py +++ b/evennia/commands/default/cmdset_account.py @@ -74,3 +74,4 @@ class AccountCmdSet(CmdSet): self.add(comms.CmdIRC2Chan()) self.add(comms.CmdIRCStatus()) self.add(comms.CmdRSS2Chan()) + self.add(comms.CmdGrapewine2Chan()) diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index d22cc5d5ab..53c0aef813 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -816,7 +816,6 @@ def _list_bots(cmd): """ ircbots = [bot for bot in AccountDB.objects.filter(db_is_bot=True, username__startswith="ircbot-")] if ircbots: - from evennia.utils.evtable import EvTable table = cmd.styled_table("|w#dbref|n", "|wbotname|n", "|wev-channel|n", "|wirc-channel|n", "|wSSL|n", maxwidth=_DEFAULT_WIDTH) for ircbot in ircbots: @@ -829,7 +828,7 @@ def _list_bots(cmd): class CmdIRC2Chan(COMMAND_DEFAULT_CLASS): """ - link an evennia channel to an external IRC channel + Link an evennia channel to an external IRC channel Usage: irc2chan[/switches] = <#irchannel> [:typeclass] @@ -924,9 +923,8 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS): self.msg("Account '%s' already exists and is not a bot." % botname) return else: - password = hashlib.md5(bytes(str(time.time()), 'utf-8')).hexdigest()[:11] try: - bot = create.create_account(botname, None, password, typeclass=botclass) + bot = create.create_account(botname, None, None, typeclass=botclass) except Exception as err: self.msg("|rError, could not create the bot:|n '%s'." % err) return @@ -1052,7 +1050,6 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS): # show all connections rssbots = [bot for bot in AccountDB.objects.filter(db_is_bot=True, username__startswith="rssbot-")] if rssbots: - from evennia.utils.evtable import EvTable table = self.styled_table("|wdbid|n", "|wupdate rate|n", "|wev-channel", "|wRSS feed URL|n", border="cells", maxwidth=_DEFAULT_WIDTH) for rssbot in rssbots: @@ -1083,7 +1080,6 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS): url = self.rhs botname = "rssbot-%s" % url - # create a new bot bot = AccountDB.objects.filter(username__iexact=botname) if bot: # re-use existing bot @@ -1092,6 +1088,95 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS): self.msg("Account '%s' already exists and is not a bot." % botname) return else: + # create a new bot bot = create.create_account(botname, None, None, typeclass=bots.RSSBot) bot.start(ev_channel=channel, rss_url=url, rss_rate=10) self.msg("RSS reporter created. Fetching RSS.") + + +class CmdGrapewine2Chan(COMMAND_DEFAULT_CLASS): + """ + Link an Evennia channel to an exteral Grapewine channel + + Usage: + grapewine2chan[/switches] = + grapewine2chan/disconnect + + Switches: + /list - (or no switch): show existing grapewine <-> Evennia + mappings and available grapewine chans + /remove - alias to disconnect + /delete - alias to disconnect + + Example: + grapewine2chan mygrapewine = gossip + + This creates a link between an in-game Evennia channel and an external + Grapewine channel. The game must be registered with the Grapewine network + (register at https://grapewine.haus) and the GRAPEWINE_* auth information + must be added to game settings. + """ + + key = "grapewine2chan" + switch_options = ("disconnect", "remove", "delete", "list") + locks = "cmd:serversetting(GRAPEWINE_ENABLED) and pperm(Developer)" + help_category = "Comms" + + def func(self): + """Setup the Grapewine channel mapping""" + + if not settings.GRAPEWINE_ENABLED: + self.msg("Set GRAPEWINE_ENABLED=True in settings to enable.") + return + + if "list" in self.switches: + # show all connections + gwbots = [bot for bot in + AccountDB.objects.filter(db_is_bot=True, + username__startswith="grapewinebot-")] + if gwbots: + table = self.styled_table("|wdbid|n", "|wev-channel", + "|wgw-channel|n", border="cells", maxwidth=_DEFAULT_WIDTH) + for gwbot in gwbots: + table.add_row(gwbot.id, gwbot.db.ev_channel, gwbot.db.grapewine_channel) + self.msg(table) + else: + self.msg("No grapewine bots found.") + return + + if 'disconnect' in self.switches or 'remove' in self.switches or 'delete' in self.switches: + botname = "grapewinebot-%s" % self.lhs + matches = AccountDB.objects.filter(db_is_bot=True, db_key=botname) + + if not matches: + # try dbref match + matches = AccountDB.objects.filter(db_is_bot=True, id=self.args.lstrip("#")) + if matches: + matches[0].delete() + self.msg("Grapewine connection destroyed.") + else: + self.msg("Grapewine connection/bot could not be removed, does it exist?") + return + + if not self.args or not self.rhs: + string = "Usage: grapewine2chan[/switches] = " + self.msg(string) + return + + channel = self.lhs + grapewine_channel = self.rhs + + botname = "grapewinebot-%s-%s" % (channel, grapewine_channel) + bot = AccountDB.objects.filter(username__iexact=botname) + if bot: + # re-use existing bot + bot = bot[0] + if not bot.is_bot: + self.msg("Account '%s' already exists and is not a bot." % botname) + return + else: + # create a new bot + bot = create.create_account(botname, None, None, typeclass=bots.GrapewineBot) + + bot.start(ev_channel=channel, grapewine_channel=grapewine_channel) + self.msg(f"Grapewine connection created {channel} <-> {grapewine_channel}.") diff --git a/evennia/server/portal/webclient.py b/evennia/server/portal/webclient.py index d4e21e649c..43a3a7bf8c 100644 --- a/evennia/server/portal/webclient.py +++ b/evennia/server/portal/webclient.py @@ -37,7 +37,7 @@ class WebSocketClient(WebSocketServerProtocol, Session): Implements the server-side of the Websocket connection. """ def __init__(self, *args, **kwargs): - super(WebSocketClient, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.protocol_key = "webclient/websocket" def get_client_session(self): @@ -167,7 +167,7 @@ class WebSocketClient(WebSocketServerProtocol, Session): """ Data User > Evennia. - Args:: + Args: text (str): Incoming text. kwargs (any): Options from protocol. diff --git a/evennia/server/server.py b/evennia/server/server.py index 3b3ff049f6..1ee8d9642c 100644 --- a/evennia/server/server.py +++ b/evennia/server/server.py @@ -72,6 +72,7 @@ GUEST_ENABLED = settings.GUEST_ENABLED WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES IRC_ENABLED = settings.IRC_ENABLED RSS_ENABLED = settings.RSS_ENABLED +GRAPEWINE_ENABLED = settings.GRAPEWINE_ENABLED WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED INFO_DICT = {"servername": SERVERNAME, "version": VERSION, @@ -583,6 +584,10 @@ if RSS_ENABLED: # RSS feed channel connections ENABLED.append('rss') +if GRAPEWINE_ENABLED: + # Grapewine channel connections + ENABLED.append('grapewine') + if ENABLED: INFO_DICT["irc_rss"] = ", ".join(ENABLED) + " enabled." diff --git a/evennia/settings_default.py b/evennia/settings_default.py index d378b2157d..c4f8c3cb07 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -706,6 +706,22 @@ IRC_ENABLED = False RSS_ENABLED = False RSS_UPDATE_INTERVAL = 60 * 10 # 10 minutes +# Grapewine (grapewine.haus) is a network for listing MUDs as well as allow +# users of said MUDs to communicate with each other on shared channels. To use, +# your game must first be registered by logging in and creating a game entry at +# https://grapewine.haus. Evennia links grapewine channels to in-game channels +# with the @grapewine2chan command, available once this flag is set +# Grapewine requires installing the pyopenssl library (pip install pyopenssl) +GRAPEWINE_ENABLED = False +# Grapewine channels to allow connection to. See https://grapevine.haus/chat +# for the available channels. Only channels in this list can be linked to in-game +# channels later. +GRAPEWINE_CHANNELS = ["gossip", "testing"] +# Grapewine authentication. Register your game at https://grapewine.haus to get +# them. These are secret and should thus be overridden in secret_settings file +GRAPEWINE_CLIENT_ID = "" +GRAPEWINE_CLIENT_SECRET = "" + ###################################################################### # Django web features ###################################################################### diff --git a/evennia/utils/create.py b/evennia/utils/create.py index fa934ca394..d245ab7a5f 100644 --- a/evennia/utils/create.py +++ b/evennia/utils/create.py @@ -469,11 +469,14 @@ def create_account(key, email, password, new_account = typeclass(username=key, email=email, is_staff=is_superuser, is_superuser=is_superuser, last_login=now, date_joined=now) - valid, error = new_account.validate_password(password, new_account) - if not valid: - raise error + if password is not None: + # the password may be None for 'fake' accounts, like bots + valid, error = new_account.validate_password(password, new_account) + if not valid: + raise error + + new_account.set_password(password) - new_account.set_password(password) new_account._createdict = dict(locks=locks, permissions=permissions, report_to=report_to, tags=tags, attributes=attributes) # saving will trigger the signal that calls the