Move alias/connect setup to channel class rather than cmd

This commit is contained in:
Griatch 2021-05-14 20:28:36 +02:00
parent 8e19017dc3
commit 51bef9bf97
8 changed files with 224 additions and 84 deletions

View file

@ -315,6 +315,12 @@ 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`.
The log file name is set on the channel class as the `log_file` property. This
is a string that takes the formatting token `{channelname}` to be replaced with
the (lower-case) name of the channel. By default the log is written to in the
channel's `at_post_channel_msg` method.
### Properties on Channels ### Properties on Channels
Channels have all the standard properties of a Typeclassed entity (`key`, Channels have all the standard properties of a Typeclassed entity (`key`,
@ -323,16 +329,24 @@ see the [Channel api docs](api:evennia.comms.comms.DefaultChannel) for details.
- `send_to_online_only` - this class boolean defaults to `True` and is a - `send_to_online_only` - this class boolean defaults to `True` and is a
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_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_{channelname}.log"`. The log file will appear in `settings.LOG_DIR` (usually
`mygame/server/logs/`). 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 `channelname` format key. Default is `"[{channelname}] "`
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
has methods `has`, `add`, `remove`, `all`, `clear` and also `online` (to get has methods `has`, `add`, `remove`, `all`, `clear` and also `online` (to get
only actually online channel-members). only actually online channel-members).
- `wholist`, `mutelist`, `banlist` are properties that return a list of subscribers, - `wholist`, `mutelist`, `banlist` are properties that return a list of subscribers,
as well as who are currently muted or banned. as well as who are currently muted or banned.
- `channel_msg_nick_pattern` - this is a regex pattern for performing the in-place nick
replacement (detect that `channelalias <msg` means that you want to send a message to a channel).
This pattern accepts an `{alias}` formatting marker. Don't mess with this unless you really
want to change how channels work.
- `channel_msg_nick_replacement` - this is a string on the [nick replacement
- form](Nicks). It accepts the `{channelname}` formatting tag. This is strongly tied to the
`channel` command and is by default `channel {channelname} = $1`.
Notable `Channel` hooks: Notable `Channel` hooks:
@ -347,12 +361,19 @@ Notable `Channel` hooks:
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
this channel this channel.
- `mute/unmute(subscriber)` - this mutes the channel for this user. - `mute/unmute(subscriber)` - this mutes the channel for this user.
- `ban/unban(subscriber)` - adds/remove user from banlist. - `ban/unban(subscriber)` - adds/remove user from banlist.
- `connect/disconnect(subscriber)` - adds/removes a subscriber. - `connect/disconnect(subscriber)` - adds/removes a subscriber.
- `add_user_channel_alias(user, alias, **kwargs)` - sets up a user-nick for this channel. This is
what maps e.g. `alias <msg>` to `channel channelname = <msg>`.
- `remove_user_channel_alias(user, alias, **kwargs)` - remove an alias. Note that this is
a class-method that will happily remove found channel-aliases from the user linked to _any_
channel, not only from the channel the method is called on.
- `pre_join_channel(subscriber)` - if this returns `False`, connection will be refused. - `pre_join_channel(subscriber)` - if this returns `False`, connection will be refused.
- `post_join_channel(subscriber)` - unused by default. - `post_join_channel(subscriber)` - by default this sets up a users's channel-nicks/aliases.
- `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)` - this will clean up any channel aliases/nicks of the user.
- `delete` the standard typeclass-delete mechanism will also automatically un-subscribe all
subscribers (and thus wipe all their aliases).

View file

@ -12,6 +12,7 @@ from evennia.comms.models import Msg
from evennia.accounts.models import AccountDB from evennia.accounts.models import AccountDB
from evennia.accounts import bots from evennia.accounts import bots
from evennia.locks.lockhandler import LockException from evennia.locks.lockhandler import LockException
from evennia.comms.comms import DefaultChannel
from evennia.utils import create, logger, utils from evennia.utils import create, logger, utils
from evennia.utils.logger import tail_log_file from evennia.utils.logger import tail_log_file
from evennia.utils.utils import class_from_module from evennia.utils.utils import class_from_module
@ -229,17 +230,6 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
# disable this in child command classes if wanting on-character channels # disable this in child command classes if wanting on-character channels
account_caller = True account_caller = True
# note - changing this will invalidate existing aliases in db
# channel_msg_nick_alias = r"{alias}\s*?(?P<arg1>.+?){{0,1}}"
channel_msg_nick_alias = r"{alias}\s*?|{alias}\s+?(?P<arg1>.+?)"
channel_msg_nick_replacement = "channel {channelname} = $1"
# to make it easier to override help functionality, we add the ability to
# tweak access to different sub-functionality. Note that the system will
# still check control lock etc even if you can use this functionality.
# changing these does not change access to this command itself (that's the
# locks property)
def search_channel(self, channelname, exact=False, handle_errors=True): 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
@ -323,9 +313,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
""" """
caller = self.caller caller = self.caller
log_file = channel.get_log_filename()
log_file = channel.attributes.get(
"log_file", default=channel.log_to_file.format(channel_key=channel.key))
def send_msg(lines): def send_msg(lines):
return self.msg( return self.msg(
@ -351,11 +339,9 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
if channel.has_connection(caller): if channel.has_connection(caller):
return False, f"Already listening to channel {channel.key}." return False, f"Already listening to channel {channel.key}."
result = channel.connect(caller)
key_and_aliases = [channel.key.lower()] + [alias.lower() for alias in channel.aliases.all()] # this sets up aliases in post_join_channel by default
for key_or_alias in key_and_aliases: result = channel.connect(caller)
self.add_alias(channel, key_or_alias)
return result, "" if result else f"Were not allowed to subscribe to channel {channel.key}" return result, "" if result else f"Were not allowed to subscribe to channel {channel.key}"
@ -377,14 +363,10 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
if not channel.has_connection(caller): if not channel.has_connection(caller):
return False, f"Not listening to channel {channel.key}." return False, f"Not listening to channel {channel.key}."
# clear aliases
for key_or_alias in self.get_channel_aliases(channel):
self.remove_alias(key_or_alias, **kwargs)
# remove the channel-name alias too
msg_alias = self.channel_msg_nick_alias.format(alias=channel.key.lower())
caller.nicks.remove(msg_alias, category="inputline", **kwargs)
# this will also clean aliases
result = channel.disconnect(caller) result = channel.disconnect(caller)
return result, "" if result else f"Could not unsubscribe from channel {channel.key}" return result, "" if result else f"Could not unsubscribe from channel {channel.key}"
def add_alias(self, channel, alias, **kwargs): def add_alias(self, channel, alias, **kwargs):
@ -401,7 +383,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
we need to be able to reference this channel easily. The other we need to be able to reference this channel easily. The other
is a templated nick to easily be able to send messages to the is a templated nick to easily be able to send messages to the
channel without needing to give the full `channel` command. The channel without needing to give the full `channel` command. The
structure of this nick is given by `self.channel_msg_nick_alias` structure of this nick is given by `self.channel_msg_pattern`
and `self.channel_msg_nick_replacement`. By default it maps and `self.channel_msg_nick_replacement`. By default it maps
`alias <msg> -> channel <channelname> = <msg>`, so that you can `alias <msg> -> channel <channelname> = <msg>`, so that you can
for example just write `pub Hello` to send a message. for example just write `pub Hello` to send a message.
@ -410,16 +392,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
for sending to channel using the main channel command. for sending to channel using the main channel command.
""" """
chan_key = channel.key.lower() channel.add_user_channel_alias(self.caller, alias, **kwargs)
# the message-pattern allows us to type the channel on its own without
# needing to use the `channel` command explicitly.
msg_pattern = self.channel_msg_nick_alias.format(alias=alias)
msg_replacement = self.channel_msg_nick_replacement.format(channelname=chan_key)
if chan_key != alias:
self.caller.nicks.add(alias, chan_key, category="channel", **kwargs)
self.caller.nicks.add(msg_pattern, msg_replacement, category="inputline",
pattern_is_regex=True, **kwargs)
def remove_alias(self, alias, **kwargs): def remove_alias(self, alias, **kwargs):
""" """
@ -440,13 +413,9 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
nick used for easily sending messages to the channel. nick used for easily sending messages to the channel.
""" """
caller = self.caller if self.caller.nicks.has(alias, category="channel", **kwargs):
if caller.nicks.get(alias, category="channel", **kwargs): DefaultChannel.remove_user_channel_alias(self.caller, alias)
caller.nicks.remove(alias, category="chan nel", **kwargs)
msg_alias = self.channel_msg_nick_alias.format(alias=alias)
caller.nicks.remove(msg_alias, category="inputline", **kwargs)
return True, "" return True, ""
return False, "No such alias was defined." return False, "No such alias was defined."
def get_channel_aliases(self, channel): def get_channel_aliases(self, channel):
@ -462,7 +431,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
""" """
chan_key = channel.key.lower() chan_key = channel.key.lower()
nicktuples = self.caller.nicks.get(category="channel", return_tuple=True) nicktuples = self.caller.nicks.get(category="channel", return_tuple=True, return_list=True)
if nicktuples: if nicktuples:
return [tup[2] for tup in nicktuples if tup[3].lower() == chan_key] return [tup[2] for tup in nicktuples if tup[3].lower() == chan_key]
return [] return []
@ -915,6 +884,8 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
# first 'channel name' is in fact 'channelname text' # first 'channel name' is in fact 'channelname text'
no_rhs_channel_name = self.args.split(" ", 1)[0] no_rhs_channel_name = self.args.split(" ", 1)[0]
possible_lhs_message = self.args[len(no_rhs_channel_name):] possible_lhs_message = self.args[len(no_rhs_channel_name):]
if possible_lhs_message.strip() == '=':
possible_lhs_message = ""
channel_names.append(no_rhs_channel_name) channel_names.append(no_rhs_channel_name)
@ -952,13 +923,10 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
subscribed, available = self.list_channels() subscribed, available = self.list_channels()
if channel in subscribed: if channel in subscribed:
table = self.display_subbed_channels([channel]) table = self.display_subbed_channels([channel])
inputname = self.raw_cmdname header = f"Channel |w{channel.key}|n"
if inputname.lower() != channel.key.lower(): self.msg(f"{header}\n(use |w{channel.key} <msg>|n (or a channel-alias) "
header = f"Channel |w{inputname}|n (alias for {channel.key} channel)" f"to chat and the 'channel' command "
else: f"to customize)\n{table}")
header = f"Channel |w{channel.key}|n"
self.msg(f"{header}\n(use |w{inputname} <msg>|n to chat and "
f"the 'channel' command to customize)\n{table}")
elif channel in available: elif channel in available:
table = self.display_all_channels([], [channel]) table = self.display_all_channels([], [channel])
self.msg( self.msg(
@ -1055,7 +1023,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
ask_yes_no( ask_yes_no(
caller, caller,
prompt=f"Are you sure you want to delete channel '{channel.key}' " prompt=f"Are you sure you want to delete channel '{channel.key}' "
"(make sure name is correct!)? This will disconnect and " "(make sure name is correct!)?\nThis will disconnect and "
"remove all users' aliases. {options}?", "remove all users' aliases. {options}?",
yes_action=_perform_delete, yes_action=_perform_delete,
no_action="Aborted.", no_action="Aborted.",

View file

@ -21,12 +21,12 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
Class-level variables: Class-level variables:
- `send_to_online_only` (bool, default True) - if set, will only try to - `send_to_online_only` (bool, default True) - if set, will only try to
send to subscribers that are actually active. This is a useful optimization. send to subscribers that are actually active. This is a useful optimization.
- `log_to_file` (str, default `"channel_{channel_key}.log"`). This is the - `log_file` (str, default `"channel_{channelname}.log"`). This is the
log file to which the channel history will be saved. The `{channel_key}` tag log file to which the channel history will be saved. The `{channelname}` tag
will be replaced by the key of the Channel. If an Attribute 'log_file' will be replaced by the key of the Channel. If an Attribute 'log_file'
is set, this will be used instead. If this is None and no Attribute is found, is set, this will be used instead. If this is None and no Attribute is found,
no history will be saved. no history will be saved.
- `channel_prefix_string` (str, default `"[{channel_key} ]"`) - this is used - `channel_prefix_string` (str, default `"[{channelname} ]"`) - this is used
as a simple template to get the channel prefix with `.channel_prefix()`. as a simple template to get the channel prefix with `.channel_prefix()`.
""" """
@ -40,10 +40,15 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
send_to_online_only = True send_to_online_only = True
# store log in log file. `channel_key tag will be replace with key of channel. # store log in log file. `channel_key tag will be replace with key of channel.
# Will use log_file Attribute first, if given # Will use log_file Attribute first, if given
log_to_file = "channel_{channel_key}.log" log_file = "channel_{channelname}.log"
# which prefix to use when showing were a message is coming from. Set to # which prefix to use when showing were a message is coming from. Set to
# None to disable and set this later. # None to disable and set this later.
channel_prefix_string = "[{channel_key}] " channel_prefix_string = "[{channelname}] "
# default nick-alias replacements (default using the 'channel' command)
channel_msg_nick_pattern = r"{alias}\s*?|{alias}\s+?(?P<arg1>.+?)"
channel_msg_nick_replacement = "channel {channelname} = $1"
def at_first_save(self): def at_first_save(self):
""" """
@ -54,7 +59,6 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
""" """
self.basetype_setup() self.basetype_setup()
self.at_channel_creation() self.at_channel_creation()
self.attributes.add("log_file", "channel_%s.log" % self.key)
if hasattr(self, "_createdict"): if hasattr(self, "_createdict"):
# this is only set if the channel was created # this is only set if the channel was created
# with the utils.create.create_channel function. # with the utils.create.create_channel function.
@ -78,6 +82,10 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
def basetype_setup(self): def basetype_setup(self):
self.locks.add("send:all();listen:all();control:perm(Admin)") self.locks.add("send:all();listen:all();control:perm(Admin)")
# make sure we don't have access to a same-named old channel's history.
log_file = self.get_log_filename()
logger.rotate_log_file(log_file, num_lines_to_append=0)
def at_channel_creation(self): def at_channel_creation(self):
""" """
Called once, when the channel is first created. Called once, when the channel is first created.
@ -87,6 +95,33 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
# helper methods, for easy overloading # helper methods, for easy overloading
_log_file = None
def get_log_filename(self):
"""
File name to use for channel log.
Returns:
str: The filename to use (this is always assumed to be inside
settings.LOG_DIR)
"""
if not self._log_file:
self._log_file = self.attributes.get(
"log_file", self.log_file.format(channelname=self.key.lower()))
return self._log_file
def set_log_filename(self, filename):
"""
Set a custom log filename.
Args:
filename (str): The filename to set. This is a path starting from
inside the settings.LOG_DIR location.
"""
self.attributes.add("log_file", filename)
def has_connection(self, subscriber): def has_connection(self, subscriber):
""" """
Checks so this account is actually listening Checks so this account is actually listening
@ -368,6 +403,8 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
""" """
self.attributes.clear() self.attributes.clear()
self.aliases.clear() self.aliases.clear()
for subscriber in self.subscriptions.all():
self.disconnect(subscriber)
super().delete() super().delete()
def channel_prefix(self): def channel_prefix(self):
@ -378,7 +415,73 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
str: The channel prefix. str: The channel prefix.
""" """
return self.channel_prefix_string.format(channel_key=self.key) return self.channel_prefix_string.format(channelname=self.key)
def add_user_channel_alias(self, user, alias, **kwargs):
"""
Add a personal user-alias for this channel to a given subscriber.
Args:
user (Object or Account): The one to alias this channel.
alias (str): The desired alias.
Note:
This is tightly coupled to the default `channel` command. If you
change that, you need to change this as well.
We add two nicks - one is a plain `alias -> channel.key` that
users need to be able to reference this channel easily. The other
is a templated nick to easily be able to send messages to the
channel without needing to give the full `channel` command. The
structure of this nick is given by `self.channel_msg_nick_pattern`
and `self.channel_msg_nick_replacement`. By default it maps
`alias <msg> -> channel <channelname> = <msg>`, so that you can
for example just write `pub Hello` to send a message.
The alias created is `alias $1 -> channel channel = $1`, to allow
for sending to channel using the main channel command.
"""
chan_key = self.key.lower()
# the message-pattern allows us to type the channel on its own without
# needing to use the `channel` command explicitly.
msg_nick_pattern = self.channel_msg_nick_pattern.format(alias=alias)
msg_nick_replacement = self.channel_msg_nick_replacement.format(channelname=chan_key)
user.nicks.add(msg_nick_pattern, msg_nick_replacement, category="inputline",
pattern_is_regex=True, **kwargs)
if chan_key != alias:
# this allows for using the alias for general channel lookups
user.nicks.add(alias, chan_key, category="channel", **kwargs)
@classmethod
def remove_user_channel_alias(cls, user, alias, **kwargs):
"""
Remove a personal channel alias from a user.
Args:
user (Object or Account): The user to remove an alias from.
alias (str): The alias to remove.
**kwargs: Unused by default. Can be used to pass extra variables
into a custom implementation.
Notes:
The channel-alias actually consists of two aliases - one
channel-based one for searching channels with the alias and one
inputline one for doing the 'channelalias msg' - call.
This is a classmethod because it doesn't actually operate on the
channel instance.
It sits on the channel because the nick structure for this is
pretty complex and needs to be located in a central place (rather
on, say, the channel command).
"""
user.nicks.remove(alias, category="channel", **kwargs)
msg_nick_pattern = cls.channel_msg_nick_pattern.format(alias=alias)
user.nicks.remove(msg_nick_pattern, category="inputline", **kwargs)
def at_pre_msg(self, message, **kwargs): def at_pre_msg(self, message, **kwargs):
""" """
@ -472,9 +575,7 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
""" """
# save channel history to log file # save channel history to log file
default_log_file = (self.log_to_file.format(channel_key=self.key) log_file = self.get_log_filename()
if self.log_to_file else None)
log_file = self.attributes.get("log_file", default=default_log_file)
if log_file: if log_file:
senders = ",".join(sender.key for sender in kwargs.get("senders", [])) senders = ",".join(sender.key for sender in kwargs.get("senders", []))
senders = f"{senders}: " if senders else "" senders = f"{senders}: " if senders else ""
@ -506,8 +607,13 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
**kwargs (dict): Arbitrary, optional arguments for users **kwargs (dict): Arbitrary, optional arguments for users
overriding the call (unused by default). overriding the call (unused by default).
Notes:
By default this adds the needed channel nicks to the joiner.
""" """
pass key_and_aliases = [self.key.lower()] + [alias.lower() for alias in self.aliases.all()]
for key_or_alias in key_and_aliases:
self.add_user_channel_alias(joiner, key_or_alias, **kwargs)
def pre_leave_channel(self, leaver, **kwargs): def pre_leave_channel(self, leaver, **kwargs):
""" """
@ -535,7 +641,12 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
overriding the call (unused by default). overriding the call (unused by default).
""" """
pass chan_key = self.key.lower()
key_or_aliases = [self.key.lower()] + [alias.lower() for alias in self.aliases.all()]
nicktuples = leaver.nicks.get(category="channel", return_tuple=True, return_list=True)
key_or_aliases += [tup[2] for tup in nicktuples if tup[3].lower() == chan_key]
for key_or_alias in key_or_aliases:
self.remove_user_channel_alias(leaver, key_or_alias, **kwargs)
def at_init(self): def at_init(self):
""" """

View file

@ -186,7 +186,7 @@ class CraftingRecipeBase:
are optional but will be passed into all of the following hooks. are optional but will be passed into all of the following hooks.
2. `.pre_craft(**kwargs)` - this normally validates inputs and stores them in 2. `.pre_craft(**kwargs)` - this normally validates inputs and stores them in
`.validated_inputs.`. Raises `CraftingValidationError` otherwise. `.validated_inputs.`. Raises `CraftingValidationError` otherwise.
4. `.craft(**kwargs)` - should return the crafted item(s) or the empty list. Any 4. `.do_craft(**kwargs)` - should return the crafted item(s) or the empty list. Any
crafting errors should be immediately reported to user. crafting errors should be immediately reported to user.
5. `.post_craft(crafted_result, **kwargs)`- always called, even if `pre_craft` 5. `.post_craft(crafted_result, **kwargs)`- always called, even if `pre_craft`
raised a `CraftingError` or `CraftingValidationError`. raised a `CraftingError` or `CraftingValidationError`.
@ -252,7 +252,7 @@ class CraftingRecipeBase:
else: else:
raise CraftingValidationError raise CraftingValidationError
def craft(self, **kwargs): def do_craft(self, **kwargs):
""" """
Hook to override. Hook to override.
@ -277,7 +277,7 @@ class CraftingRecipeBase:
method is to delete the inputs. method is to delete the inputs.
Args: Args:
crafting_result (any): The outcome of crafting, as returned by `craft()`. crafting_result (any): The outcome of crafting, as returned by `do_craft`.
**kwargs: Any extra flags passed at initialization. **kwargs: Any extra flags passed at initialization.
Returns: Returns:
@ -324,7 +324,7 @@ class CraftingRecipeBase:
if raise_exception: if raise_exception:
raise raise
else: else:
craft_result = self.craft(**craft_kwargs) craft_result = self.do_craft(**craft_kwargs)
finally: finally:
craft_result = self.post_craft(craft_result, **craft_kwargs) craft_result = self.post_craft(craft_result, **craft_kwargs)
except (CraftingError, CraftingValidationError): except (CraftingError, CraftingValidationError):
@ -455,7 +455,7 @@ class CraftingRecipe(CraftingRecipeBase):
3. `.pre_craft(**kwargs)` should handle validation of inputs. Results should 3. `.pre_craft(**kwargs)` should handle validation of inputs. Results should
be stored in `validated_consumables/tools` respectively. Raises `CraftingValidationError` be stored in `validated_consumables/tools` respectively. Raises `CraftingValidationError`
otherwise. otherwise.
4. `.craft(**kwargs)` will not be called if validation failed. Should return 4. `.do_craft(**kwargs)` will not be called if validation failed. Should return
a list of the things crafted. a list of the things crafted.
5. `.post_craft(crafting_result, **kwargs)` is always called, also if validation 5. `.post_craft(crafting_result, **kwargs)` is always called, also if validation
failed (`crafting_result` will then be falsy). It does any cleanup. By default failed (`crafting_result` will then be falsy). It does any cleanup. By default
@ -819,7 +819,7 @@ class CraftingRecipe(CraftingRecipeBase):
self.validated_tools = tools self.validated_tools = tools
self.validated_consumables = consumables self.validated_consumables = consumables
def craft(self, **kwargs): def do_craft(self, **kwargs):
""" """
Hook to override. This will not be called if validation in `pre_craft` Hook to override. This will not be called if validation in `pre_craft`
fails. fails.
@ -847,7 +847,7 @@ class CraftingRecipe(CraftingRecipeBase):
this method is to delete the inputs. this method is to delete the inputs.
Args: Args:
craft_result (list): The crafted result, provided by `self.craft()`. craft_result (list): The crafted result, provided by `self.do_craft`.
**kwargs (any): Passed from `self.craft`. **kwargs (any): Passed from `self.craft`.
Returns: Returns:

View file

@ -99,7 +99,7 @@ class _SwordSmithingBaseRecipe(CraftingRecipe):
"You work and work but you are not happy with the result. You need to start over." "You work and work but you are not happy with the result. You need to start over."
) )
def do_craft(self, **kwargs): def craft(self, **kwargs):
""" """
Making a sword blade takes skill. Here we emulate this by introducing a Making a sword blade takes skill. Here we emulate this by introducing a
random chance of failure (in a real game this could be a skill check random chance of failure (in a real game this could be a skill check
@ -126,7 +126,7 @@ class _SwordSmithingBaseRecipe(CraftingRecipe):
if random.random() < 0.8: if random.random() < 0.8:
# 80% chance of success. This will spawn the sword and show # 80% chance of success. This will spawn the sword and show
# success-message. # success-message.
return super().do_craft(**kwargs) return super().craft(**kwargs)
else: else:
# fail and show failed message # fail and show failed message
return None return None

View file

@ -91,7 +91,7 @@ class TestCraftingRecipeBase(TestCase):
"""Test craft hook, the main access method.""" """Test craft hook, the main access method."""
expected_result = _TestMaterial("test_result") expected_result = _TestMaterial("test_result")
self.recipe.craft = mock.MagicMock(return_value=expected_result) self.recipe.do_craft = mock.MagicMock(return_value=expected_result)
self.assertTrue(self.recipe.allow_craft) self.assertTrue(self.recipe.allow_craft)
@ -99,7 +99,7 @@ class TestCraftingRecipeBase(TestCase):
# check result # check result
self.assertEqual(result, expected_result) self.assertEqual(result, expected_result)
self.recipe.craft.assert_called_with(kw1=1, kw2=2) self.recipe.do_craft.assert_called_with(kw1=1, kw2=2)
# since allow_reuse is False, this usage should now be turned off # since allow_reuse is False, this usage should now be turned off
self.assertFalse(self.recipe.allow_craft) self.assertFalse(self.recipe.allow_craft)
@ -110,7 +110,7 @@ class TestCraftingRecipeBase(TestCase):
def test_craft_hook__fail(self): def test_craft_hook__fail(self):
"""Test failing the call""" """Test failing the call"""
self.recipe.craft = mock.MagicMock(return_value=None) self.recipe.do_craft = mock.MagicMock(return_value=None)
# trigger exception # trigger exception
with self.assertRaises(crafting.CraftingError): with self.assertRaises(crafting.CraftingError):
@ -213,7 +213,7 @@ class TestCraftingRecipe(TestCase):
self.assertIsNotNone(self.tool1.pk) self.assertIsNotNone(self.tool1.pk)
self.assertIsNotNone(self.tool2.pk) self.assertIsNotNone(self.tool2.pk)
def test_seed__succcess(self): def test_seed__success(self):
"""Test seed helper classmethod""" """Test seed helper classmethod"""
# needed for other dbs to pass seed # needed for other dbs to pass seed

View file

@ -995,7 +995,8 @@ class AttributeHandler:
looked-after Attribute. looked-after Attribute.
default_access (bool, optional): If no `attrread` lock is set on default_access (bool, optional): If no `attrread` lock is set on
object, this determines if the lock should then be passed or not. object, this determines if the lock should then be passed or not.
return_list (bool, optional): return_list (bool, optional): Always return a list, also if there is only
one or zero matches found.
Returns: Returns:
result (any or list): One or more matches for keys and/or result (any or list): One or more matches for keys and/or

View file

@ -357,12 +357,14 @@ class EvenniaLogFile(logfile.LogFile):
_CHANNEL_LOG_NUM_TAIL_LINES = settings.CHANNEL_LOG_NUM_TAIL_LINES _CHANNEL_LOG_NUM_TAIL_LINES = settings.CHANNEL_LOG_NUM_TAIL_LINES
num_lines_to_append = _CHANNEL_LOG_NUM_TAIL_LINES num_lines_to_append = _CHANNEL_LOG_NUM_TAIL_LINES
def rotate(self): def rotate(self, num_lines_to_append=None):
""" """
Rotates our log file and appends some number of lines from Rotates our log file and appends some number of lines from
the previous log to the start of the new one. the previous log to the start of the new one.
""" """
append_tail = self.num_lines_to_append > 0 append_tail = (num_lines_to_append
if num_lines_to_append is not None
else self.num_lines_to_append)
if not append_tail: if not append_tail:
logfile.LogFile.rotate(self) logfile.LogFile.rotate(self)
return return
@ -472,6 +474,43 @@ def log_file(msg, filename="game.log"):
deferToThread(callback, filehandle, msg).addErrback(errback) deferToThread(callback, filehandle, msg).addErrback(errback)
def log_file_exists(filename="game.log"):
"""
Determine if a log-file already exists.
Args:
filename (str): The filename (within the log-dir).
Returns:
bool: If the log file exists or not.
"""
global _LOGDIR
if not _LOGDIR:
from django.conf import settings
_LOGDIR = settings.LOG_DIR
filename = os.path.join(_LOGDIR, filename)
return os.path.exists(filename)
def rotate_log_file(filename="game.log", num_lines_to_append=None):
"""
Force-rotate a log-file, without
Args:
filename (str): The log file, located in settings.LOG_DIR.
num_lines_to_append (int, optional): Include N number of
lines from previous file in new one. If `None`, use default.
Set to 0 to include no lines.
"""
if log_file_exists(filename):
file_handle = _open_log_file(filename)
if file_handle:
file_handle.rotate(num_lines_to_append=num_lines_to_append)
def tail_log_file(filename, offset, nlines, callback=None): def tail_log_file(filename, offset, nlines, callback=None):
""" """
Return the tail of the log file. Return the tail of the log file.