Add channel sub-permission checks for admin/manage switches
This commit is contained in:
parent
d118fb17b8
commit
adf484b9df
9 changed files with 186 additions and 642 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
# Channels
|
# Channels
|
||||||
|
|
||||||
In a multiplayer game, players often need other means of in-game communication
|
In a multiplayer game, players often need other means of in-game communication
|
||||||
than moving to the same room and use `say` or `emote`.
|
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
|
`chan`). By default, this command will assume all entities dealing with
|
||||||
channels are `Accounts`.
|
channels are `Accounts`.
|
||||||
|
|
||||||
### Viewing, joining and creating channels
|
### Viewing and joining channels
|
||||||
|
|
||||||
channel - shows your subscriptions
|
channel - shows your subscriptions
|
||||||
channel/all - shows all subs available to you
|
channel/all - shows all subs available to you
|
||||||
|
|
@ -51,18 +51,9 @@ unsubscribing), you can mute it:
|
||||||
channel/mute channelname
|
channel/mute channelname
|
||||||
channel/unmute 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
|
### Chat on channels
|
||||||
|
|
||||||
To speak on a channel, do
|
To speak on a channel, do
|
||||||
|
|
||||||
channel public Hello world!
|
channel public Hello world!
|
||||||
|
|
||||||
|
|
@ -86,7 +77,7 @@ Any user can make up their own channel aliases:
|
||||||
|
|
||||||
channel/alias public = foo;bar
|
channel/alias public = foo;bar
|
||||||
|
|
||||||
You can now just do
|
You can now just do
|
||||||
|
|
||||||
foo Hello world!
|
foo Hello world!
|
||||||
bar Hello again!
|
bar Hello again!
|
||||||
|
|
@ -97,19 +88,19 @@ And even remove the default one if they don't want to use it
|
||||||
public Hello
|
public Hello
|
||||||
|
|
||||||
But you can also use your alias with the `channel` command:
|
But you can also use your alias with the `channel` command:
|
||||||
|
|
||||||
channel foo Hello world!
|
channel foo Hello world!
|
||||||
|
|
||||||
> What happens when aliasing is that a [nick](./Nicks) is created that maps your
|
> 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`,
|
> 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
|
> 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.
|
> 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
|
You can check if you missed channel conversations by viewing the channel's
|
||||||
scrollback with
|
scrollback with
|
||||||
|
|
||||||
channel/history public
|
channel/history public
|
||||||
|
|
||||||
This retrieves the last 20 lines of text (also from a time when you were
|
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:
|
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
|
### Channel administration
|
||||||
|
|
||||||
If you control the channel (because you are an admin or created it) you have the
|
To create/destroy a new channel you can do
|
||||||
ability to control who can access it by use of [locks](./Locks):
|
|
||||||
|
|
||||||
channel/lock buildchannel = listen:all();send:perm(Builders)
|
channel/create channelname;alias;alias = description
|
||||||
|
channel/destroy channelname
|
||||||
|
|
||||||
Channels use three lock-types by default:
|
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
|
||||||
- `listen` - who may listen to the channel. Users without this access will not
|
channel you created and will be controlling it. You can also use `channel/desc` to
|
||||||
even be able to join the channel and it will not appear in listings for them.
|
change the description on a channel you wnn later.
|
||||||
- `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.
|
|
||||||
|
|
||||||
If you control a channel you can also kick people off it:
|
If you control a channel you can also kick people off it:
|
||||||
|
|
||||||
channel/boot mychannel = annoyinguser123 : stop spamming!
|
channel/boot mychannel = annoyinguser123 : stop spamming!
|
||||||
|
|
||||||
The last part is an optional reason to send to the user before they are booted.
|
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
|
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
|
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
|
See the [Channel command](api:evennia.commands.default.comms.CmdChannel) api
|
||||||
docs (and in-game help) for more details.
|
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
|
## 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
|
and (more importantly) on each recipient. So you can customize things a lot by
|
||||||
just modifying hooks on your normal Object/Account typeclasses.
|
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
|
`channel.msg(message, senders=sender, bypass_mute=False, **kwargs)`, where
|
||||||
`bypass_mute=True` means the message ignores muting (good for alerts or if you
|
`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
|
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`
|
but could in principle be multiple) and `bypass_mute` are part of the `kwargs`
|
||||||
below:
|
below:
|
||||||
|
|
||||||
1. `channel.at_pre_msg(message, **kwargs)`
|
1. `channel.at_pre_msg(message, **kwargs)`
|
||||||
2. For each recipient:
|
2. For each recipient:
|
||||||
- `message = recipient.at_pre_channel_msg(message, channel, **kwargs)` -
|
- `message = recipient.at_pre_channel_msg(message, channel, **kwargs)` -
|
||||||
allows for the message to be tweaked per-receiver (for example coloring it depending
|
allows for the message to be tweaked per-receiver (for example coloring it depending
|
||||||
|
|
@ -190,7 +236,7 @@ below:
|
||||||
recipient is skipped.
|
recipient is skipped.
|
||||||
- `recipient.channel_msg(message, channel, **kwargs)` - actually sends to recipient.
|
- `recipient.channel_msg(message, channel, **kwargs)` - actually sends to recipient.
|
||||||
- `recipient.at_post_channel_msg(message, channel, **kwargs)` - any post-receive effects.
|
- `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.
|
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).
|
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
|
To change which channel typeclass Evennia uses for default commands, change
|
||||||
`settings.BASE_CHANNEL_TYPECLASS`. The base command class is
|
`settings.BASE_CHANNEL_TYPECLASS`. The base command class is
|
||||||
[`evennia.comms.comms.DefaultChannel`](api:evennia.comms.comms.DefaultChannel).
|
[`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.
|
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`:
|
`Channel.create`:
|
||||||
|
|
||||||
```python
|
```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)
|
# view subscriptions (the SubscriptionHandler handles all subs under the hood)
|
||||||
channel.subscriptions.has(me) # check we subbed
|
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.online() # get only subs currently online
|
||||||
channel.subscriptions.clear() # unsub all
|
channel.subscriptions.clear() # unsub all
|
||||||
|
|
||||||
|
|
@ -264,7 +310,7 @@ details.
|
||||||
|
|
||||||
The channel messages are not stored in the database. A channel is instead
|
The channel messages are not stored in the database. A channel is instead
|
||||||
always logged to a regular text log-file
|
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
|
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
|
thus also automatically limits the max amount of history a user can view with
|
||||||
`/history`.
|
`/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.
|
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
|
- `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.
|
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}] "`
|
the channel is prefixed. It takes the `channel_key` format key. Default is `"[{channel_key}] "`
|
||||||
and produces output like `[public] ...``.
|
and produces output like `[public] ...``.
|
||||||
- `subscriptions` - this is the [SubscriptionHandler](`api:evennia.comms.comms.SubscriptionHandler`), which
|
- `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
|
- `at_post_channel_msg(message, **kwargs)` - by default this is used to store the message
|
||||||
to the log file.
|
to the log file.
|
||||||
- `channel_prefix(message)` - this is called to allow the channel to prefix. This is called
|
- `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.
|
also just remove that call.
|
||||||
- every channel message. By default it just returns `channel_prefix_string`.
|
- every channel message. By default it just returns `channel_prefix_string`.
|
||||||
- `has_connection(subscriber)` - shortcut to check if an entity subscribes to
|
- `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.
|
- `post_join_channel(subscriber)` - unused by default.
|
||||||
- `pre_leave_channel(subscriber)` - if this returns `False`, the user is not allowed to leave.
|
- `pre_leave_channel(subscriber)` - if this returns `False`, the user is not allowed to leave.
|
||||||
- `post_leave_channel(subscriber)` - unused by default.
|
- `post_leave_channel(subscriber)` - unused by default.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -339,7 +339,6 @@ def _init():
|
||||||
CMD_NOINPUT - no input was given on command line
|
CMD_NOINPUT - no input was given on command line
|
||||||
CMD_NOMATCH - no valid command key was found
|
CMD_NOMATCH - no valid command key was found
|
||||||
CMD_MULTIMATCH - multiple command matches were 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
|
CMD_LOGINSTART - this command will be called as the very
|
||||||
first command when an account connects to
|
first command when an account connects to
|
||||||
the server.
|
the server.
|
||||||
|
|
@ -354,7 +353,6 @@ def _init():
|
||||||
CMD_NOINPUT = cmdhandler.CMD_NOINPUT
|
CMD_NOINPUT = cmdhandler.CMD_NOINPUT
|
||||||
CMD_NOMATCH = cmdhandler.CMD_NOMATCH
|
CMD_NOMATCH = cmdhandler.CMD_NOMATCH
|
||||||
CMD_MULTIMATCH = cmdhandler.CMD_MULTIMATCH
|
CMD_MULTIMATCH = cmdhandler.CMD_MULTIMATCH
|
||||||
CMD_CHANNEL = cmdhandler.CMD_CHANNEL
|
|
||||||
CMD_LOGINSTART = cmdhandler.CMD_LOGINSTART
|
CMD_LOGINSTART = cmdhandler.CMD_LOGINSTART
|
||||||
del cmdhandler
|
del cmdhandler
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
1. The calling object (caller) is analyzed based on its callertype.
|
||||||
2. Cmdsets are gathered from different sources:
|
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
|
- object cmdsets: all objects at caller's location are scanned for non-empty
|
||||||
cmdsets. This includes cmdsets on exits.
|
cmdsets. This includes cmdsets on exits.
|
||||||
- caller: the caller is searched for its own currently active cmdset.
|
- caller: the caller is searched for its own currently active cmdset.
|
||||||
|
|
@ -72,8 +68,6 @@ CMD_NOINPUT = "__noinput_command"
|
||||||
CMD_NOMATCH = "__nomatch_command"
|
CMD_NOMATCH = "__nomatch_command"
|
||||||
# command to call if multiple command matches were found
|
# command to call if multiple command matches were found
|
||||||
CMD_MULTIMATCH = "__multimatch_command"
|
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.
|
# command to call as the very first one when the user connects.
|
||||||
# (is expected to display the login screen)
|
# (is expected to display the login screen)
|
||||||
CMD_LOGINSTART = "__unloggedin_look_command"
|
CMD_LOGINSTART = "__unloggedin_look_command"
|
||||||
|
|
@ -757,18 +751,6 @@ def cmdhandler(
|
||||||
sysarg += _(' Type "help" for help.')
|
sysarg += _(' Type "help" for help.')
|
||||||
raise ExecSystemCommand(syscmd, sysarg)
|
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.
|
# A normal command.
|
||||||
ret = yield _run_command(cmd, cmdname, args, raw_cmdname, cmdset, session, account)
|
ret = yield _run_command(cmd, cmdname, args, raw_cmdname, cmdset, session, account)
|
||||||
returnValue(ret)
|
returnValue(ret)
|
||||||
|
|
|
||||||
|
|
@ -170,11 +170,34 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
Creates a new channel (or destroys one you control).
|
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"
|
key = "channel"
|
||||||
aliases = ["chan", "channels"]
|
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 = (
|
switch_options = (
|
||||||
"list", "all", "history", "sub", "unsub", "mute", "unmute", "alias", "unalias",
|
"list", "all", "history", "sub", "unsub", "mute", "unmute", "alias", "unalias",
|
||||||
"create", "destroy", "desc", "lock", "unlock", "boot", "ban", "unban", "who",)
|
"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_alias = r"{alias}\s*?|{alias}\s+?(?P<arg1>.+?)"
|
||||||
channel_msg_nick_replacement = "channel {channelname} = $1"
|
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):
|
def search_channel(self, channelname, exact=False, handle_errors=True):
|
||||||
"""
|
"""
|
||||||
Helper function for searching for a single channel with some error
|
Helper function for searching for a single channel with some error
|
||||||
|
|
@ -721,20 +750,26 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
comtable = self.styled_table(
|
comtable = self.styled_table(
|
||||||
"|wchannel|n",
|
"channel",
|
||||||
"|wmy aliases|n",
|
"my aliases",
|
||||||
"|wdescription|n",
|
"locks",
|
||||||
|
"description",
|
||||||
align="l",
|
align="l",
|
||||||
maxwidth=_DEFAULT_WIDTH
|
maxwidth=_DEFAULT_WIDTH
|
||||||
)
|
)
|
||||||
|
|
||||||
for chan in subscribed:
|
for chan in subscribed:
|
||||||
|
|
||||||
|
locks = "-"
|
||||||
|
if chan.access(self.caller, "control"):
|
||||||
|
locks = chan.locks
|
||||||
|
|
||||||
my_aliases = ", ".join(self.get_channel_aliases(chan))
|
my_aliases = ", ".join(self.get_channel_aliases(chan))
|
||||||
comtable.add_row(
|
comtable.add_row(
|
||||||
*("{}{}".format(
|
*("{}{}".format(
|
||||||
chan.key,
|
chan.key,
|
||||||
"({})".format(",".join(chan.aliases.all())) if chan.aliases.all() else ""),
|
"({})".format(",".join(chan.aliases.all())) if chan.aliases.all() else ""),
|
||||||
my_aliases,
|
my_aliases,
|
||||||
|
locks,
|
||||||
chan.db.desc))
|
chan.db.desc))
|
||||||
return comtable
|
return comtable
|
||||||
|
|
||||||
|
|
@ -744,7 +779,6 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
subscribed (list): List of subscribed channels
|
subscribed (list): List of subscribed channels
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
EvTable: Table to display.
|
EvTable: Table to display.
|
||||||
|
|
||||||
|
|
@ -752,33 +786,30 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
|
||||||
caller = self.caller
|
caller = self.caller
|
||||||
|
|
||||||
comtable = self.styled_table(
|
comtable = self.styled_table(
|
||||||
"|wsub|n",
|
"sub",
|
||||||
"|wchannel|n",
|
"channel",
|
||||||
"|wmy aliases|n",
|
"aliases",
|
||||||
"|wlocks|n",
|
"my aliases",
|
||||||
"|wdescription|n",
|
"description",
|
||||||
maxwidth=_DEFAULT_WIDTH,
|
maxwidth=_DEFAULT_WIDTH,
|
||||||
)
|
)
|
||||||
channels = subscribed + available
|
channels = subscribed + available
|
||||||
|
|
||||||
for chan in channels:
|
for chan in channels:
|
||||||
my_aliases = ", ".join(self.get_channel_aliases(chan))
|
|
||||||
if chan not in subscribed:
|
if chan not in subscribed:
|
||||||
substatus = "|rNo|n"
|
substatus = "|rNo|n"
|
||||||
elif caller in chan.mutelist:
|
elif caller in chan.mutelist:
|
||||||
substatus = "|rMuting|n"
|
substatus = "|rMuting|n"
|
||||||
else:
|
else:
|
||||||
substatus = "|gYes|n"
|
substatus = "|gYes|n"
|
||||||
|
my_aliases = ", ".join(self.get_channel_aliases(chan))
|
||||||
comtable.add_row(
|
comtable.add_row(
|
||||||
*(substatus,
|
*(substatus,
|
||||||
"{}{}".format(
|
chan.key,
|
||||||
chan.key,
|
",".join(chan.aliases.all()) if chan.aliases.all() else "",
|
||||||
"({})".format(",".join(chan.aliases.all())) if chan.aliases.all() else ""),
|
|
||||||
my_aliases,
|
my_aliases,
|
||||||
str(chan.locks),
|
|
||||||
chan.db.desc))
|
chan.db.desc))
|
||||||
comtable.reformat_column(0, width=9)
|
comtable.reformat_column(0, width=8)
|
||||||
comtable.reformat_column(3, width=14)
|
|
||||||
|
|
||||||
return comtable
|
return comtable
|
||||||
|
|
||||||
|
|
@ -819,6 +850,11 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
if 'create' in switches:
|
if 'create' in switches:
|
||||||
# create a new channel
|
# 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
|
config = self.lhs
|
||||||
if not config:
|
if not config:
|
||||||
self.msg("To create: channel/create name[;aliases][:typeclass] [= description]")
|
self.msg("To create: channel/create name[;aliases][:typeclass] [= description]")
|
||||||
|
|
@ -836,7 +872,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
if 'unalias' in switches:
|
if 'unalias' in switches:
|
||||||
# remove a personal alias (no channel needed)
|
# remove a personal alias (no channel needed)
|
||||||
alias = self.rhs
|
alias = self.args.strip()
|
||||||
if not alias:
|
if not alias:
|
||||||
self.msg("Specify the alias to remove as channel/unalias <alias>")
|
self.msg("Specify the alias to remove as channel/unalias <alias>")
|
||||||
return
|
return
|
||||||
|
|
@ -976,12 +1012,17 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
if 'destroy' in switches or 'delete' in switches:
|
if 'destroy' in switches or 'delete' in switches:
|
||||||
# destroy a channel we control
|
# 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"):
|
if not channel.access(caller, "control"):
|
||||||
self.msg("You can only delete channels you control.")
|
self.msg("You can only delete channels you control.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
reason = self.rhs or None
|
||||||
|
|
||||||
def _perform_delete(caller, *args, **kwargs):
|
def _perform_delete(caller, *args, **kwargs):
|
||||||
self.destroy_channel(channel, message=reason)
|
self.destroy_channel(channel, message=reason)
|
||||||
self.msg(f"Channel {channel.key} was successfully deleted.")
|
self.msg(f"Channel {channel.key} was successfully deleted.")
|
||||||
|
|
@ -997,12 +1038,17 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
if 'desc' in switches:
|
if 'desc' in switches:
|
||||||
# set channel description
|
# 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"):
|
if not channel.access(caller, "control"):
|
||||||
self.msg("You can only change description of channels you control.")
|
self.msg("You can only change description of channels you control.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
desc = self.rhs.strip()
|
||||||
|
|
||||||
if not desc:
|
if not desc:
|
||||||
self.msg("Usage: /desc channel = description")
|
self.msg("Usage: /desc channel = description")
|
||||||
return
|
return
|
||||||
|
|
@ -1012,12 +1058,17 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
if 'lock' in switches:
|
if 'lock' in switches:
|
||||||
# add a lockstring to channel
|
# 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"):
|
if not channel.access(caller, "control"):
|
||||||
self.msg("You need 'control'-access to change locks on this channel.")
|
self.msg("You need 'control'-access to change locks on this channel.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
lockstring = self.rhs.strip()
|
||||||
|
|
||||||
if not lockstring:
|
if not lockstring:
|
||||||
self.msg("Usage: channel/lock channelname = lockstring")
|
self.msg("Usage: channel/lock channelname = lockstring")
|
||||||
return
|
return
|
||||||
|
|
@ -1031,16 +1082,21 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
if 'unlock' in switches:
|
if 'unlock' in switches:
|
||||||
# remove/update lockstring from channel
|
# remove/update lockstring from channel
|
||||||
lockstring = self.rhs.strip()
|
|
||||||
|
|
||||||
if not lockstring:
|
if not self.access(caller, "changelocks"):
|
||||||
self.msg("Usage: channel/unlock channelname = lockstring")
|
self.msg("You don't have access to use channel/unlock.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not channel.access(caller, "control"):
|
if not channel.access(caller, "control"):
|
||||||
self.msg("You need 'control'-access to change locks on this channel.")
|
self.msg("You need 'control'-access to change locks on this channel.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
lockstring = self.rhs.strip()
|
||||||
|
|
||||||
|
if not lockstring:
|
||||||
|
self.msg("Usage: channel/unlock channelname = lockstring")
|
||||||
|
return
|
||||||
|
|
||||||
success, err = self.unset_lock(channel, self.rhs)
|
success, err = self.unset_lock(channel, self.rhs)
|
||||||
if success:
|
if success:
|
||||||
self.msg("Removed lock from channel.")
|
self.msg("Removed lock from channel.")
|
||||||
|
|
@ -1051,6 +1107,10 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
|
||||||
if 'boot' in switches:
|
if 'boot' in switches:
|
||||||
# boot a user from channel(s)
|
# 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:
|
if not self.rhs:
|
||||||
self.msg("Usage: channel/boot channel[,channel,...] = username [:reason]")
|
self.msg("Usage: channel/boot channel[,channel,...] = username [:reason]")
|
||||||
return
|
return
|
||||||
|
|
@ -1095,6 +1155,10 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
|
||||||
if 'ban' in switches:
|
if 'ban' in switches:
|
||||||
# ban a user from channel(s)
|
# 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:
|
if not self.rhs:
|
||||||
# view bans for channels
|
# view bans for channels
|
||||||
|
|
||||||
|
|
@ -1104,7 +1168,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
bans = ["Channel bans "
|
bans = ["Channel bans "
|
||||||
"(to ban, use channel/ban channel[,channel,...] = username [:reason]"]
|
"(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))
|
self.msg("\n".join(bans))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -1146,6 +1210,11 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
if 'unban' in switches:
|
if 'unban' in switches:
|
||||||
# unban a previously banned user from channel
|
# 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()
|
target_str = self.rhs.strip()
|
||||||
|
|
||||||
if not target_str:
|
if not target_str:
|
||||||
|
|
@ -1596,7 +1665,7 @@ class CmdClock(CmdChannel):
|
||||||
|
|
||||||
key = "clock"
|
key = "clock"
|
||||||
aliases = ["clock"]
|
aliases = ["clock"]
|
||||||
locks = "cmd:not pperm(channel_banned)"
|
locks = "cmd:not pperm(channel_banned) and perm(Admin)"
|
||||||
help_category = "Comms"
|
help_category = "Comms"
|
||||||
|
|
||||||
# this is used by the COMMAND_DEFAULT_CLASS parent
|
# this is used by the COMMAND_DEFAULT_CLASS parent
|
||||||
|
|
@ -1705,7 +1774,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
|
||||||
caller = self.caller
|
caller = self.caller
|
||||||
|
|
||||||
# get the messages we've sent (not to channels)
|
# 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
|
# get last messages we've got
|
||||||
pages_we_got = Msg.objects.get_messages_by_receiver(caller)
|
pages_we_got = Msg.objects.get_messages_by_receiver(caller)
|
||||||
targets, message, number = [], None, None
|
targets, message, number = [], None, None
|
||||||
|
|
@ -1745,7 +1814,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
|
||||||
# a single-word message
|
# a single-word message
|
||||||
message = message[0].strip()
|
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)
|
pages = sorted(pages, key=lambda page: page.date_created)
|
||||||
|
|
||||||
if message:
|
if message:
|
||||||
|
|
|
||||||
|
|
@ -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_NOINPUT
|
||||||
from evennia.commands.cmdhandler import CMD_NOMATCH
|
from evennia.commands.cmdhandler import CMD_NOMATCH
|
||||||
from evennia.commands.cmdhandler import CMD_MULTIMATCH
|
from evennia.commands.cmdhandler import CMD_MULTIMATCH
|
||||||
from evennia.commands.cmdhandler import CMD_CHANNEL
|
|
||||||
from evennia.utils import utils
|
from evennia.utils import utils
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
@ -104,50 +103,3 @@ class SystemMultimatch(COMMAND_DEFAULT_CLASS):
|
||||||
matches = self.matches
|
matches = self.matches
|
||||||
# at_search_result will itself msg the multimatch options to the caller.
|
# 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])
|
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)
|
|
||||||
|
|
|
||||||
|
|
@ -1861,7 +1861,7 @@ class TestCommsChannel(CommandTest):
|
||||||
# remove alias
|
# remove alias
|
||||||
self.call(
|
self.call(
|
||||||
self.cmdchannel(),
|
self.cmdchannel(),
|
||||||
"/unalias testchannel = foo",
|
"/unalias foo",
|
||||||
"Removed your channel alias 'foo'"
|
"Removed your channel alias 'foo'"
|
||||||
)
|
)
|
||||||
self.assertEqual(self.char1.nicks.get('foo $1', category="channel"), None)
|
self.assertEqual(self.char1.nicks.get('foo $1', category="channel"), None)
|
||||||
|
|
@ -2053,12 +2053,3 @@ class TestSystemCommands(CommandTest):
|
||||||
multimatch.matches = matches
|
multimatch.matches = matches
|
||||||
|
|
||||||
self.call(multimatch, "look", "")
|
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()
|
|
||||||
|
|
|
||||||
|
|
@ -447,19 +447,17 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
|
||||||
# send to each individual subscriber
|
# send to each individual subscriber
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message = receiver.at_pre_channel_msg(message, self, **send_kwargs)
|
recv_message = receiver.at_pre_channel_msg(message, self, **send_kwargs)
|
||||||
if message in (None, False):
|
if recv_message in (None, False):
|
||||||
return
|
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:
|
except Exception:
|
||||||
logger.log_trace(f"Error sending channel message to {receiver}.")
|
logger.log_trace(f"Error sending channel message to {receiver}.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# post-send hook
|
# post-send hook
|
||||||
self.at_post_msg(message, **send_kwargs)
|
self.at_post_msg(message, **send_kwargs)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -144,14 +144,6 @@ class TestCreateMessage(EvenniaTest):
|
||||||
self.assertEqual(msg.header, "TestHeader")
|
self.assertEqual(msg.header, "TestHeader")
|
||||||
self.assertEqual(msg.senders, [self.char1])
|
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):
|
def test_create_msg__custom(self):
|
||||||
locks = "foo:false();bar:true()"
|
locks = "foo:false();bar:true()"
|
||||||
tags = ["tag1", "tag2", "tag3"]
|
tags = ["tag1", "tag2", "tag3"]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue