command_system.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import logging
  2. from dataclasses import dataclass
  3. from enum import Enum, auto
  4. from sqlalchemy.ext.declarative import declarative_base
  5. from config import GLOBAL_ADMINS, GROUP_ADMINS
  6. BANGS = ('!',)
  7. ModelBase = declarative_base()
  8. def pop_arg(text):
  9. if text is None:
  10. return None, None
  11. parts = text.split(maxsplit=1)
  12. if len(parts) == 1:
  13. return parts[0], None
  14. return parts[0], parts[1].strip()
  15. @dataclass
  16. class RollbotMessage:
  17. src: str
  18. name: str
  19. sender_id: str
  20. group_id: str
  21. message_id: str
  22. message_txt: str
  23. def __post_init__(self):
  24. self.is_command = False
  25. if len(self.message_txt) > 0 and self.message_txt[0] in BANGS:
  26. cmd, raw = pop_arg(self.message_txt[1:].strip())
  27. if cmd is not None:
  28. self.is_command = True
  29. self.command = cmd.lower()
  30. self.raw_args = raw
  31. self.from_admin = self.sender_id is not None and \
  32. self.sender_id in GLOBAL_ADMINS or (
  33. self.group_id in GROUP_ADMINS and
  34. self.sender_id in GROUP_ADMINS[self.group_id])
  35. @staticmethod
  36. def from_groupme(msg):
  37. return RollbotMessage("GROUPME", msg["name"], msg["sender_id"], msg["group_id"], msg["id"], msg["text"].strip())
  38. @staticmethod
  39. def from_web(content):
  40. content = content.strip()
  41. if len(content) > 0 and content[0] not in BANGS:
  42. content = BANGS[0] + content
  43. # TODO should still assign an id...
  44. return RollbotMessage("WEB_FRONTEND", "user", None, None, None, content)
  45. def args(self, normalize=True):
  46. arg, rest = pop_arg(self.raw_args)
  47. while arg is not None:
  48. yield arg.lower() if normalize else arg
  49. arg, rest = pop_arg(rest)
  50. class RollbotFailure(Enum):
  51. INVALID_COMMAND = auto()
  52. MISSING_SUBCOMMAND = auto()
  53. INVALID_SUBCOMMAND = auto()
  54. INVALID_ARGUMENTS = auto()
  55. SERVICE_DOWN = auto()
  56. PERMISSIONS = auto()
  57. INTERNAL_ERROR = auto()
  58. _RESPONSE_TEMPLATE = """Response{
  59. Original Message: %s,
  60. Text Response: %s,
  61. Image Response: %s,
  62. Respond: %s,
  63. Failure Reason: %s,
  64. Failure Notes: %s
  65. }"""
  66. @dataclass
  67. class RollbotResponse:
  68. msg: RollbotMessage
  69. txt: str = None
  70. img: str = None
  71. respond: bool = True
  72. failure: RollbotFailure = None
  73. debugging: dict = None
  74. def __post_init__(self):
  75. self.info = _RESPONSE_TEMPLATE % (self.msg, self.txt, self.img, self.respond, self.failure, self.debugging)
  76. self.is_success = self.failure is None
  77. if self.failure is None:
  78. self.failure_msg = None
  79. elif self.failure == RollbotFailure.INVALID_COMMAND:
  80. self.failure_msg = "Sorry - I don't think I understand the command '!%s'... " % self.msg.command \
  81. + "I'll try to figure it out and get back to you!"
  82. elif self.failure == RollbotFailure.MISSING_SUBCOMMAND:
  83. self.failure_msg = "Sorry - !%s requires a sub-command." % self.msg.command
  84. elif self.failure == RollbotFailure.INVALID_SUBCOMMAND:
  85. self.failure_msg = "Sorry - the sub-command you used for %s was not valid." % self.msg.command
  86. elif self.failure == RollbotFailure.INVALID_ARGUMENTS:
  87. self.failure_msg = "Sorry - %s cannot use those arguments!" % self.msg.command
  88. elif self.failure == RollbotFailure.SERVICE_DOWN:
  89. self.failure_msg = "Sorry - %s relies on a service I couldn't reach!" % self.msg.command
  90. elif self.failure == RollbotFailure.PERMISSIONS:
  91. self.failure_msg = "Sorry - you don't have permission to use that command or sub-command in this chat!"
  92. elif self.failure == RollbotFailure.INTERNAL_ERROR:
  93. self.failure_msg = "Sorry - I encountered an unrecoverable error, please review internal logs."
  94. if self.debugging is not None and "explain" in self.debugging:
  95. self.failure_msg += " " + self.debugging["explain"]
  96. class RollbotPlugin:
  97. def __init__(self, command, logger=logging.getLogger(__name__)):
  98. self.command = command
  99. self.logger = logger
  100. self.logger.info(f"Intializing {type(self).__name__} matching {command}")
  101. def on_start(self, db):
  102. self.logger.info(f"No on_start initialization of {type(self).__name__}")
  103. def on_shutdown(self, db):
  104. self.logger.info(f"No on_shutdown de-initialization of {type(self).__name__}")
  105. def on_command(self, db, message):
  106. raise NotImplementedError
  107. def as_plugin(command):
  108. if isinstance(command, str):
  109. command_name = command
  110. else:
  111. command_name = command.__name__
  112. def init_standin(self, logger=logging.getLogger(__name__)):
  113. RollbotPlugin.__init__(self, command_name, logger)
  114. def decorator(fn):
  115. return type(
  116. f"AutoGenerated`{fn.__name__}`Command",
  117. (RollbotPlugin,),
  118. dict(
  119. __init__=init_standin,
  120. on_command=(lambda _, *a: fn(*a))
  121. )
  122. )
  123. if isinstance(command, str):
  124. return decorator
  125. else:
  126. return decorator(command)