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.logger import tail_log_file
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)
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
# 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
handling.
@ -64,9 +101,10 @@ def search_channel(caller, channelname, exact=False):
list: A list of zero, one or more channels found.
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
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
channels = CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(channelname, exact=exact)
# check permissions
channels = [channel for channel in channels
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 [channels[0]]
def msg_channel(caller, channel, message, **kwargs):
def msg_channel(self, channel, message, **kwargs):
"""
Send a message to a given channel. At this point
any permissions should already be done.
Args:
caller (Object or Account): The entity sending the message.
channel (Channel): The channel to send to.
message (str): The message to send.
**kwargs: Unused by default. These kwargs will be passed into
all channel messaging hooks for custom overriding.
"""
channel.msg(message, senders=caller, **kwargs)
channel.msg(message, senders=self.caller, **kwargs)
def get_channel_history(caller, channel, start_index=0):
def get_channel_history(self, channel, start_index=0):
"""
View a channel's history.
Args:
caller (Object or Account): The entity performing the action.
channel (Channel): The channel to access.
message (str): The message to send.
**kwargs: Unused by default. These kwargs will be passed into
all channel messaging hooks for custom overriding.
"""
caller = self.caller
log_file = channel.attributes.get(
"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
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
be checked before this step.
Args:
caller (Object or Account): The entity performing the action.
channel (Channel): The channel to access.
Returns:
@ -141,19 +175,19 @@ def sub_to_channel(caller, channel):
the second part is an error string.
"""
caller = self.caller
if channel.has_connection(caller):
return False, f"Already listening to channel {channel.key}."
result = channel.connect(caller)
return result, "" if result else f"Were not allowed to subscribe to channel {channel.key}"
def unsub_to_channel(caller, channel):
def unsub_from_channel(self, channel):
"""
Un-Subscribe to a channel. Note that all permissions should
be checked before this step.
Args:
caller (Object or Account): The entity performing the action.
channel (Channel): The channel to unsub from.
Returns:
@ -161,6 +195,8 @@ def unsub_to_channel(caller, channel):
the second part is an error string.
"""
caller = self.caller
if not channel.has_connection(caller):
return False, f"Not listening to channel {channel.key}."
# clear nicks
@ -174,26 +210,22 @@ def unsub_to_channel(caller, channel):
result = channel.disconnect(caller)
return result, "" if result else f"Could not unsubscribe from channel {channel.key}"
def add_alias(caller, channel, alias):
def add_alias(self, channel, alias):
"""
Add a new alias for the user to use with this channel.
Args:
caller (Object or Account): The entity performing the action.
channel (Channel): The channel to alias.
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(caller, alias):
def remove_alias(self, alias):
"""
Remove an alias from a given channel.
Args:
caller (Object or Account): The entity performing the action.
alias (str, optional): The alias to remove, or `None` for all.
Returns:
@ -201,36 +233,33 @@ def remove_alias(caller, alias):
the second part is an error string.
"""
caller = self.caller
channame = caller.nicks.get(key=alias, category="channel")
if channame:
caller.nicks.remove(key=alias, category="channel")
return True, ""
return False, "No such alias was defined."
def mute_channel(caller, channel):
def mute_channel(self, channel):
"""
Temporarily mute a channel.
Args:
caller (Object or Account): The entity performing the action.
channel (Channel): The channel to alias.
Returns:
bool, str: True, None if muting successful. If False,
the second part is an error string.
"""
if channel.mute(caller):
if channel.mute(self.caller):
return True, ""
return False, f"Channel {channel.key} was already muted."
def unmute_channel(caller, channel):
def unmute_channel(self, channel):
"""
Unmute a channel.
Args:
caller (Object or Account): The entity performing the action.
channel (Channel): The channel to alias.
Returns:
@ -238,19 +267,17 @@ def unmute_channel(caller, channel):
the second part is an error string.
"""
if channel.unmute(caller):
if channel.unmute(self.caller):
return True, ""
return False, f"Channel {channel.key} was already unmuted."
def create_channel(caller, name, description, aliases=None):
def create_channel(self, name, description, typeclass=None, aliases=None):
"""
Create a new channel. Its name must not previously exist
(users can alias as needed). Will also connect to the
new channel.
Args:
caller (Object or Account): The entity performing the action.
name (str): The new channel name/key.
description (str): This is used in listings.
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.
"""
caller = self.caller
if typeclass:
pass
if CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(name, exact=True):
return False, f"Channel {name} already exists."
# set up the new channel
@ -270,19 +301,26 @@ def create_channel(caller, name, description, aliases=None):
new_chan.connect(caller)
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
calling this function.
Args:
caller (Object or Account): The entity performing the action.
channel (Channel): The channel to alias.
message (str, optional): Final message to send onto the channel
before destroying it. If not given, a default message is
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
if message is None:
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)
)
def set_lock(caller, channel, lockstring):
def set_lock(self, channel, lockstring):
"""
Set a lockstring on a channel.
Args:
caller (Object or Account): The entity performing the action.
channel (Channel): The channel to operate on.
lockstring (str): A lockstring on the form 'type:lockfunc();...'
@ -315,8 +351,7 @@ def set_lock(caller, channel, lockstring):
return False, err
return True, ""
def set_desc(caller, channel, description):
def set_desc(self, channel, description):
"""
Set a channel description. This is shown in listings etc.
@ -332,13 +367,12 @@ def set_desc(caller, channel, 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
also remove all their aliases for this channel.
Args:
caller (Object or Account): The entity performing the action.
channel (Channel): The channel to operate on.
target (Object or Account): The entity to boot.
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}, "
f"Reason: {reason}, Caller: {caller}")
def ban_user(caller, channel, target, quiet=False, reason=""):
def ban_user(self, channel, target, quiet=False, reason=""):
"""
Ban a user from a channel, by locking them out. This will also
boot them, if they are currently connected.
Args:
caller (Object or Account): The entity performing the action.
channel (Channel): The channel to operate on.
target (Object or Account): The entity to ban
quiet (bool, optional): Whether or not to announce to channel.
reason (str, optional): A reason for the ban
"""
result, err = boot_user(caller, channel, target, quiet=quiet, reason=reason)
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.
Returns:
bool, str: True, None if banning was successful. If False,
the second part is an error string.
"""
key = "channel"
aliases = ["chan", "channels"]
locks = "cmd: not pperm(channel_banned)"
switch_options = (
"history", "sub", "unsub", "mute", "alias", "unalias", "create",
"destroy", "desc", "boot", "who")
boot_user(self.caller, channel, target, quiet=quiet, reason=reason)
if channel.ban(target):
return True, ""
return False, f"{target} is already banned from this channel."
def parse(self):
super().parse()
self.channelnames = self.lhslist
def unban_user(self, channel, target):
"""
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):
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
if not self.access(subscriber, "listen"):
if subscriber in self.banlist or not self.access(subscriber, "listen"):
return False
# pre-join hook
connect = self.pre_join_channel(subscriber)