diff --git a/docs/source/Components/Channels.md b/docs/source/Components/Channels.md index 6447b80993..29e050812e 100644 --- a/docs/source/Components/Channels.md +++ b/docs/source/Components/Channels.md @@ -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_.log`. This is where `channels/history channelname` +`mygame/server/logs/channel_.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. - + diff --git a/docs/source/Howto/Customize-channels.md b/docs/source/Howto/Customize-channels.md deleted file mode 100644 index 313df706a5..0000000000 --- a/docs/source/Howto/Customize-channels.md +++ /dev/null @@ -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} - {lower_channelkey}/history [start] - {lower_channelkey}/me - {lower_channelkey}/who - - Switch: - history: View 20 previous messages, either from the end or - from 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) \ No newline at end of file diff --git a/evennia/__init__.py b/evennia/__init__.py index 3b637ff80d..1753c09d90 100644 --- a/evennia/__init__.py +++ b/evennia/__init__.py @@ -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 diff --git a/evennia/commands/cmdhandler.py b/evennia/commands/cmdhandler.py index 3f8fb85e51..2ed8ac4d4a 100644 --- a/evennia/commands/cmdhandler.py +++ b/evennia/commands/cmdhandler.py @@ -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) diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index 1ae88649a7..3b6d2a8aa6 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -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.+?)" 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 ") 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: diff --git a/evennia/commands/default/syscommands.py b/evennia/commands/default/syscommands.py index b6291d94d0..a2964f7ff6 100644 --- a/evennia/commands/default/syscommands.py +++ b/evennia/commands/default/syscommands.py @@ -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) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 4cfaaf4336..268b87b046 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -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() diff --git a/evennia/comms/comms.py b/evennia/comms/comms.py index 33e9365043..6acd8dff93 100644 --- a/evennia/comms/comms.py +++ b/evennia/comms/comms.py @@ -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) diff --git a/evennia/utils/tests/test_create_functions.py b/evennia/utils/tests/test_create_functions.py index 4daea8f570..1c6cf622af 100644 --- a/evennia/utils/tests/test_create_functions.py +++ b/evennia/utils/tests/test_create_functions.py @@ -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"]