From 48ebe5e3c2cd9b3f3d1c3384c274078df7826da2 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 11 Apr 2021 16:08:28 +0200 Subject: [PATCH] Start refactoring channels --- evennia/accounts/accounts.py | 92 ++++++ evennia/comms/comms.py | 613 +++++++++++++++++++++-------------- 2 files changed, 469 insertions(+), 236 deletions(-) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 40946d9cb6..2aa6043743 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -945,6 +945,98 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): self, raw_string, callertype="account", session=session, **kwargs ) + # channel receive hooks + + def at_pre_channel_msg(self, message, channel, senders=None, **kwargs): + """ + Called by `self.channel_msg` before sending a channel message to the + user. This allows for customizing messages per-user and also to abort + the receive on the receiver-level. + + Args: + message (str): The message sent to the channel. + channel (Channel): The sending channel. + senders (list, optional): Accounts or Objects acting as senders. + For most normal messages, there is only a single sender. If + there are no senders, this may be a broadcasting message. + **kwargs: These are additional keywords passed into `channel_msg`. + If `no_prefix=True` or `emit=True` are passed, the channel + prefix will not be added (`[channelname]: ` by default) + + Returns: + str or None: Allows for customizing the message for this recipient. + If returning `None` (or `False`) message-receiving is aborted. + The returning string will be passed into `self.channel_msg`. + + Notes: + This support posing/emotes by starting channel-send with : or ;. + + """ + if senders: + sender_string = ', '.join(sender.key for sender in senders) + message_lstrip = message.lstrip() + if message_lstrip.startswith((':', ';')): + # this is a pose, should show as e.g. "User1 smiles to channel" + spacing = "" if message_lstrip.startswith((':', '\'', ',')) else " " + message = f"{sender_string}{spacing}{message_lstrip[1:]}" + else: + # normal message + message = f"{sender_string}: {message}" + + if not kwargs.get("no_prefix") or not kwargs.get("emit"): + message = channel.channel_prefix() + message + + return message + + def channel_msg(self, message, channel, senders=None, **kwargs): + """ + This performs the actions of receiving a message to an un-muted + channel. + + Args: + message (str): The message sent to the channel. + channel (Channel): The sending channel. + senders (list, optional): Accounts or Objects acting as senders. + For most normal messages, there is only a single sender. If + there are no senders, this may be a broadcasting message or + similar. + **kwargs: These are additional keywords originally passed into + `Channel.msg`. + + Notes: + Before this, `Channel.at_before_msg` will fire, which offers a way + to customize the message for the receiver on the channel-level. + + """ + # channel pre-msg hook + message = self.at_pre_channel_msg(message, channel, senders=senders, **kwargs) + if message in (None, False): + return + + # the actual sending + self.msg(text=(message, {"from_channel": channel.id}), + from_obj=senders, options={"from_channel": channel.id}) + + # channel post-msg hook + self.at_post_channel_msg(message, channel, senders=senders, **kwargs) + + def at_post_channel_msg(self, message, channel, senders=None, **kwargs): + """ + Called by `self.channel_msg` after message was received. + + Args: + message (str): The message sent to the channel. + channel (Channel): The sending channel. + senders (list, optional): Accounts or Objects acting as senders. + For most normal messages, there is only a single sender. If + there are no senders, this may be a broadcasting message. + **kwargs: These are additional keywords passed into `channel_msg`. + + """ + pass + + # search method + def search( self, searchdata, diff --git a/evennia/comms/comms.py b/evennia/comms/comms.py index 7c868869e0..09250db17e 100644 --- a/evennia/comms/comms.py +++ b/evennia/comms/comms.py @@ -20,10 +20,33 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase): This is the base class for all Channel Comms. Inherit from this to create different types of communication channels. + Class-level variables: + - `send_to_online_only` (bool, default True) - if set, will only try to + send to subscribers that are actually active. This is a useful optimization. + - `log_to_file` (str, default `"channel_{channel_key}.log"`). This is the + log file to which the channel history will be saved. The `{channel_key}` tag + will be replaced by the key of the Channel. If an Attribute 'log_file' + is set, this will be used instead. If this is None and no Attribute is found, + no history will be saved. + - `channel_prefix_string` (str, default `"[{channel_key} ]"`) - this is used + as a simple template to get the channel prefix with `.channel_prefix()`. + """ objects = ChannelManager() + # channel configuration + + # only send to characters/accounts who has an active session (this is a + # good optimization since people can still recover history separately). + send_to_online_only = True + # store log in log file. `channel_key tag will be replace with key of channel. + # Will use log_file Attribute first, if given + log_to_file = "channel_{channel_key}.log" + # which prefix to use when showing were a message is coming from. Set to + # None to disable and set this later. + channel_prefix_string = "[{channel_key}] " + def at_first_save(self): """ Called by the typeclass system the very first time the channel @@ -139,8 +162,8 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase): def unmute(self, subscriber, **kwargs): """ - Removes an entity to the list of muted subscribers. A muted subscriber will no longer see channel messages, - but may use channel commands. + Removes an entity to the list of muted subscribers. A muted subscriber + will no longer see channel messages, but may use channel commands. Args: subscriber (Object or Account): The subscriber to unmute. @@ -302,156 +325,7 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase): CHANNELHANDLER.update() - def message_transform( - self, msgobj, emit=False, prefix=True, sender_strings=None, external=False, **kwargs - ): - """ - Generates the formatted string sent to listeners on a channel. - - Args: - msgobj (Msg): Message object to send. - emit (bool, optional): In emit mode the message is not associated - with a specific sender name. - prefix (bool, optional): Prefix `msg` with a text given by `self.channel_prefix`. - sender_strings (list, optional): Used by bots etc, one string per external sender. - external (bool, optional): If this is an external sender or not. - **kwargs (dict): Arbitrary, optional arguments for users - overriding the call (unused by default). - - """ - if sender_strings or external: - body = self.format_external(msgobj, sender_strings, emit=emit) - else: - body = self.format_message(msgobj, emit=emit) - if prefix: - body = "%s%s" % (self.channel_prefix(msgobj, emit=emit), body) - msgobj.message = body - return msgobj - - def distribute_message(self, msgobj, online=False, **kwargs): - """ - Method for grabbing all listeners that a message should be - sent to on this channel, and sending them a message. - - Args: - msgobj (Msg or TempMsg): Message to distribute. - online (bool): Only send to receivers who are actually online - (not currently used): - **kwargs (dict): Arbitrary, optional arguments for users - overriding the call (unused by default). - - Notes: - This is also where logging happens, if enabled. - - """ - # get all accounts or objects connected to this channel and send to them - if online: - subs = self.subscriptions.online() - else: - subs = self.subscriptions.all() - for entity in subs: - # if the entity is muted, we don't send them a message - if entity in self.mutelist: - continue - try: - # note our addition of the from_channel keyword here. This could be checked - # by a custom account.msg() to treat channel-receives differently. - entity.msg( - msgobj.message, from_obj=msgobj.senders, options={"from_channel": self.id} - ) - except AttributeError as e: - logger.log_trace("%s\nCannot send msg to '%s'." % (e, entity)) - - if msgobj.keep_log: - # log to file - logger.log_file( - msgobj.message, self.attributes.get("log_file") or "channel_%s.log" % self.key - ) - - def msg( - self, - msgobj, - header=None, - senders=None, - sender_strings=None, - keep_log=None, - online=False, - emit=False, - external=False, - ): - """ - Send the given message to all accounts connected to channel. Note that - no permission-checking is done here; it is assumed to have been - done before calling this method. The optional keywords are not used if - persistent is False. - - Args: - msgobj (Msg, TempMsg or str): If a Msg/TempMsg, the remaining - keywords will be ignored (since the Msg/TempMsg object already - has all the data). If a string, this will either be sent as-is - (if persistent=False) or it will be used together with `header` - and `senders` keywords to create a Msg instance on the fly. - header (str, optional): A header for building the message. - senders (Object, Account or list, optional): Optional if persistent=False, used - to build senders for the message. - sender_strings (list, optional): Name strings of senders. Used for external - connections where the sender is not an account or object. - When this is defined, external will be assumed. The list will be - filtered so each sender-string only occurs once. - keep_log (bool or None, optional): This allows to temporarily change the logging status of - this channel message. If `None`, the Channel's `keep_log` Attribute will - be used. If `True` or `False`, that logging status will be used for this - message only (note that for unlogged channels, a `True` value here will - create a new log file only for this message). - online (bool, optional) - If this is set true, only messages people who are - online. Otherwise, messages all accounts connected. This can - make things faster, but may not trigger listeners on accounts - that are offline. - emit (bool, optional) - Signals to the message formatter that this message is - not to be directly associated with a name. - external (bool, optional): Treat this message as being - agnostic of its sender. - - Returns: - success (bool): Returns `True` if message sending was - successful, `False` otherwise. - - """ - senders = make_iter(senders) if senders else [] - if isinstance(msgobj, str): - # given msgobj is a string - convert to msgobject (always TempMsg) - msgobj = TempMsg(senders=senders, header=header, message=msgobj, channels=[self]) - # we store the logging setting for use in distribute_message() - msgobj.keep_log = keep_log if keep_log is not None else self.db.keep_log - - # start the sending - msgobj = self.pre_send_message(msgobj) - if not msgobj: - return False - if sender_strings: - sender_strings = list(set(make_iter(sender_strings))) - msgobj = self.message_transform( - msgobj, emit=emit, sender_strings=sender_strings, external=external - ) - self.distribute_message(msgobj, online=online) - self.post_send_message(msgobj) - return True - - def tempmsg(self, message, header=None, senders=None): - """ - A wrapper for sending non-persistent messages. - - Args: - message (str): Message to send. - header (str, optional): Header of message to send. - senders (Object or list, optional): Senders of message to send. - - """ - self.msg(message, senders=senders, header=header, keep_log=False) - - # hooks - - def channel_prefix(self, msg=None, emit=False, **kwargs): + def channel_prefix(self): """ Hook method. How the channel should prefix itself for users. @@ -465,111 +339,378 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase): Returns: prefix (str): The created channel prefix. - """ - return "" if emit else "[%s] " % self.key + Notes: + This is normally retrieved from the hooks on the receiver. - def format_senders(self, senders=None, **kwargs): """ - Hook method. Function used to format a list of sender names. + return self.channel_prefix_string.format(channel_key=self.key) + + def at_pre_msg(self, message, **kwargs): + """ + Called before the starting of sending the message to a receiver. This + is called before any hooks on the receiver itself. If this returns + None/False, the sending will be aborted. Args: - senders (list): Sender object names. - **kwargs (dict): Arbitrary, optional arguments for users - overriding the call (unused by default). + message (str): The message to send. + **kwargs (any): Keywords passed on from `.msg`. This includes + `senders`. Returns: - formatted_list (str): The list of names formatted appropriately. + str, False or None: Any custom changes made to the message. If + falsy, no message will be sent. + + """ + return message + + def msg(self, message, senders=None, bypass_mute=False, **kwargs): + """ + Send message to channel, causing it to be distributed to all non-muted + subscribed users of that channel. + + Args: + message (str): The message to send. + senders (Object, Account or list, optional): If not given, there is + no way to associate one or more senders with the message (like + a broadcast message or similar). + bypass_mute (bool, optional): If set, always send, regardless of + individual mute-state of subscriber. This can be used for + global announcements or warnings/alerts. + **kwargs (any): This will be passed on to all hooks. Use `no_prefix` + to exclude the channel prefix. Notes: - This function exists separately so that external sources - can use it to format source names in the same manner as - normal object/account names. + The call hook calling sequence is: + + - `msg = channel.at_pre_msg(message, **kwargs)` (aborts for all if return None) + - `msg = receiver.at_pre_channel_msg(msg, channel, **kwargs)` (aborts for receiver if return None) + - `receiver.at_channel_msg(msg, channel, **kwargs)` + - `receiver.at_post_channel_msg(msg, channel, **kwargs)`` + Called after all receivers are processed: + - `channel.at_post_all_msg(message, **kwargs)` + + (where the senders/bypass_mute are embedded into **kwargs for + later access in hooks) """ - if not senders: - return "" - return ", ".join(senders) - - def pose_transform(self, msgobj, sender_string, **kwargs): - """ - Hook method. Detects if the sender is posing, and modifies the - message accordingly. - - Args: - msgobj (Msg or TempMsg): The message to analyze for a pose. - sender_string (str): The name of the sender/poser. - **kwargs (dict): Arbitrary, optional arguments for users - overriding the call (unused by default). - - Returns: - string (str): A message that combines the `sender_string` - component with `msg` in different ways depending on if a - pose was performed or not (this must be analyzed by the - hook). - - """ - pose = False - message = msgobj.message - message_start = message.lstrip() - if message_start.startswith((":", ";")): - pose = True - message = message[1:] - if not message.startswith((":", "'", ",")): - if not message.startswith(" "): - message = " " + message - if pose: - return "%s%s" % (sender_string, message) + senders = make_iter(senders) if senders else [] + if self.send_to_online_only: + receivers = self.subscriptions.online() else: - return "%s: %s" % (sender_string, message) + receivers = self.subscriptions.all() + if not bypass_mute: + receivers = receivers.exclude(id__in=[muted.id for muted in self.mutelist]) - def format_external(self, msgobj, senders, emit=False, **kwargs): + send_kwargs = {'senders': senders, 'bypass_mute': bypass_mute, **kwargs} + + # pre-send hook + message = self.at_pre_msg(message, **send_kwargs) + if message in (None, False): + return + + for receiver in receivers: + # send to each individual subscriber + try: + recv_msg = receiver.at_pre_channel_msg(message, self, **send_kwargs) + if recv_msg in (None, False): + continue + receiver.channel_msg(recv_msg, self, **send_kwargs) + receiver.at_post_channel_msg(recv_msg, **send_kwargs) + + except Exception: + logger.log_trace(f"Cannot send channel message to {receiver}.") + + self.at_post_msg(self, message, **send_kwargs) + + def at_post_msg(self, message, **kwargs): """ - Hook method. Used for formatting external messages. This is - needed as a separate operation because the senders of external - messages may not be in-game objects/accounts, and so cannot - have things like custom user preferences. + This is called after sending to *all* valid recipients. It is normally + used for logging/channel history. Args: - msgobj (Msg or TempMsg): The message to send. - senders (list): Strings, one per sender. - emit (bool, optional): A sender-agnostic message or not. - **kwargs (dict): Arbitrary, optional arguments for users - overriding the call (unused by default). - - Returns: - transformed (str): A formatted string. + message (str): The message sent. + **kwargs (any): Keywords passed on from `msg`, including `senders`. """ - if emit or not senders: - return msgobj.message - senders = ", ".join(senders) - return self.pose_transform(msgobj, senders) + # save channel history to log file + default_log_file = (self.log_to_file.format(channel_key=self.key) + if self.log_to_file else None) + log_file = self.attributes.get("log_file", default=default_log_file) + if log_file: + senders = ",".join(kwargs.get("senders", [])) + senders = f"{senders}: " if senders else "" + message = f"{senders}{message}" + logger.log_file(message, log_file) - def format_message(self, msgobj, emit=False, **kwargs): - """ - Hook method. Formats a message body for display. + # def message_transform( + # self, msgobj, emit=False, prefix=True, sender_strings=None, external=False, **kwargs + # ): + # """ + # Generates the formatted string sent to listeners on a channel. - Args: - msgobj (Msg or TempMsg): The message object to send. - emit (bool, optional): The message is agnostic of senders. - **kwargs (dict): Arbitrary, optional arguments for users - overriding the call (unused by default). + # Args: + # msgobj (Msg): Message object to send. + # emit (bool, optional): In emit mode the message is not associated + # with a specific sender name. + # prefix (bool, optional): Prefix `msg` with a text given by `self.channel_prefix`. + # sender_strings (list, optional): Used by bots etc, one string per external sender. + # external (bool, optional): If this is an external sender or not. + # **kwargs (dict): Arbitrary, optional arguments for users + # overriding the call (unused by default). - Returns: - transformed (str): The formatted message. + # """ + # if sender_strings or external: + # body = self.format_external(msgobj, sender_strings, emit=emit) + # else: + # body = self.format_message(msgobj, emit=emit) + # if prefix: + # body = "%s%s" % (self.channel_prefix(msgobj, emit=emit), body) + # msgobj.message = body + # return msgobj - """ - # We don't want to count things like external sources as senders for - # the purpose of constructing the message string. - senders = [sender for sender in msgobj.senders if hasattr(sender, "key")] - if not senders: - emit = True - if emit: - return msgobj.message - else: - senders = [sender.key for sender in msgobj.senders] - senders = ", ".join(senders) - return self.pose_transform(msgobj, senders) + # def distribute_message(self, msgobj, online=False, **kwargs): + # """ + # Method for grabbing all listeners that a message should be + # sent to on this channel, and sending them a message. + + # Args: + # msgobj (Msg or TempMsg): Message to distribute. + # online (bool): Only send to receivers who are actually online + # (not currently used): + # **kwargs (dict): Arbitrary, optional arguments for users + # overriding the call (unused by default). + + # Notes: + # This is also where logging happens, if enabled. + + # """ + # # get all accounts or objects connected to this channel and send to them + # if online: + # subs = self.subscriptions.online() + # else: + # subs = self.subscriptions.all() + # for entity in subs: + # # if the entity is muted, we don't send them a message + # if entity in self.mutelist: + # continue + # try: + # # note our addition of the from_channel keyword here. This could be checked + # # by a custom account.msg() to treat channel-receives differently. + # entity.msg( + # msgobj.message, from_obj=msgobj.senders, options={"from_channel": self.id} + # ) + # except AttributeError as e: + # logger.log_trace("%s\nCannot send msg to '%s'." % (e, entity)) + + # if msgobj.keep_log: + # # log to file + # logger.log_file( + # msgobj.message, self.attributes.get("log_file") or "channel_%s.log" % self.key + # ) + + # def msg( + # self, + # msgobj, + # header=None, + # senders=None, + # sender_strings=None, + # keep_log=None, + # online=False, + # emit=False, + # external=False, + # ): + # """ + # Send the given message to all accounts connected to channel. Note that + # no permission-checking is done here; it is assumed to have been + # done before calling this method. The optional keywords are not used if + # persistent is False. + + # Args: + # msgobj (Msg, TempMsg or str): If a Msg/TempMsg, the remaining + # keywords will be ignored (since the Msg/TempMsg object already + # has all the data). If a string, this will either be sent as-is + # (if persistent=False) or it will be used together with `header` + # and `senders` keywords to create a Msg instance on the fly. + # header (str, optional): A header for building the message. + # senders (Object, Account or list, optional): Optional if persistent=False, used + # to build senders for the message. + # sender_strings (list, optional): Name strings of senders. Used for external + # connections where the sender is not an account or object. + # When this is defined, external will be assumed. The list will be + # filtered so each sender-string only occurs once. + # keep_log (bool or None, optional): This allows to temporarily change the logging status of + # this channel message. If `None`, the Channel's `keep_log` Attribute will + # be used. If `True` or `False`, that logging status will be used for this + # message only (note that for unlogged channels, a `True` value here will + # create a new log file only for this message). + # online (bool, optional) - If this is set true, only messages people who are + # online. Otherwise, messages all accounts connected. This can + # make things faster, but may not trigger listeners on accounts + # that are offline. + # emit (bool, optional) - Signals to the message formatter that this message is + # not to be directly associated with a name. + # external (bool, optional): Treat this message as being + # agnostic of its sender. + + # Returns: + # success (bool): Returns `True` if message sending was + # successful, `False` otherwise. + + # """ + # senders = make_iter(senders) if senders else [] + # if isinstance(msgobj, str): + # # given msgobj is a string - convert to msgobject (always TempMsg) + # msgobj = TempMsg(senders=senders, header=header, message=msgobj, channels=[self]) + # # we store the logging setting for use in distribute_message() + # msgobj.keep_log = keep_log if keep_log is not None else self.db.keep_log + + # # start the sending + # msgobj = self.pre_send_message(msgobj) + + # if not msgobj: + # return False + # if sender_strings: + # sender_strings = list(set(make_iter(sender_strings))) + # msgobj = self.message_transform( + # msgobj, emit=emit, sender_strings=sender_strings, external=external + # ) + # self.distribute_message(msgobj, online=online) + # self.post_send_message(msgobj) + # return True + + # def tempmsg(self, message, header=None, senders=None): + # """ + # A wrapper for sending non-persistent messages. + + # Args: + # message (str): Message to send. + # header (str, optional): Header of message to send. + # senders (Object or list, optional): Senders of message to send. + + # """ + # self.msg(message, senders=senders, header=header, keep_log=False) + + # # hooks + + # def channel_prefix(self, msg=None, emit=False, **kwargs): + # """ + # Hook method. How the channel should prefix itself for users. + + # Args: + # msg (str, optional): Prefix text + # emit (bool, optional): Switches to emit mode, which usually + # means to not prefix the channel's info. + # **kwargs (dict): Arbitrary, optional arguments for users + # overriding the call (unused by default). + + # Returns: + # prefix (str): The created channel prefix. + + # """ + # return "" if emit else "[%s] " % self.key + + # def format_senders(self, senders=None, **kwargs): + # """ + # Hook method. Function used to format a list of sender names. + + # Args: + # senders (list): Sender object names. + # **kwargs (dict): Arbitrary, optional arguments for users + # overriding the call (unused by default). + + # Returns: + # formatted_list (str): The list of names formatted appropriately. + + # Notes: + # This function exists separately so that external sources + # can use it to format source names in the same manner as + # normal object/account names. + + # """ + # if not senders: + # return "" + # return ", ".join(senders) + + # def pose_transform(self, msgobj, sender_string, **kwargs): + # """ + # Hook method. Detects if the sender is posing, and modifies the + # message accordingly. + + # Args: + # msgobj (Msg or TempMsg): The message to analyze for a pose. + # sender_string (str): The name of the sender/poser. + # **kwargs (dict): Arbitrary, optional arguments for users + # overriding the call (unused by default). + + # Returns: + # string (str): A message that combines the `sender_string` + # component with `msg` in different ways depending on if a + # pose was performed or not (this must be analyzed by the + # hook). + + # """ + # pose = False + # message = msgobj.message + # message_start = message.lstrip() + # if message_start.startswith((":", ";")): + # pose = True + # message = message[1:] + # if not message.startswith((":", "'", ",")): + # if not message.startswith(" "): + # message = " " + message + # if pose: + # return "%s%s" % (sender_string, message) + # else: + # return "%s: %s" % (sender_string, message) + + # def format_external(self, msgobj, senders, emit=False, **kwargs): + # """ + # Hook method. Used for formatting external messages. This is + # needed as a separate operation because the senders of external + # messages may not be in-game objects/accounts, and so cannot + # have things like custom user preferences. + + # Args: + # msgobj (Msg or TempMsg): The message to send. + # senders (list): Strings, one per sender. + # emit (bool, optional): A sender-agnostic message or not. + # **kwargs (dict): Arbitrary, optional arguments for users + # overriding the call (unused by default). + + # Returns: + # transformed (str): A formatted string. + + # """ + # if emit or not senders: + # return msgobj.message + # senders = ", ".join(senders) + # return self.pose_transform(msgobj, senders) + + # def format_message(self, msgobj, emit=False, **kwargs): + # """ + # Hook method. Formats a message body for display. + + # Args: + # msgobj (Msg or TempMsg): The message object to send. + # emit (bool, optional): The message is agnostic of senders. + # **kwargs (dict): Arbitrary, optional arguments for users + # overriding the call (unused by default). + + # Returns: + # transformed (str): The formatted message. + + # """ + # # We don't want to count things like external sources as senders for + # # the purpose of constructing the message string. + # senders = [sender for sender in msgobj.senders if hasattr(sender, "key")] + # if not senders: + # emit = True + # if emit: + # return msgobj.message + # else: + # senders = [sender.key for sender in msgobj.senders] + # senders = ", ".join(senders) + # return self.pose_transform(msgobj, senders) def pre_join_channel(self, joiner, **kwargs): """