123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240 |
- from dataclasses import dataclass
- from enum import Enum, auto
- BANGS = ('!',)
- def pop_arg(text):
- """
- Pop an argument from a string of text. The text is split at the first
- substring containing only whitespace characters, ignoring leading and
- trailing whitespace of the string, with the text preceeding being
- returned as the first value, and the text following being returned
- as the second value.
-
- Both return values will be stripped of leading and trailing whitespace.
- If the given text is None, both return values will be None. If there is
- no text following the split point, the second return value will be None.
- """
- if text is None:
- return None, None
- text = text.strip()
- parts = text.split(maxsplit=1)
- if len(parts) == 1:
- return parts[0], None
- return parts[0], parts[1]
- @dataclass
- class RollbotMessage:
- """
- A data class modeling a message that was received by a Rollbot.
- This class is used both for mundane messages and for commands.
- Init Fields:
- src - a string describing the source of the message, usually GROUPME
- name - the plaintext display name of the sender of the message
- sender_id - the service-specific id of the sender, which will not change
- group_id - the id of the "group" the message was sent to, which can
- mean different concepts depending on src
- message_id - the service-specific unique id of the message
- message_txt - the raw, full text of the message
- from_admin - a boolean flag denoting if the sender has admin privileges
- Derived Fields:
- is_command - a boolean flag denoting if the message is a command. This
- will be true if message_txt begins with a "!" character followed by
- one or more non-whitespace characters (with whitespace between the
- bang and the first non-whitespace character being ignored)
- raw_command - the raw text of the command, i.e., the first "word" after
- the bang, with leading and trailing whitespace removed. This field
- will only be present if is_command is True
- command - raw_command normalized to lower case. This field will only be
- present if is_command is True
- raw_args - the raw text of the arguments following command, i.e., the
- remaining ontent of message_txt, with leading and trailing whitespace
- removed. This field will only be present if is_command is True
- """
- src: str
- name: str
- sender_id: str
- group_id: str
- message_id: str
- message_txt: str
- from_admin: bool
- def __post_init__(self):
- self.is_command = False
- self._subc_memo = None
- if self.message_txt is not None and len(self.message_txt) > 0 and self.message_txt[0] in BANGS:
- cmd, raw = pop_arg(self.message_txt[1:].strip())
- if cmd is not None:
- self.is_command = True
- self.raw_command = cmd
- self.command = cmd.lower()
- self.raw_args = raw
- self._arg_list_memo = None
-
- @staticmethod
- def from_subcommand(msg):
- if msg._subc_memo is None:
- msg._subc_memo = RollbotMessage(
- msg.src,
- msg.name,
- msg.sender_id,
- msg.group_id,
- msg.message_id,
- msg.raw_args, # this strips the command of the old message
- msg.from_admin
- )
- if msg._subc_memo.is_command:
- return msg._subc_memo
- # silently return None if a subcommand could not be made
- @staticmethod
- def from_groupme(msg, global_admins=(), group_admins={}):
- sender_id = msg["sender_id"]
- group_id = msg["group_id"]
- return RollbotMessage(
- "GROUPME",
- msg["name"],
- sender_id,
- group_id,
- msg["id"],
- msg["text"].strip(),
- sender_id in global_admins or (
- group_id in group_admins and
- sender_id in group_admins[group_id])
- )
- @staticmethod
- def from_discord(msg, global_admins=(), group_admins={}):
- sender_id = str(msg.author.id)
- group_id = str(msg.channel.id)
- return RollbotMessage(
- "DISCORD",
- msg.author.name,
- sender_id,
- group_id,
- msg.id,
- msg.content.strip(),
- sender_id in global_admins or (
- group_id in group_admins and
- sender_id in group_admins[group_id])
- )
- def args(self, normalize=True):
- """
- Lazily pop arguments from the raw argument string of this message
- and yield them one at a time as a generator. If the optional
- normalize parameter is set to False, the arguments will
- be returned exactly as they appear in the message, and if normalize
- is set to True or omitted, the arguments will be converted to lower
- case.
-
- For details on argument "popping", see the rollbot.pop_arg function.
- Behavior is undefined if this method is called on a message whose
- is_command field is false.
- """
- arg, rest = pop_arg(self.raw_args)
- while arg is not None:
- yield arg.lower() if normalize else arg
- arg, rest = pop_arg(rest)
- def arg_list(self):
- """
- Take the raw argument string of this message and split it on any
- sequence of one or more whitespace characters, and return the result.
- This can be useful to pass to an argparse.ArgumentParser.
- Behavior is undefined if this method is called on a message whose
- is_command field is false.
- """
- if self._arg_list_memo is None:
- self._arg_list_memo = self.raw_args.split()
- return self._arg_list_memo
- class RollbotFailureException(BaseException):
- def __init__(self, failure):
- super().__init__()
- self.failure = failure
- class RollbotFailure(Enum):
- INVALID_COMMAND = auto()
- MISSING_SUBCOMMAND = auto()
- INVALID_SUBCOMMAND = auto()
- INVALID_ARGUMENTS = auto()
- SERVICE_DOWN = auto()
- PERMISSIONS = auto()
- INTERNAL_ERROR = auto()
- def get_debugging(self):
- debugging = {}
- reason = getattr(self, "reason", None)
- if reason is not None:
- debugging["explain"] = reason
- exception = getattr(self, "exception", None)
- if exception is not None:
- debugging["exception"] = exception
- return debugging
- def with_reason(self, reason):
- self.reason = reason
- return self
- def with_exception(self, exception):
- self.exception = exception
- return self
- def raise_exc(self):
- raise RollbotFailureException(self)
- _RESPONSE_TEMPLATE = """Response{
- Original Message: %s,
- Text Response: %s,
- Image Response: %s,
- Respond: %s,
- Failure Reason: %s,
- Failure Notes: %s
- }"""
- @dataclass
- class RollbotResponse:
- msg: RollbotMessage
- txt: str = None
- img: str = None
- respond: bool = True
- failure: RollbotFailure = None
- debugging: dict = None
- def __post_init__(self):
- self.info = _RESPONSE_TEMPLATE % (self.msg, self.txt, self.img, self.respond, self.failure, self.debugging)
- self.is_success = self.failure is None
- if self.failure is None:
- self.failure_msg = None
- elif self.failure == RollbotFailure.INVALID_COMMAND:
- self.failure_msg = "Sorry - I don't think I understand the command '!%s'... " % self.msg.command \
- + "I'll try to figure it out and get back to you!"
- elif self.failure == RollbotFailure.MISSING_SUBCOMMAND:
- self.failure_msg = "Sorry - !%s requires a sub-command." % self.msg.command
- elif self.failure == RollbotFailure.INVALID_SUBCOMMAND:
- self.failure_msg = "Sorry - the sub-command you used for %s was not valid." % self.msg.command
- elif self.failure == RollbotFailure.INVALID_ARGUMENTS:
- self.failure_msg = "Sorry - !%s cannot use those arguments!" % self.msg.command
- elif self.failure == RollbotFailure.SERVICE_DOWN:
- self.failure_msg = "Sorry - !%s relies on a service I couldn't reach!" % self.msg.command
- elif self.failure == RollbotFailure.PERMISSIONS:
- self.failure_msg = "Sorry - you don't have permission to use that command or sub-command in this chat!"
- elif self.failure == RollbotFailure.INTERNAL_ERROR:
- self.failure_msg = "Sorry - I encountered an unrecoverable error, please review internal logs."
- if self.debugging is not None and "explain" in self.debugging:
- self.failure_msg += " " + self.debugging["explain"]
|