Add channel sub-permission checks for admin/manage switches

This commit is contained in:
Griatch 2021-05-11 23:19:09 +02:00
parent d118fb17b8
commit adf484b9df
9 changed files with 186 additions and 642 deletions

View file

@ -1,4 +1,4 @@
# Channels
# Channels
In a multiplayer game, players often need other means of in-game communication
than moving to the same room and use `say` or `emote`.
@ -34,7 +34,7 @@ In the default command set, channels are all handled via the mighty
`chan`). By default, this command will assume all entities dealing with
channels are `Accounts`.
### Viewing, joining and creating channels
### Viewing and joining channels
channel - shows your subscriptions
channel/all - shows all subs available to you
@ -51,18 +51,9 @@ unsubscribing), you can mute it:
channel/mute channelname
channel/unmute channelname
To create/destroy a new channel you can do
channel/create channelname;alias;alias = description
channel/destroy channelname
Aliases are optional but can be good for obvious shortcuts everyone may want to
use. The description is used in channel-listings. You will automatically join a
channel you created and will be controlling it.
### Chat on channels
To speak on a channel, do
To speak on a channel, do
channel public Hello world!
@ -86,7 +77,7 @@ Any user can make up their own channel aliases:
channel/alias public = foo;bar
You can now just do
You can now just do
foo Hello world!
bar Hello again!
@ -97,19 +88,19 @@ And even remove the default one if they don't want to use it
public Hello
But you can also use your alias with the `channel` command:
channel foo Hello world!
> What happens when aliasing is that a [nick](./Nicks) is created that maps your
> alias + argument onto calling the `channel` command. So when you enter `foo hello`,
> what the server sees is actually `channel foo = hello`. The system is also
> what the server sees is actually `channel foo = hello`. The system is also
> clever enough to know that whenever you search for channels, your channel-nicks
> should also be considered so as to convert your input to an existing channel name.
You can check if you missed channel conversations by viewing the channel's
scrollback with
channel/history public
channel/history public
This retrieves the last 20 lines of text (also from a time when you were
offline). You can step further back by specifying how many lines back to start:
@ -122,23 +113,20 @@ This again retrieve 20 lines, but starting 30 lines back (so you'll get lines
### Channel administration
If you control the channel (because you are an admin or created it) you have the
ability to control who can access it by use of [locks](./Locks):
To create/destroy a new channel you can do
channel/lock buildchannel = listen:all();send:perm(Builders)
channel/create channelname;alias;alias = description
channel/destroy channelname
Channels use three lock-types by default:
- `listen` - who may listen to the channel. Users without this access will not
even be able to join the channel and it will not appear in listings for them.
- `send` - who may send to the channel.
- `control` - this is assigned to you automatically when you create the channel. With
control over the channel you can edit it, boot users and do other management tasks.
Aliases are optional but can be good for obvious shortcuts everyone may want to
use. The description is used in channel-listings. You will automatically join a
channel you created and will be controlling it. You can also use `channel/desc` to
change the description on a channel you wnn later.
If you control a channel you can also kick people off it:
channel/boot mychannel = annoyinguser123 : stop spamming!
The last part is an optional reason to send to the user before they are booted.
You can give a comma-separated list of channels to kick the same user from all
those channels at once. The user will be unsubbed from the channel and all
@ -155,6 +143,64 @@ actually kick them out.
See the [Channel command](api:evennia.commands.default.comms.CmdChannel) api
docs (and in-game help) for more details.
Admin-level users can also modify channel's [locks](./Locks):
channel/lock buildchannel = listen:all();send:perm(Builders)
Channels use three lock-types by default:
- `listen` - who may listen to the channel. Users without this access will not
even be able to join the channel and it will not appear in listings for them.
- `send` - who may send to the channel.
- `control` - this is assigned to you automatically when you create the channel. With
control over the channel you can edit it, boot users and do other management tasks.
#### Restricting channel administration
By default everyone can use the channel command ([evennia.commands.default.comms.CmdChannel](api:evennia.commands.default.comms.CmdChannel))
to create channels and will then control the channels they created (to boot/ban
people etc). If you as a developer does not want regular players to do this
(perhaps you want only staff to be able to spawn new channels), you can
override the `channel` command and change its `locks` property.
The default `help` command has the following `locks` property:
```python
locks = "cmd:not perm(channel_banned); admin:all(); manage:all(); changelocks: perm(Admin)"
```
This is a regular [lockstring](Locks).
- `cmd: pperm(channel_banned)` - The `cmd` locktype is the standard one used for all Commands.
an accessing object failing this will not even know that the command exists. The `pperm()` lockfunc
checks an on-account [Permission](Building Permissions) 'channel_banned' - and the `not` means
that if they _have_ that 'permission' they are cut off from using the `channel` command. You usually
don't need to change this lock.
- `admin:all()` - this is a lock checked in the `channel` command itself. It controls access to the
`/boot`, `/ban` and `/unban` switches (by default letting everyone use them).
- `manage:all()` - this controls access to the `/create`, `/destroy`, `/desc` switches.
- `changelocks: perm(Admin)` - this controls access to the `/lock` and `/unlock` switches. By
default this is something only [Admins](Building Permissions) can change.
> Note - while `admin:all()` and `manage:all()` will let everyone use these switches, users
> will still only be able to admin or destroy channels they actually control!
If you only want (say) Builders and higher to be able to create and admin
channels you could override the `help` command and change the lockstring to:
```python
# in for example mygame/commands/commands.py
from evennia import default_cmds
class MyCustomChannelCmd(default_cmds.CmdChannel):
locks = "cmd: not pperm(channel_banned);admin:perm(Builder);manage:perm(Builder);changelocks:perm(Admin)"
```
Add this custom command to your default cmdset and regular users wil now get an
access-denied error when trying to use use these switches.
## Allowing Characters to use Channels
@ -174,7 +220,7 @@ When distributing a message, the channel will call a series of hooks on itself
and (more importantly) on each recipient. So you can customize things a lot by
just modifying hooks on your normal Object/Account typeclasses.
Internally, the message is sent with
Internally, the message is sent with
`channel.msg(message, senders=sender, bypass_mute=False, **kwargs)`, where
`bypass_mute=True` means the message ignores muting (good for alerts or if you
delete the channel etc) and `**kwargs` are any extra info you may want to pass
@ -182,7 +228,7 @@ to the hooks. The `senders` (it's always only one in the default implementation
but could in principle be multiple) and `bypass_mute` are part of the `kwargs`
below:
1. `channel.at_pre_msg(message, **kwargs)`
1. `channel.at_pre_msg(message, **kwargs)`
2. For each recipient:
- `message = recipient.at_pre_channel_msg(message, channel, **kwargs)` -
allows for the message to be tweaked per-receiver (for example coloring it depending
@ -190,7 +236,7 @@ below:
recipient is skipped.
- `recipient.channel_msg(message, channel, **kwargs)` - actually sends to recipient.
- `recipient.at_post_channel_msg(message, channel, **kwargs)` - any post-receive effects.
3. `channel.at_post_channel_msg(message, **kwargs)`
3. `channel.at_post_channel_msg(message, **kwargs)`
Note that `Accounts` and `Objects` both have their have separate sets of hooks.
So make sure you modify the set actually used by your subcribers (or both).
@ -209,10 +255,10 @@ and can be easily extended.
To change which channel typeclass Evennia uses for default commands, change
`settings.BASE_CHANNEL_TYPECLASS`. The base command class is
[`evennia.comms.comms.DefaultChannel`](api:evennia.comms.comms.DefaultChannel).
There is an empty child class in `mygame/typeclasses/channels.py`, same
There is an empty child class in `mygame/typeclasses/channels.py`, same
as for other typelass-bases.
In code you create a new channel with `evennia.create_channel` or
In code you create a new channel with `evennia.create_channel` or
`Channel.create`:
```python
@ -232,7 +278,7 @@ In code you create a new channel with `evennia.create_channel` or
# view subscriptions (the SubscriptionHandler handles all subs under the hood)
channel.subscriptions.has(me) # check we subbed
channel.subscriptions.all() # get all subs
channel.subscriptions.all() # get all subs
channel.subscriptions.online() # get only subs currently online
channel.subscriptions.clear() # unsub all
@ -264,7 +310,7 @@ details.
The channel messages are not stored in the database. A channel is instead
always logged to a regular text log-file
`mygame/server/logs/channel_<channelname>.log`. This is where `channels/history channelname`
`mygame/server/logs/channel_<channelname>.log`. This is where `channels/history channelname`
gets its data from. A channel's log will rotate when it grows too big, which
thus also automatically limits the max amount of history a user can view with
`/history`.
@ -279,7 +325,7 @@ see the [Channel api docs](api:evennia.comms.comms.DefaultChannel) for details.
sensible optimization since people offline people will not see the message anyway.
- `log_to_file` - this is a string that determines the name of the channel log file. Default
is `"channel_{channel_key}.log"`. You should usually not change this.
- `channel_prefix_string` - this property is a string to easily change how
- `channel_prefix_string` - this property is a string to easily change how
the channel is prefixed. It takes the `channel_key` format key. Default is `"[{channel_key}] "`
and produces output like `[public] ...``.
- `subscriptions` - this is the [SubscriptionHandler](`api:evennia.comms.comms.SubscriptionHandler`), which
@ -297,7 +343,7 @@ Notable `Channel` hooks:
- `at_post_channel_msg(message, **kwargs)` - by default this is used to store the message
to the log file.
- `channel_prefix(message)` - this is called to allow the channel to prefix. This is called
by the object/account when they build the message, so if wanting something else one can
by the object/account when they build the message, so if wanting something else one can
also just remove that call.
- every channel message. By default it just returns `channel_prefix_string`.
- `has_connection(subscriber)` - shortcut to check if an entity subscribes to
@ -309,4 +355,4 @@ Notable `Channel` hooks:
- `post_join_channel(subscriber)` - unused by default.
- `pre_leave_channel(subscriber)` - if this returns `False`, the user is not allowed to leave.
- `post_leave_channel(subscriber)` - unused by default.

View file

@ -1,484 +0,0 @@
# Customize channels
# Channel commands in Evennia
By default, Evennia's default channel commands are inspired by MUX. They all
begin with "c" followed by the action to perform (like "ccreate" or "cdesc").
If this default seems strange to you compared to other Evennia commands that
rely on switches, you might want to check this tutorial out.
This tutorial will also give you insight into the workings of the channel system.
So it may be useful even if you don't plan to make the exact changes shown here.
## What we will try to do
Our mission: change the default channel commands to have a different syntax.
This tutorial will do the following changes:
- Remove all the default commands to handle channels.
- Add a `+` and `-` command to join and leave a channel. So, assuming there is
a `public` channel on your game (most often the case), you could type `+public`
to join it and `-public` to leave it.
- Group the commands to manipulate channels under the channel name, after a
switch. For instance, instead of writing `cdesc public = My public channel`,
you would write `public/desc My public channel`.
> I listed removing the default Evennia commands as a first step in the
> process. Actually, we'll move it at the very bottom of the list, since we
> still want to use them, we might get it wrong and rely on Evennia commands
> for a while longer.
## A command to join, another to leave
We'll do the most simple task at first: create two commands, one to join a
channel, one to leave.
> Why not have them as switches? `public/join` and `public/leave` for instance?
For security reasons, I will hide channels to which the caller is not
connected. It means that if the caller is not connected to the "public"
channel, he won't be able to use the "public" command. This is somewhat
standard: if we create an administrator-only channel, we don't want players to
try (or even know) the channel command. Again, you could design it a different
way should you want to.
First create a file named `comms.py` in your `commands` package. It's
a rather logical place, since we'll write different commands to handle
communication.
Okay, let's add the first command to join a channel:
```python
# in commands/comms.py
from evennia.utils.search import search_channel
from commands.command import Command
class CmdConnect(Command):
"""
Connect to a channel.
"""
key = "+"
help_category = "Comms"
locks = "cmd:not pperm(channel_banned)"
auto_help = False
def func(self):
"""Implement the command"""
caller = self.caller
args = self.args
if not args:
self.msg("Which channel do you want to connect to?")
return
channelname = self.args
channel = search_channel(channelname)
if not channel:
return
# Check permissions
if not channel.access(caller, 'listen'):
self.msg("%s: You are not allowed to listen to this channel." % channel.key)
return
# If not connected to the channel, try to connect
if not channel.has_connection(caller):
if not channel.connect(caller):
self.msg("%s: You are not allowed to join this channel." % channel.key)
return
else:
self.msg("You now are connected to the %s channel. " % channel.key.lower())
else:
self.msg("You already are connected to the %s channel. " % channel.key.lower())
```
Okay, let's review this code, but if you're used to Evennia commands, it shouldn't be too strange:
1. We import `search_channel`. This is a little helper function that we will use to search for
channels by name and aliases, found in `evennia.utils.search`. It's just more convenient.
2. Our class `CmdConnect` contains the body of our command to join a channel.
3. Notice the key of this command is simply `"+"`. When you enter `+something` in the game, it will
try to find a command key `+something`. Failing that, it will look at other potential matches.
Evennia is smart enough to understand that when we type `+something`, `+` is the command key and
`something` is the command argument. This will, of course, fail if you have a command beginning by
`+` conflicting with the `CmdConnect` key.
4. We have altered some class attributes, like `auto_help`. If you want to know what they do and
why they have changed here, you can check the [documentation on commands](../Components/Commands).
5. In the command body, we begin by extracting the channel name. Remember that this name should be
in the command arguments (that is, in `self.args`). Following the same example, if a player enters
`+something`, `self.args` should contain `"something"`. We use `search_channel` to see if this
channel exists.
6. We then check the access level of the channel, to see if the caller can listen to it (not
necessarily use it to speak, mind you, just listen to others speak, as these are two different locks
on Evennia).
7. Finally, we connect the caller if he's not already connected to the channel. We use the
channel's `connect` method to do this. Pretty straightforward eh?
Now we'll add a command to leave a channel. It's almost the same, turned upside down:
```python
class CmdDisconnect(Command):
"""
Disconnect from a channel.
"""
key = "-"
help_category = "Comms"
locks = "cmd:not pperm(channel_banned)"
auto_help = False
def func(self):
"""Implement the command"""
caller = self.caller
args = self.args
if not args:
self.msg("Which channel do you want to disconnect from?")
return
channelname = self.args
channel = search_channel(channelname)
if not channel:
return
# If connected to the channel, try to disconnect
if channel.has_connection(caller):
if not channel.disconnect(caller):
self.msg("%s: You are not allowed to disconnect from this channel." % channel.key)
return
else:
self.msg("You stop listening to the %s channel. " % channel.key.lower())
else:
self.msg("You are not connected to the %s channel. " % channel.key.lower())
```
So far, you shouldn't have trouble following what this command does: it's
pretty much the same as the `CmdConnect` class in logic, though it accomplishes
the opposite. If you are connected to the channel `public` you could
disconnect from it using `-public`. Remember, you can use channel aliases too
(`+pub` and `-pub` will also work, assuming you have the alias `pub` on the
`public` channel).
It's time to test this code, and to do so, you will need to add these two
commands. Here is a good time to say it: by default, Evennia connects accounts
to channels. Some other games (usually with a higher multisession mode) will
want to connect characters instead of accounts, so that several characters in
the same account can be connected to various channels. You can definitely add
these commands either in the `AccountCmdSet` or `CharacterCmdSet`, the caller
will be different and the command will add or remove accounts of characters.
If you decide to install these commands on the `CharacterCmdSet`, you might
have to disconnect your superuser account (account #1) from the channel before
joining it with your characters, as Evennia tends to subscribe all accounts
automatically if you don't tell it otherwise.
So here's an example of how to add these commands into your `AccountCmdSet`.
Edit the file `commands/default_cmdsets.py` to change a few things:
```python
# In commands/default_cmdsets.py
from evennia import default_cmds
from commands.comms import CmdConnect, CmdDisconnect
# ... Skip to the AccountCmdSet class ...
class AccountCmdSet(default_cmds.AccountCmdSet):
"""
This is the cmdset available to the Account at all times. It is
combined with the `CharacterCmdSet` when the Account puppets a
Character. It holds game-account-specific commands, channel
commands, etc.
"""
key = "DefaultAccount"
def at_cmdset_creation(self):
"""
Populates the cmdset
"""
super().at_cmdset_creation()
# Channel commands
self.add(CmdConnect())
self.add(CmdDisconnect())
```
Save, reload your game, and you should be able to use `+public` and `-public`
now!
## A generic channel command with switches
It's time to dive a little deeper into channel processing. What happens in
Evennia when a player enters `public Hello everybody!`?
Like exits, channels are a particular command that Evennia automatically
creates and attaches to individual channels. So when you enter `public
message` in your game, Evennia calls the `public` command.
> But I didn't add any public command...
Evennia will just create these commands automatically based on the existing
channels. The base command is the command we'll need to edit.
> Why edit it? It works just fine to talk.
Unfortunately, if we want to add switches to our channel names, we'll have to
edit this command. It's not too hard, however, we'll just start writing a
standard command with minor twitches.
### Some additional imports
You'll need to add a line of import in your `commands/comms.py` file. We'll
see why this import is important when diving in the command itself:
```python
from evennia.comms.models import ChannelDB
```
### The class layout
```python
# In commands/comms.py
class ChannelCommand(Command):
"""
{channelkey} channel
{channeldesc}
Usage:
{lower_channelkey} <message>
{lower_channelkey}/history [start]
{lower_channelkey}/me <message>
{lower_channelkey}/who
Switch:
history: View 20 previous messages, either from the end or
from <start> number of messages from the end.
me: Perform an emote on this channel.
who: View who is connected to this channel.
Example:
{lower_channelkey} Hello World!
{lower_channelkey}/history
{lower_channelkey}/history 30
{lower_channelkey}/me grins.
{lower_channelkey}/who
"""
# note that channeldesc and lower_channelkey will be filled
# automatically by ChannelHandler
# this flag is what identifies this cmd as a channel cmd
# and branches off to the system send-to-channel command
# (which is customizable by admin)
is_channel = True
key = "general"
help_category = "Channel Names"
obj = None
arg_regex = ""
```
There are some differences here compared to most common commands.
- There is something disconcerting in the class docstring. Some information is
between curly braces. This is a format-style which is only used for channel
commands. `{channelkey}` will be replaced by the actual channel key (like
public). `{channeldesc}` will be replaced by the channel description (like
"public channel"). And `{lower_channelkey}`.
- We have set `is_channel` to `True` in the command class variables. You
shouldn't worry too much about that: it just tells Evennia this is a special
command just for channels.
- `key` is a bit misleading because it will be replaced eventually. So we
could set it to virtually anything.
- The `obj` class variable is another one we won't detail right now.
- `arg_regex` is important: the default `arg_regex` in the channel command will
forbid to use switches (a slash just after the channel name is not allowed).
That's why we enforce it here, we allow any syntax.
> What will become of this command?
Well, when we'll be through with it, and once we'll add it as the default
command to handle channels, Evennia will create one per existing channel. For
instance, the public channel will receive one command of this class, with `key`
set to `public` and `aliases` set to the channel aliases (like `['pub']`).
> Can I see it work?
Not just yet, there's still a lot of code needed.
Okay we have the command structure but it's rather empty.
### The parse method
The `parse` method is called before `func` in every command. Its job is to
parse arguments and in our case, we will analyze switches here.
```python
# ...
def parse(self):
"""
Simple parser
"""
# channel-handler sends channame:msg here.
channelname, msg = self.args.split(":", 1)
self.switch = None
if msg.startswith("/"):
try:
switch, msg = msg[1:].split(" ", 1)
except ValueError:
switch = msg[1:]
msg = ""
self.switch = switch.lower().strip()
self.args = (channelname.strip(), msg.strip())
```
Reading the comments we see that the channel handler will send the command in a
strange way: a string with the channel name, a colon and the actual message
entered by the player. So if the player enters "public hello", the command
`args` will contain `"public:hello"`. You can look at the way the channel name
and message are parsed, this can be used in a lot of different commands.
Next we check if there's any switch, that is, if the message starts with a
slash. This would be the case if a player entered `public/me jumps up and
down`, for instance. If there is a switch, we save it in `self.switch`. We
alter `self.args` at the end to contain a tuple with two values: the channel
name, and the message (if a switch was used, notice that the switch will be
stored in `self.switch`, not in the second element of `self.args`).
### The command func
Finally, let's see the `func` method in the command class. It will have to
handle switches and also the raw message to send if no switch was used.
```python
# ...
def func(self):
"""
Create a new message and send it to channel, using
the already formatted input.
"""
channelkey, msg = self.args
caller = self.caller
channel = ChannelDB.objects.get_channel(channelkey)
# Check that the channel exists
if not channel:
self.msg(_("Channel '%s' not found.") % channelkey)
return
# Check that the caller is connected
if not channel.has_connection(caller):
string = "You are not connected to channel '%s'."
self.msg(string % channelkey)
return
# Check that the caller has send access
if not channel.access(caller, 'send'):
string = "You are not permitted to send to channel '%s'."
self.msg(string % channelkey)
return
# Handle the various switches
if self.switch == "me":
if not msg:
self.msg("What do you want to do on this channel?")
else:
msg = "{} {}".format(caller.key, msg)
channel.msg(msg, online=True)
elif self.switch:
self.msg("{}: Invalid switch {}.".format(channel.key, self.switch))
elif not msg:
self.msg("Say what?")
else:
if caller in channel.mutelist:
self.msg("You currently have %s muted." % channel)
return
channel.msg(msg, senders=self.caller, online=True)
```
- First of all, we try to get the channel object from the channel name we have
in the `self.args` tuple. We use `ChannelDB.objects.get_channel` this time
because we know the channel name isn't an alias (that was part of the deal,
`channelname` in the `parse` method contains a command key).
- We check that the channel does exist.
- We then check that the caller is connected to the channel. Remember, if the
caller isn't connected, we shouldn't allow him to use this command (that
includes the switches on channels).
- We then check that the caller has access to the channel's `send` lock. This
time, we make sure the caller can send messages to the channel, no matter what
operation he's trying to perform.
- Finally we handle switches. We try only one switch: `me`. This switch would
be used if a player entered `public/me jumps up and down` (to do a channel
emote).
- We handle the case where the switch is unknown and where there's no switch
(the player simply wants to talk on this channel).
The good news: The code is not too complicated by itself. The bad news is that
this is just an abridged version of the code. If you want to handle all the
switches mentioned in the command help, you will have more code to write. This
is left as an exercise.
### End of class
It's almost done, but we need to add a method in this command class that isn't
often used. I won't detail it's usage too much, just know that Evennia will use
it and will get angry if you don't add it. So at the end of your class, just
add:
```python
# ...
def get_extra_info(self, caller, **kwargs):
"""
Let users know that this command is for communicating on a channel.
Args:
caller (TypedObject): A Character or Account who has entered an ambiguous command.
Returns:
A string with identifying information to disambiguate the object, conventionally with a
preceding space.
"""
return " (channel)"
```
### Adding this channel command
Contrary to most Evennia commands, we won't add our `ChannelCommand` to a
`CmdSet`. Instead we need to tell Evennia that it should use the command we
just created instead of its default channel-command.
In your `server/conf/settings.py` file, add a new setting:
```python
# Channel options
CHANNEL_COMMAND_CLASS = "commands.comms.ChannelCommand"
```
Then you can reload your game. Try to type `public hello` and `public/me jumps
up and down`. Don't forget to enter `help public` to see if your command has
truly been added.
## Conclusion and full code
That was some adventure! And there's still things to do! But hopefully, this
tutorial will have helped you in designing your own channel system. Here are a
few things to do:
- Add more switches to handle various actions, like changing the description of
a channel for instance, or listing the connected participants.
- Remove the default Evennia commands to handle channels.
- Alter the behavior of the channel system so it better aligns with what you
want to do.
As a special bonus, you can find a full, working example of a communication
system similar to the one I've shown you: this is a working example, it
integrates all switches and does ever some extra checking, but it's also very
close from the code I've provided here. Notice, however, that this resource is
external to Evennia and not maintained by anyone but the original author of
this article.
[Read the full example on Github](https://github.com/vincent-
lg/avenew/blob/master/commands/comms.py)

View file

@ -339,7 +339,6 @@ def _init():
CMD_NOINPUT - no input was given on command line
CMD_NOMATCH - no valid command key was found
CMD_MULTIMATCH - multiple command matches were found
CMD_CHANNEL - the command name is a channel name
CMD_LOGINSTART - this command will be called as the very
first command when an account connects to
the server.
@ -354,7 +353,6 @@ def _init():
CMD_NOINPUT = cmdhandler.CMD_NOINPUT
CMD_NOMATCH = cmdhandler.CMD_NOMATCH
CMD_MULTIMATCH = cmdhandler.CMD_MULTIMATCH
CMD_CHANNEL = cmdhandler.CMD_CHANNEL
CMD_LOGINSTART = cmdhandler.CMD_LOGINSTART
del cmdhandler

View file

@ -6,10 +6,6 @@ command line. The processing of a command works as follows:
1. The calling object (caller) is analyzed based on its callertype.
2. Cmdsets are gathered from different sources:
- channels: all available channel names are auto-created into a cmdset, to allow
for giving the channel name and have the following immediately
sent to the channel. The sending is performed by the CMD_CHANNEL
system command.
- object cmdsets: all objects at caller's location are scanned for non-empty
cmdsets. This includes cmdsets on exits.
- caller: the caller is searched for its own currently active cmdset.
@ -72,8 +68,6 @@ CMD_NOINPUT = "__noinput_command"
CMD_NOMATCH = "__nomatch_command"
# command to call if multiple command matches were found
CMD_MULTIMATCH = "__multimatch_command"
# command to call if found command is the name of a channel
CMD_CHANNEL = "__send_to_channel_command"
# command to call as the very first one when the user connects.
# (is expected to display the login screen)
CMD_LOGINSTART = "__unloggedin_look_command"
@ -757,18 +751,6 @@ def cmdhandler(
sysarg += _(' Type "help" for help.')
raise ExecSystemCommand(syscmd, sysarg)
# Check if this is a Channel-cmd match.
if hasattr(cmd, "is_channel") and cmd.is_channel:
# even if a user-defined syscmd is not defined, the
# found cmd is already a system command in its own right.
syscmd = yield cmdset.get(CMD_CHANNEL)
if syscmd:
# replace system command with custom version
cmd = syscmd
cmd.session = session
sysarg = "%s:%s" % (cmdname, args)
raise ExecSystemCommand(cmd, sysarg)
# A normal command.
ret = yield _run_command(cmd, cmdname, args, raw_cmdname, cmdset, session, account)
returnValue(ret)

View file

@ -170,11 +170,34 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
Creates a new channel (or destroys one you control).
## lock and unlock
Usage: channel/lock channelname = lockstring
channel/unlock channelname = lockstring
Note: this is an admin command.
A lockstring is on the form locktype:lockfunc(). Channels understand three
locktypes:
listen - who may listen or join the channel.
send - who may send messages to the channel
control - who controls the channel. This is usually the one creating
the channel.
Common lockfuncs are all() and perm(). To make a channel everyone can listen
to but only builders can talk on, use this:
listen:all()
send: perm(Builders)
"""
key = "channel"
aliases = ["chan", "channels"]
locks = "cmd: not pperm(channel_banned)"
help_category = "Comms"
# these cmd: lock controls access to the channel command itself
# the admin: lock controls access to /boot/ban/unban switches
# the manage: lock controls access to /create/destroy/desc/lock/unlock switches
locks = "cmd:not pperm(channel_banned);admin:all();manage:all();changelocks:perm(Admin)"
switch_options = (
"list", "all", "history", "sub", "unsub", "mute", "unmute", "alias", "unalias",
"create", "destroy", "desc", "lock", "unlock", "boot", "ban", "unban", "who",)
@ -186,6 +209,12 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
channel_msg_nick_alias = r"{alias}\s*?|{alias}\s+?(?P<arg1>.+?)"
channel_msg_nick_replacement = "channel {channelname} = $1"
# to make it easier to override help functionality, we add the ability to
# tweak access to different sub-functionality. Note that the system will
# still check control lock etc even if you can use this functionality.
# changing these does not change access to this command itself (that's the
# locks property)
def search_channel(self, channelname, exact=False, handle_errors=True):
"""
Helper function for searching for a single channel with some error
@ -721,20 +750,26 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
"""
comtable = self.styled_table(
"|wchannel|n",
"|wmy aliases|n",
"|wdescription|n",
"channel",
"my aliases",
"locks",
"description",
align="l",
maxwidth=_DEFAULT_WIDTH
)
for chan in subscribed:
locks = "-"
if chan.access(self.caller, "control"):
locks = chan.locks
my_aliases = ", ".join(self.get_channel_aliases(chan))
comtable.add_row(
*("{}{}".format(
chan.key,
"({})".format(",".join(chan.aliases.all())) if chan.aliases.all() else ""),
my_aliases,
locks,
chan.db.desc))
return comtable
@ -744,7 +779,6 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
Args:
subscribed (list): List of subscribed channels
Returns:
EvTable: Table to display.
@ -752,33 +786,30 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
caller = self.caller
comtable = self.styled_table(
"|wsub|n",
"|wchannel|n",
"|wmy aliases|n",
"|wlocks|n",
"|wdescription|n",
"sub",
"channel",
"aliases",
"my aliases",
"description",
maxwidth=_DEFAULT_WIDTH,
)
channels = subscribed + available
for chan in channels:
my_aliases = ", ".join(self.get_channel_aliases(chan))
if chan not in subscribed:
substatus = "|rNo|n"
elif caller in chan.mutelist:
substatus = "|rMuting|n"
else:
substatus = "|gYes|n"
my_aliases = ", ".join(self.get_channel_aliases(chan))
comtable.add_row(
*(substatus,
"{}{}".format(
chan.key,
"({})".format(",".join(chan.aliases.all())) if chan.aliases.all() else ""),
chan.key,
",".join(chan.aliases.all()) if chan.aliases.all() else "",
my_aliases,
str(chan.locks),
chan.db.desc))
comtable.reformat_column(0, width=9)
comtable.reformat_column(3, width=14)
comtable.reformat_column(0, width=8)
return comtable
@ -819,6 +850,11 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
if 'create' in switches:
# create a new channel
if not self.access(caller, "manage"):
self.msg("You don't have access to use channel/create.")
return
config = self.lhs
if not config:
self.msg("To create: channel/create name[;aliases][:typeclass] [= description]")
@ -836,7 +872,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
if 'unalias' in switches:
# remove a personal alias (no channel needed)
alias = self.rhs
alias = self.args.strip()
if not alias:
self.msg("Specify the alias to remove as channel/unalias <alias>")
return
@ -976,12 +1012,17 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
if 'destroy' in switches or 'delete' in switches:
# destroy a channel we control
reason = self.rhs or None
if not self.access(caller, "manage"):
self.msg("You don't have access to use channel/destroy.")
return
if not channel.access(caller, "control"):
self.msg("You can only delete channels you control.")
return
reason = self.rhs or None
def _perform_delete(caller, *args, **kwargs):
self.destroy_channel(channel, message=reason)
self.msg(f"Channel {channel.key} was successfully deleted.")
@ -997,12 +1038,17 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
if 'desc' in switches:
# set channel description
desc = self.rhs.strip()
if not self.access(caller, "manage"):
self.msg("You don't have access to use channel/desc.")
return
if not channel.access(caller, "control"):
self.msg("You can only change description of channels you control.")
return
desc = self.rhs.strip()
if not desc:
self.msg("Usage: /desc channel = description")
return
@ -1012,12 +1058,17 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
if 'lock' in switches:
# add a lockstring to channel
lockstring = self.rhs.strip()
if not self.access(caller, "changelocks"):
self.msg("You don't have access to use channel/lock.")
return
if not channel.access(caller, "control"):
self.msg("You need 'control'-access to change locks on this channel.")
return
lockstring = self.rhs.strip()
if not lockstring:
self.msg("Usage: channel/lock channelname = lockstring")
return
@ -1031,16 +1082,21 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
if 'unlock' in switches:
# remove/update lockstring from channel
lockstring = self.rhs.strip()
if not lockstring:
self.msg("Usage: channel/unlock channelname = lockstring")
if not self.access(caller, "changelocks"):
self.msg("You don't have access to use channel/unlock.")
return
if not channel.access(caller, "control"):
self.msg("You need 'control'-access to change locks on this channel.")
return
lockstring = self.rhs.strip()
if not lockstring:
self.msg("Usage: channel/unlock channelname = lockstring")
return
success, err = self.unset_lock(channel, self.rhs)
if success:
self.msg("Removed lock from channel.")
@ -1051,6 +1107,10 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
if 'boot' in switches:
# boot a user from channel(s)
if not self.access(caller, "admin"):
self.msg("You don't have access to use channel/boot.")
return
if not self.rhs:
self.msg("Usage: channel/boot channel[,channel,...] = username [:reason]")
return
@ -1095,6 +1155,10 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
if 'ban' in switches:
# ban a user from channel(s)
if not self.access(caller, "admin"):
self.msg("You don't have access to use channel/ban.")
return
if not self.rhs:
# view bans for channels
@ -1104,7 +1168,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
bans = ["Channel bans "
"(to ban, use channel/ban channel[,channel,...] = username [:reason]"]
bans.expand(self.channel_list_bans(channel))
bans.extend(self.channel_list_bans(channel))
self.msg("\n".join(bans))
return
@ -1146,6 +1210,11 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
if 'unban' in switches:
# unban a previously banned user from channel
if not self.access(caller, "admin"):
self.msg("You don't have access to use channel/unban.")
return
target_str = self.rhs.strip()
if not target_str:
@ -1596,7 +1665,7 @@ class CmdClock(CmdChannel):
key = "clock"
aliases = ["clock"]
locks = "cmd:not pperm(channel_banned)"
locks = "cmd:not pperm(channel_banned) and perm(Admin)"
help_category = "Comms"
# this is used by the COMMAND_DEFAULT_CLASS parent
@ -1705,7 +1774,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
caller = self.caller
# get the messages we've sent (not to channels)
pages_we_sent = Msg.objects.get_messages_by_sender(caller, exclude_channel_messages=True)
pages_we_sent = Msg.objects.get_messages_by_sender(caller)
# get last messages we've got
pages_we_got = Msg.objects.get_messages_by_receiver(caller)
targets, message, number = [], None, None
@ -1745,7 +1814,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
# a single-word message
message = message[0].strip()
pages = pages_we_sent + pages_we_got
pages = list(pages_we_sent) + list(pages_we_got)
pages = sorted(pages, key=lambda page: page.date_created)
if message:

View file

@ -27,7 +27,6 @@ from evennia.utils.utils import at_search_result
from evennia.commands.cmdhandler import CMD_NOINPUT
from evennia.commands.cmdhandler import CMD_NOMATCH
from evennia.commands.cmdhandler import CMD_MULTIMATCH
from evennia.commands.cmdhandler import CMD_CHANNEL
from evennia.utils import utils
from django.conf import settings
@ -104,50 +103,3 @@ class SystemMultimatch(COMMAND_DEFAULT_CLASS):
matches = self.matches
# at_search_result will itself msg the multimatch options to the caller.
at_search_result([match[2] for match in matches], self.caller, query=matches[0][0])
# Command called when the command given at the command line
# was identified as a channel name, like there existing a
# channel named 'ooc' and the user wrote
# > ooc Hello!
class SystemSendToChannel(COMMAND_DEFAULT_CLASS):
"""
This is a special command that the cmdhandler calls
when it detects that the command given matches
an existing Channel object key (or alias).
"""
key = CMD_CHANNEL
locks = "cmd:all()"
def parse(self):
channelname, msg = self.args.split(":", 1)
self.args = channelname.strip(), msg.strip()
def func(self):
"""
Create a new message and send it to channel, using
the already formatted input.
"""
caller = self.caller
channelkey, msg = self.args
if not msg:
caller.msg("Say what?")
return
channel = ChannelDB.objects.get_channel(channelkey)
if not channel:
caller.msg("Channel '%s' not found." % channelkey)
return
if not channel.has_connection(caller):
string = "You are not connected to channel '%s'."
caller.msg(string % channelkey)
return
if not channel.access(caller, "send"):
string = "You are not permitted to send to channel '%s'."
caller.msg(string % channelkey)
return
msg = "[%s] %s: %s" % (channel.key, caller.name, msg)
msgobj = create.create_message(caller, msg, channels=[channel])
channel.msg(msgobj)

View file

@ -1861,7 +1861,7 @@ class TestCommsChannel(CommandTest):
# remove alias
self.call(
self.cmdchannel(),
"/unalias testchannel = foo",
"/unalias foo",
"Removed your channel alias 'foo'"
)
self.assertEqual(self.char1.nicks.get('foo $1', category="channel"), None)
@ -2053,12 +2053,3 @@ class TestSystemCommands(CommandTest):
multimatch.matches = matches
self.call(multimatch, "look", "")
@patch("evennia.commands.default.syscommands.ChannelDB")
def test_channelcommand(self, mock_channeldb):
channel = MagicMock()
channel.msg = MagicMock()
mock_channeldb.objects.get_channel = MagicMock(return_value=channel)
self.call(syscommands.SystemSendToChannel(), "public:Hello")
channel.msg.assert_called()

View file

@ -447,19 +447,17 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
# send to each individual subscriber
try:
message = receiver.at_pre_channel_msg(message, self, **send_kwargs)
if message in (None, False):
recv_message = receiver.at_pre_channel_msg(message, self, **send_kwargs)
if recv_message in (None, False):
return
receiver.channel_msg(message, self, **send_kwargs)
receiver.channel_msg(recv_message, self, **send_kwargs)
receiver.at_post_channel_msg(message, self, **send_kwargs)
receiver.at_post_channel_msg(recv_message, self, **send_kwargs)
except Exception:
logger.log_trace(f"Error sending channel message to {receiver}.")
# post-send hook
self.at_post_msg(message, **send_kwargs)

View file

@ -144,14 +144,6 @@ class TestCreateMessage(EvenniaTest):
self.assertEqual(msg.header, "TestHeader")
self.assertEqual(msg.senders, [self.char1])
def test_create_msg__channel(self):
chan1 = create.create_channel("DummyChannel1")
chan2 = create.create_channel("DummyChannel2")
msg = create.create_message(
self.char1, self.msgtext, channels=[chan1, chan2], header="TestHeader"
)
self.assertEqual(list(msg.channels), [chan1, chan2])
def test_create_msg__custom(self):
locks = "foo:false();bar:true()"
tags = ["tag1", "tag2", "tag3"]