"""
In-Game Mail system
Evennia Contribution - grungies1138 2016
A simple Brandymail style @mail system that uses the Msg class from Evennia
Core. It has two Commands, both of which can be used on their own:
- CmdMail - this should sit on the Account cmdset and makes the `mail` command
available both IC and OOC. Mails will always go to Accounts (other players).
- CmdMailCharacter - this should sit on the Character cmdset and makes the `mail`
command ONLY available when puppeting a character. Mails will be sent to other
Characters only and will not be available when OOC.
- If adding *both* commands to their respective cmdsets, you'll get two separate
IC and OOC mailing systems, with different lists of mail for IC and OOC modes.
Installation:
Install one or both of the following (see above):
- CmdMail (IC + OOC mail, sent between players)
# mygame/commands/default_cmds.py
from evennia.contrib.game_systems import mail
# in AccountCmdSet.at_cmdset_creation:
self.add(mail.CmdMail())
- CmdMailCharacter (optional, IC only mail, sent between characters)
# mygame/commands/default_cmds.py
from evennia.contrib.game_systems import mail
# in CharacterCmdSet.at_cmdset_creation:
self.add(mail.CmdMailCharacter())
Once installed, use `help mail` in game for help with the mail command. Use
ic/ooc to switch in and out of IC/OOC modes.
"""
import re
from evennia import AccountDB, ObjectDB, default_cmds
from evennia.comms.models import Msg
from evennia.utils import create, datetime_format, evtable, inherits_from, make_iter
_HEAD_CHAR = "|015-|n"
_SUB_HEAD_CHAR = "-"
_WIDTH = 78
[docs]class CmdMail(default_cmds.MuxAccountCommand):
"""
Communicate with others by sending mail.
Usage:
@mail - Displays all the mail an account has in their mailbox
@mail <#> - Displays a specific message
@mail <accounts>=<subject>/<message>
- Sends a message to the comma separated list of accounts.
@mail/delete <#> - Deletes a specific message
@mail/forward <account list>=<#>[/<Message>]
- Forwards an existing message to the specified list of accounts,
original message is delivered with optional Message prepended.
@mail/reply <#>=<message>
- Replies to a message #. Prepends message to the original
message text.
Switches:
delete - deletes a message
forward - forward a received message to another object with an optional message attached.
reply - Replies to a received message, appending the original message to the bottom.
Examples:
@mail 2
@mail Griatch=New mail/Hey man, I am sending you a message!
@mail/delete 6
@mail/forward feend78 Griatch=4/You guys should read this.
@mail/reply 9=Thanks for the info!
"""
key = "@mail"
aliases = ["mail"]
lock = "cmd:all()"
help_category = "General"
[docs] def parse(self):
"""
Add convenience check to know if caller is an Account or not since this cmd
will be able to add to either Object- or Account level.
"""
super().parse()
self.caller_is_account = bool(
inherits_from(self.caller, "evennia.accounts.accounts.DefaultAccount")
)
[docs] def search_targets(self, namelist):
"""
Search a list of targets of the same type as caller.
Args:
caller (Object or Account): The type of object to search.
namelist (list): List of strings for objects to search for.
Returns:
targetlist (Queryset): Any target matches.
"""
nameregex = r"|".join(r"^%s$" % re.escape(name) for name in make_iter(namelist))
if self.caller_is_account:
matches = AccountDB.objects.filter(username__iregex=nameregex)
else:
matches = ObjectDB.objects.filter(db_key__iregex=nameregex)
return matches
[docs] def get_all_mail(self):
"""
Returns a list of all the messages where the caller is a recipient. These
are all messages tagged with tags of the `mail` category.
Returns:
messages (QuerySet): Matching Msg objects.
"""
if self.caller_is_account:
return Msg.objects.get_by_tag(category="mail").filter(db_receivers_accounts=self.caller)
else:
return Msg.objects.get_by_tag(category="mail").filter(db_receivers_objects=self.caller)
[docs] def send_mail(self, recipients, subject, message, caller):
"""
Function for sending new mail. Also useful for sending notifications
from objects or systems.
Args:
recipients (list): list of Account or Character objects to receive
the newly created mails.
subject (str): The header or subject of the message to be delivered.
message (str): The body of the message being sent.
caller (obj): The object (or Account or Character) that is sending the message.
"""
for recipient in recipients:
recipient.msg("You have received a new @mail from %s" % caller)
new_message = create.create_message(
self.caller, message, receivers=recipient, header=subject
)
new_message.tags.add("new", category="mail")
if recipients:
caller.msg("You sent your message.")
return
else:
caller.msg("No valid target(s) found. Cannot send message.")
return
[docs] def func(self):
"""
Do the main command functionality
"""
subject = ""
body = ""
if self.switches or self.args:
if "delete" in self.switches or "del" in self.switches:
try:
if not self.lhs:
self.caller.msg("No Message ID given. Unable to delete.")
return
else:
all_mail = self.get_all_mail()
mind_max = max(0, all_mail.count() - 1)
mind = max(0, min(mind_max, int(self.lhs) - 1))
if all_mail[mind]:
mail = all_mail[mind]
question = "Delete message {} ({}) [Y]/N?".format(mind + 1, mail.header)
ret = yield (question)
# handle not ret, it will be None during unit testing
if not ret or ret.strip().upper() not in ("N", "No"):
all_mail[mind].delete()
self.caller.msg("Message %s deleted" % (mind + 1,))
else:
self.caller.msg("Message not deleted.")
else:
raise IndexError
except IndexError:
self.caller.msg("That message does not exist.")
except ValueError:
self.caller.msg("Usage: @mail/delete <message ID>")
elif "forward" in self.switches or "fwd" in self.switches:
try:
if not self.rhs:
self.caller.msg(
"Cannot forward a message without a target list. " "Please try again."
)
return
elif not self.lhs:
self.caller.msg("You must define a message to forward.")
return
else:
all_mail = self.get_all_mail()
mind_max = max(0, all_mail.count() - 1)
if "/" in self.rhs:
message_number, message = self.rhs.split("/", 1)
mind = max(0, min(mind_max, int(message_number) - 1))
if all_mail[mind]:
old_message = all_mail[mind]
self.send_mail(
self.search_targets(self.lhslist),
"FWD: " + old_message.header,
message
+ "\n---- Original Message ----\n"
+ old_message.message,
self.caller,
)
self.caller.msg("Message forwarded.")
else:
raise IndexError
else:
mind = max(0, min(mind_max, int(self.rhs) - 1))
if all_mail[mind]:
old_message = all_mail[mind]
self.send_mail(
self.search_targets(self.lhslist),
"FWD: " + old_message.header,
"\n---- Original Message ----\n" + old_message.message,
self.caller,
)
self.caller.msg("Message forwarded.")
old_message.tags.remove("new", category="mail")
old_message.tags.add("fwd", category="mail")
else:
raise IndexError
except IndexError:
self.caller.msg("Message does not exist.")
except ValueError:
self.caller.msg("Usage: @mail/forward <account list>=<#>[/<Message>]")
elif "reply" in self.switches or "rep" in self.switches:
try:
if not self.rhs:
self.caller.msg("You must define a message to reply to.")
return
elif not self.lhs:
self.caller.msg("You must supply a reply message")
return
else:
all_mail = self.get_all_mail()
mind_max = max(0, all_mail.count() - 1)
mind = max(0, min(mind_max, int(self.lhs) - 1))
if all_mail[mind]:
old_message = all_mail[mind]
self.send_mail(
old_message.senders,
"RE: " + old_message.header,
self.rhs + "\n---- Original Message ----\n" + old_message.message,
self.caller,
)
old_message.tags.remove("new", category="mail")
old_message.tags.add("-", category="mail")
return
else:
raise IndexError
except IndexError:
self.caller.msg("Message does not exist.")
except ValueError:
self.caller.msg("Usage: @mail/reply <#>=<message>")
else:
# normal send
if self.rhs:
if "/" in self.rhs:
subject, body = self.rhs.split("/", 1)
else:
body = self.rhs
self.send_mail(self.search_targets(self.lhslist), subject, body, self.caller)
else:
all_mail = self.get_all_mail()
mind_max = max(0, all_mail.count() - 1)
try:
mind = max(0, min(mind_max, int(self.lhs) - 1))
message = all_mail[mind]
except (ValueError, IndexError):
self.caller.msg("'%s' is not a valid mail id." % self.lhs)
return
messageForm = []
if message:
messageForm.append(_HEAD_CHAR * _WIDTH)
messageForm.append(
"|wFrom:|n %s" % (message.senders[0].get_display_name(self.caller))
)
# note that we cannot use %-d format here since Windows does not support it
day = message.db_date_created.day
messageForm.append(
"|wSent:|n %s"
% message.db_date_created.strftime(f"%b {day}, %Y - %H:%M:%S")
)
messageForm.append("|wSubject:|n %s" % message.header)
messageForm.append(_SUB_HEAD_CHAR * _WIDTH)
messageForm.append(message.message)
messageForm.append(_HEAD_CHAR * _WIDTH)
self.caller.msg("\n".join(messageForm))
message.tags.remove("new", category="mail")
message.tags.add("-", category="mail")
else:
# list messages
messages = self.get_all_mail()
if messages:
table = evtable.EvTable(
"|wID|n",
"|wFrom|n",
"|wSubject|n",
"|wArrived|n",
"",
table=None,
border="header",
header_line_char=_SUB_HEAD_CHAR,
width=_WIDTH,
)
index = 1
for message in messages:
status = str(message.db_tags.last().db_key.upper())
if status == "NEW":
status = "|gNEW|n"
table.add_row(
index,
message.senders[0].get_display_name(self.caller),
message.header,
datetime_format(message.db_date_created),
status,
)
index += 1
table.reformat_column(0, width=6)
table.reformat_column(1, width=18)
table.reformat_column(2, width=34)
table.reformat_column(3, width=13)
table.reformat_column(4, width=7)
self.caller.msg(_HEAD_CHAR * _WIDTH)
self.caller.msg(str(table))
self.caller.msg(_HEAD_CHAR * _WIDTH)
else:
self.caller.msg("There are no messages in your inbox.")
# character - level version of the command
[docs]class CmdMailCharacter(CmdMail):
account_caller = False