Start refactoring channels

This commit is contained in:
Griatch 2021-04-11 16:08:28 +02:00
parent 9e2b7576b1
commit 48ebe5e3c2
2 changed files with 469 additions and 236 deletions

View file

@ -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,

View file

@ -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):
"""