Continuing implementing new channel command

This commit is contained in:
Griatch 2021-04-14 22:44:11 +02:00
parent 2776aa2e20
commit 6d973f3617
2 changed files with 641 additions and 344 deletions

View file

@ -18,6 +18,7 @@ from evennia.locks.lockhandler import LockException
from evennia.utils import create, logger, utils, evtable from evennia.utils import create, logger, utils, evtable
from evennia.utils.logger import tail_log_file from evennia.utils.logger import tail_log_file
from evennia.utils.utils import make_iter, class_from_module from evennia.utils.utils import make_iter, class_from_module
from evennia.utils import evmore
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
CHANNEL_DEFAULT_TYPECLASS = class_from_module( CHANNEL_DEFAULT_TYPECLASS = class_from_module(
@ -49,7 +50,43 @@ _DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
# helper functions to make it easier to override the main CmdChannel # helper functions to make it easier to override the main CmdChannel
# command and to keep the legacy addcom etc commands around. # command and to keep the legacy addcom etc commands around.
def search_channel(caller, channelname, exact=False):
class CmdChannel(COMMAND_DEFAULT_CLASS):
"""
Talk on and manage in-game channels.
Usage:
channel channelname [= <msg>]
channel
channel/list
channel/all
channel/history channelname [= index]
channel/sub channelname [= alias]
channel/unsub channelname[,channelname, ...]
channel/alias channelname = alias
channel/unalias channelname = alias
channel/mute channelname[,channelname,...]
channel/unmute channelname[,channelname,...]
channel/create channelname;alias;alias:typeclass [= description]
channel/destroy channelname [: reason]
channel/lock channelname = lockstring
channel/desc channelname = description
channel/boot[/quiet] channelname = subscribername [: reason]
channel/ban channelname
channel/ban[/quiet] channelname = subscribername [: reason]
channel/who channelname
This handles all operations on channels.
"""
key = "channel"
aliases = ["chan", "channels"]
locks = "cmd: not pperm(channel_banned)"
switch_options = (
"history", "sub", "unsub", "mute", "alias", "unalias", "create",
"destroy", "desc", "boot", "who")
def search_channel(self, channelname, exact=False):
""" """
Helper function for searching for a single channel with some error Helper function for searching for a single channel with some error
handling. handling.
@ -64,9 +101,10 @@ def search_channel(caller, channelname, exact=False):
list: A list of zero, one or more channels found. list: A list of zero, one or more channels found.
Notes: Notes:
No permission checks will be done here. The 'listen' and 'control' accesses are checked before returning.
""" """
caller = self.caller
# first see if this is a personal alias # first see if this is a personal alias
channelname = caller.nicks.get(key=channelname, category="channel") or channelname channelname = caller.nicks.get(key=channelname, category="channel") or channelname
@ -77,7 +115,6 @@ def search_channel(caller, channelname, exact=False):
# try fuzzy matching as well # try fuzzy matching as well
channels = CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(channelname, exact=exact) channels = CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(channelname, exact=exact)
# check permissions # check permissions
channels = [channel for channel in channels channels = [channel for channel in channels
if channel.access(caller, 'listen') or channel.access(caller, 'control')] if channel.access(caller, 'listen') or channel.access(caller, 'control')]
@ -88,35 +125,33 @@ def search_channel(caller, channelname, exact=False):
return list(channels) return list(channels)
return [channels[0]] return [channels[0]]
def msg_channel(self, channel, message, **kwargs):
def msg_channel(caller, channel, message, **kwargs):
""" """
Send a message to a given channel. At this point Send a message to a given channel. At this point
any permissions should already be done. any permissions should already be done.
Args: Args:
caller (Object or Account): The entity sending the message.
channel (Channel): The channel to send to. channel (Channel): The channel to send to.
message (str): The message to send. message (str): The message to send.
**kwargs: Unused by default. These kwargs will be passed into **kwargs: Unused by default. These kwargs will be passed into
all channel messaging hooks for custom overriding. all channel messaging hooks for custom overriding.
""" """
channel.msg(message, senders=caller, **kwargs) channel.msg(message, senders=self.caller, **kwargs)
def get_channel_history(self, channel, start_index=0):
def get_channel_history(caller, channel, start_index=0):
""" """
View a channel's history. View a channel's history.
Args: Args:
caller (Object or Account): The entity performing the action.
channel (Channel): The channel to access. channel (Channel): The channel to access.
message (str): The message to send. message (str): The message to send.
**kwargs: Unused by default. These kwargs will be passed into **kwargs: Unused by default. These kwargs will be passed into
all channel messaging hooks for custom overriding. all channel messaging hooks for custom overriding.
""" """
caller = self.caller
log_file = channel.attributes.get( log_file = channel.attributes.get(
"log_file", default=channel.log_file.format(channelkey=channel.key)) "log_file", default=channel.log_file.format(channelkey=channel.key))
@ -127,13 +162,12 @@ def get_channel_history(caller, channel, start_index=0):
# asynchronously tail the log file # asynchronously tail the log file
tail_log_file(log_file, start_index, 20, callback=send_msg) tail_log_file(log_file, start_index, 20, callback=send_msg)
def sub_to_channel(caller, channel): def sub_to_channel(self, channel):
""" """
Subscribe to a channel. Note that all permissions should Subscribe to a channel. Note that all permissions should
be checked before this step. be checked before this step.
Args: Args:
caller (Object or Account): The entity performing the action.
channel (Channel): The channel to access. channel (Channel): The channel to access.
Returns: Returns:
@ -141,19 +175,19 @@ def sub_to_channel(caller, channel):
the second part is an error string. the second part is an error string.
""" """
caller = self.caller
if channel.has_connection(caller): if channel.has_connection(caller):
return False, f"Already listening to channel {channel.key}." return False, f"Already listening to channel {channel.key}."
result = channel.connect(caller) result = channel.connect(caller)
return result, "" if result else f"Were not allowed to subscribe to channel {channel.key}" return result, "" if result else f"Were not allowed to subscribe to channel {channel.key}"
def unsub_from_channel(self, channel):
def unsub_to_channel(caller, channel):
""" """
Un-Subscribe to a channel. Note that all permissions should Un-Subscribe to a channel. Note that all permissions should
be checked before this step. be checked before this step.
Args: Args:
caller (Object or Account): The entity performing the action.
channel (Channel): The channel to unsub from. channel (Channel): The channel to unsub from.
Returns: Returns:
@ -161,6 +195,8 @@ def unsub_to_channel(caller, channel):
the second part is an error string. the second part is an error string.
""" """
caller = self.caller
if not channel.has_connection(caller): if not channel.has_connection(caller):
return False, f"Not listening to channel {channel.key}." return False, f"Not listening to channel {channel.key}."
# clear nicks # clear nicks
@ -174,26 +210,22 @@ def unsub_to_channel(caller, channel):
result = channel.disconnect(caller) result = channel.disconnect(caller)
return result, "" if result else f"Could not unsubscribe from channel {channel.key}" return result, "" if result else f"Could not unsubscribe from channel {channel.key}"
def add_alias(self, channel, alias):
def add_alias(caller, channel, alias):
""" """
Add a new alias for the user to use with this channel. Add a new alias for the user to use with this channel.
Args: Args:
caller (Object or Account): The entity performing the action.
channel (Channel): The channel to alias. channel (Channel): The channel to alias.
alias (str): The personal alias to use for this channel. alias (str): The personal alias to use for this channel.
""" """
caller.nicks.add(alias, channel.key, category="channel") self.caller.nicks.add(alias, channel.key, category="channel")
def remove_alias(self, alias):
def remove_alias(caller, alias):
""" """
Remove an alias from a given channel. Remove an alias from a given channel.
Args: Args:
caller (Object or Account): The entity performing the action.
alias (str, optional): The alias to remove, or `None` for all. alias (str, optional): The alias to remove, or `None` for all.
Returns: Returns:
@ -201,36 +233,33 @@ def remove_alias(caller, alias):
the second part is an error string. the second part is an error string.
""" """
caller = self.caller
channame = caller.nicks.get(key=alias, category="channel") channame = caller.nicks.get(key=alias, category="channel")
if channame: if channame:
caller.nicks.remove(key=alias, category="channel") caller.nicks.remove(key=alias, category="channel")
return True, "" return True, ""
return False, "No such alias was defined." return False, "No such alias was defined."
def mute_channel(self, channel):
def mute_channel(caller, channel):
""" """
Temporarily mute a channel. Temporarily mute a channel.
Args: Args:
caller (Object or Account): The entity performing the action.
channel (Channel): The channel to alias. channel (Channel): The channel to alias.
Returns: Returns:
bool, str: True, None if muting successful. If False, bool, str: True, None if muting successful. If False,
the second part is an error string. the second part is an error string.
""" """
if channel.mute(caller): if channel.mute(self.caller):
return True, "" return True, ""
return False, f"Channel {channel.key} was already muted." return False, f"Channel {channel.key} was already muted."
def unmute_channel(self, channel):
def unmute_channel(caller, channel):
""" """
Unmute a channel. Unmute a channel.
Args: Args:
caller (Object or Account): The entity performing the action.
channel (Channel): The channel to alias. channel (Channel): The channel to alias.
Returns: Returns:
@ -238,19 +267,17 @@ def unmute_channel(caller, channel):
the second part is an error string. the second part is an error string.
""" """
if channel.unmute(caller): if channel.unmute(self.caller):
return True, "" return True, ""
return False, f"Channel {channel.key} was already unmuted." return False, f"Channel {channel.key} was already unmuted."
def create_channel(self, name, description, typeclass=None, aliases=None):
def create_channel(caller, name, description, aliases=None):
""" """
Create a new channel. Its name must not previously exist Create a new channel. Its name must not previously exist
(users can alias as needed). Will also connect to the (users can alias as needed). Will also connect to the
new channel. new channel.
Args: Args:
caller (Object or Account): The entity performing the action.
name (str): The new channel name/key. name (str): The new channel name/key.
description (str): This is used in listings. description (str): This is used in listings.
aliases (list): A list of strings - alternative aliases for the channel aliases (list): A list of strings - alternative aliases for the channel
@ -262,6 +289,10 @@ def create_channel(caller, name, description, aliases=None):
the second part is an error string. the second part is an error string.
""" """
caller = self.caller
if typeclass:
pass
if CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(name, exact=True): if CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(name, exact=True):
return False, f"Channel {name} already exists." return False, f"Channel {name} already exists."
# set up the new channel # set up the new channel
@ -270,19 +301,26 @@ def create_channel(caller, name, description, aliases=None):
new_chan.connect(caller) new_chan.connect(caller)
return new_chan, "" return new_chan, ""
def destroy_channel(caller, channel, message=None): def destroy_channel(self, channel, message=None):
""" """
Destroy an existing channel. Access should be checked before Destroy an existing channel. Access should be checked before
calling this function. calling this function.
Args: Args:
caller (Object or Account): The entity performing the action.
channel (Channel): The channel to alias. channel (Channel): The channel to alias.
message (str, optional): Final message to send onto the channel message (str, optional): Final message to send onto the channel
before destroying it. If not given, a default message is before destroying it. If not given, a default message is
used. Set to the empty string for no message. used. Set to the empty string for no message.
if typeclass:
pass
""" """
caller = self.caller
# set up the new channel
lockstring = "send:all();listen:all();control:id(%s)" % caller.id
channel_key = channel.key channel_key = channel.key
if message is None: if message is None:
message = (f"|rChannel {channel_key} is being destroyed. " message = (f"|rChannel {channel_key} is being destroyed. "
@ -294,13 +332,11 @@ def destroy_channel(caller, channel, message=None):
"Channel {} was deleted by {}".format(channel_key, caller) "Channel {} was deleted by {}".format(channel_key, caller)
) )
def set_lock(self, channel, lockstring):
def set_lock(caller, channel, lockstring):
""" """
Set a lockstring on a channel. Set a lockstring on a channel.
Args: Args:
caller (Object or Account): The entity performing the action.
channel (Channel): The channel to operate on. channel (Channel): The channel to operate on.
lockstring (str): A lockstring on the form 'type:lockfunc();...' lockstring (str): A lockstring on the form 'type:lockfunc();...'
@ -315,8 +351,7 @@ def set_lock(caller, channel, lockstring):
return False, err return False, err
return True, "" return True, ""
def set_desc(self, channel, description):
def set_desc(caller, channel, description):
""" """
Set a channel description. This is shown in listings etc. Set a channel description. This is shown in listings etc.
@ -332,13 +367,12 @@ def set_desc(caller, channel, description):
""" """
channel.db.desc = description channel.db.desc = description
def boot_user(caller, channel, target, quiet=False, reason=""): def boot_user(self, channel, target, quiet=False, reason=""):
""" """
Boot a user from a channel, with optional reason. This will Boot a user from a channel, with optional reason. This will
also remove all their aliases for this channel. also remove all their aliases for this channel.
Args: Args:
caller (Object or Account): The entity performing the action.
channel (Channel): The channel to operate on. channel (Channel): The channel to operate on.
target (Object or Account): The entity to boot. target (Object or Account): The entity to boot.
quiet (bool, optional): Whether or not to announce to channel. quiet (bool, optional): Whether or not to announce to channel.
@ -367,64 +401,327 @@ def boot_user(caller, channel, target, quiet=False, reason=""):
logger.log_sec(f"Channel Boot: {target} (Channel: {channel}, " logger.log_sec(f"Channel Boot: {target} (Channel: {channel}, "
f"Reason: {reason}, Caller: {caller}") f"Reason: {reason}, Caller: {caller}")
def ban_user(self, channel, target, quiet=False, reason=""):
def ban_user(caller, channel, target, quiet=False, reason=""):
""" """
Ban a user from a channel, by locking them out. This will also Ban a user from a channel, by locking them out. This will also
boot them, if they are currently connected. boot them, if they are currently connected.
Args: Args:
caller (Object or Account): The entity performing the action.
channel (Channel): The channel to operate on. channel (Channel): The channel to operate on.
target (Object or Account): The entity to ban target (Object or Account): The entity to ban
quiet (bool, optional): Whether or not to announce to channel. quiet (bool, optional): Whether or not to announce to channel.
reason (str, optional): A reason for the ban reason (str, optional): A reason for the ban
""" Returns:
result, err = boot_user(caller, channel, target, quiet=quiet, reason=reason) bool, str: True, None if banning was successful. If False,
the second part is an error string.
class CmdChannel(COMMAND_DEFAULT_CLASS):
"""
Talk on and manage in-game channels.
Usage:
channel channelname [= <msg>]
channel/history channelname [= index]
channel/sub channelname [= alias]
channel/unsub channelname[,channelname, ...]
channel/alias channelname = alias
channel/unalias channelname = alias
channel/mute channelname[,channelname,...]
channel/unmute channelname[,channelname,...]
channel/create channelname [= description]
channel/destroy channelname [: reason]
channel/lock channelname = lockstring
channel/desc channelname = description
channel/boot[/quiet] channelname = subscribername [: reason]
channel/who channelname
channel/list
channels
This handles all operations on channels.
""" """
key = "channel" boot_user(self.caller, channel, target, quiet=quiet, reason=reason)
aliases = ["chan", "channels"] if channel.ban(target):
locks = "cmd: not pperm(channel_banned)" return True, ""
switch_options = ( return False, f"{target} is already banned from this channel."
"history", "sub", "unsub", "mute", "alias", "unalias", "create",
"destroy", "desc", "boot", "who")
def parse(self): def unban_user(self, channel, target):
super().parse() """
self.channelnames = self.lhslist Un-Ban a user from a channel. This will not reconnect them
to the channel, just allow them to connect again (assuming
they have the suitable 'listen' lock like everyone else).
Args:
channel (Channel): The channel to operate on.
target (Object or Account): The entity to unban
Returns:
bool, str: True, None if unbanning was successful. If False,
the second part is an error string.
"""
if channel.unban(target):
return True, ""
return False, f"{target} was not previously banned from this channel."
def channel_list_who(self, channel):
"""
Show a list of online people is subscribing to a channel. This will check
the 'control' permission of `caller` to determine if only online users
should be returned or everyone.
Args:
channel (Channel): The channel to operate on.
Returns:
list: A list of prepared strings, with name + markers for if they are
muted or offline.
"""
caller = self.caller
mute_list = list(channel.mutelist)
online_list = channel.subscriptions.online()
if channel.access(caller, 'control'):
# for those with channel control, show also offline users
all_subs = list(channel.subscriptions.all())
else:
# for others, only show online users
all_subs = online_list
who_list = []
for subscriber in all_subs:
name = subscriber.get_display_name(caller)
conditions = ("muted" if subscriber in mute_list else "",
"offline" if subscriber not in online_list else "")
cond_text = "(" + ", ".join(conditions) + ")" if conditions else ""
who_list.append(f"{name}{cond_text}")
return who_list
def list_channels(self, channelcls=CHANNEL_DEFAULT_TYPECLASS):
"""
Return a available channels.
Args:
channelcls (Channel, optional): The channel-class to query on. Defaults
to the default channel class from settings.
Returns:
tuple: A tuple `(subbed_chans, available_chans)` with the channels
currently subscribed to, and those we have 'listen' access to but
don't actually sub to yet.
"""
caller = self.caller
subscribed_channels = channelcls.objects.get_subscriptions(caller)
unsubscribed_available_channels = [
chan
for chan in channelcls.objects.get_all_channels()
if chan not in subscribed_channels and chan.access(caller, "listen")
]
return subscribed_channels, unsubscribed_available_channels
def display_subbed_channels(self, subscribed):
"""
Display channels subscribed to.
Args:
subscribed (list): List of subscribed channels
Returns:
EvTable: Table to display.
"""
caller = self.caller
comtable = self.styled_table(
"|wchannel|n",
"|wmy aliases|n",
"|wdescription|n",
align="l",
maxwidth=_DEFAULT_WIDTH
)
for chan in subscribed:
clower = chan.key.lower()
nicks = caller.nicks.get(category="channel", return_obj=True)
comtable.add_row(
*("{}{}".format(
chan.key,
"({})".format(",".join(chan.aliases.all())) if chan.aliases.all() else ""),
",".join(nick.db_key for nick in make_iter(nicks)
if nick and nick.value[3].lower() == clower),
chan.db.desc))
def display_all_channels(self, subscribed, available):
"""
Display all available channels
Args:
subscribed (list): List of subscribed channels
Returns:
EvTable: Table to display.
"""
caller = self.caller
comtable = self.styled_table(
"|wsub|n",
"|wchannel|n",
"|wmy aliases|n",
"|wlocks|n",
"|wdescription|n",
maxwidth=_DEFAULT_WIDTH,
)
channels = subscribed + available
for chan in channels:
clower = chan.key.lower()
nicks = caller.nicks.get(category="channel", return_obj=True)
nicks = nicks or []
if chan not in subscribed:
substatus = "|rNo|n"
elif caller in chan.mutelist:
substatus = "|rMuted|n"
else:
substatus = "|gYes|n"
comtable.add_row(
*(substatus,
"{}{}".format(
chan.key,
"({})".format(",".join(chan.aliases.all())) if chan.aliases.all() else ""),
",".join(nick.db_key for nick in make_iter(nicks)
if nick.value[3].lower() == clower),
str(chan.locks),
chan.db.desc))
comtable.reformat_column(0, width=9)
comtable.reformat_column(3, width=14)
return comtable
def func(self): def func(self):
pass """
Main functionality of command.
"""
caller = self.caller
switches = self.switches
channel_names = self.lhslist
if not channel_names:
if 'all' in switches:
# show all available channels
subscribed, available = self.list_channels()
table = self.display_all_channels(subscribed, available)
self.msg(
"\n|wAvailable channels|n (use /list to "
f"only show subscriptions)\n{table}")
return
else:
# (empty or /list) show only subscribed channels
subscribed, _ = self.list_channels()
table = self.display_subbed_channels(subscribed)
self.msg("\n|wChannel subscriptions|n "
f"(use |w/all|n to see all available)\n{table}")
return
if 'create' in switches:
# create a new channel
config = self.lhs
if not config:
self.msg("To create: channel/create name[;aliases][:typeclass] [= description]")
return
name, *typeclass = config.rsplit(":", 1)
typeclass = typeclass[0] if typeclass else None
name, *aliases = name.rsplit(";")
description = self.rhs or ""
self.create_channel(name, description, typeclass=typeclass, aliases=aliases)
channels = []
for channel_name in channel_names:
# find a channel by fuzzy-matching. This also checks
# 'listen/control' perms.
channel = self.search_channel(channel_name, exact=False)
if not channel:
self.msg(f"No channel found matching '{channel_name}'.")
return
elif len(channel) > 1:
self.msg("Multiple possible channel matches/alias for "
"'{channel_name}':\n" + ", ".join(chan.key for chan in channel))
return
channels.append(channel)
# we have at least one channel at this point
channel = channels[0]
if not switches:
# send a message to channel(s)
message = self.rhs
if not message:
self.msg("To send: channel <channel-name-or-alias> = <message>")
return
for chan in channels:
self.msg_channel(chan, message)
return
if 'history' in switches or 'hist' in switches:
# view channel history
index = self.rhs or 0
try:
index = max(0, int(index))
except ValueError:
self.msg("The history index (describing how many lines to go back) "
"must be an integer >= 0.")
return
self.get_channel_history(channel, start_index=index)
return
if 'sub' in switches:
# subscribe to a channel
success, err = self.sub_to_channel(channel)
if success:
self.msg("You are now subscribed "
f"to the channel {channel.key}. Use /alias to "
"be able to use different names to refer to the channel.")
else:
self.msg(err)
return
if 'unsub' in switches:
# un-subscribe from a channel
success, err = self.unsub_from_channel(channel)
if success:
self.msg(f"You un-subscribed from channel {channel.key}. "
"All aliases were cleared.")
else:
self.msg(err)
return
if 'alias' in switches:
# create a new personal alias for a channel
alias = self.rhs
if not alias:
self.msg("Specify the alias as channel/alias channelname = alias")
return
self.add_alias(channel, alias)
self.msg(f"Added/updated your alias '{alias}' for channel {channel.key}.")
return
if 'unalias' in switches:
# remove a personal alias for a channel
alias = self.rhs
if not alias:
self.msg("Specify the alias to remove as channel/unalias channelname = alias")
return
success, err = self.remove_alias(channel, alias)
if success:
self.msg(f"Removed your alias '{alias}' for channel {channel.key}")
else:
self.msg(err)
return
if 'mute' in switches:
# mute a given channel
success, err = self.mute_channel(channel)
if success:
self.msg(f"Muted channel {channel.key}.")
else:
self.msg(err)
return
if 'unmute' in switches:
# unmute a given channel
success, err = self.unmute_channel(channel)
if success:
self.msg(f"Un-muted channel {channel.key}.")
else:
self.msg(err)
return
new_chan, err = self.create_channel()

View file

@ -248,7 +248,7 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
""" """
# check access # check access
if not self.access(subscriber, "listen"): if subscriber in self.banlist or not self.access(subscriber, "listen"):
return False return False
# pre-join hook # pre-join hook
connect = self.pre_join_channel(subscriber) connect = self.pre_join_channel(subscriber)