command_system.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  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. _RESPONSE_TEMPLATE = """Response{
  58. Original Message: %s,
  59. Text Response: %s,
  60. Image Response: %s,
  61. Respond: %s,
  62. Failure Reason: %s,
  63. Failure Notes: %s
  64. }"""
  65. @dataclass
  66. class RollbotResponse:
  67. msg: RollbotMessage
  68. txt: str = None
  69. img: str = None
  70. respond: bool = True
  71. failure: RollbotFailure = None
  72. debugging: dict = None
  73. def __post_init__(self):
  74. self.info = _RESPONSE_TEMPLATE % (self.msg, self.txt, self.img, self.respond, self.failure, self.debugging)
  75. self.is_success = self.failure is None
  76. if self.failure is None:
  77. self.failure_msg = None
  78. elif self.failure == RollbotFailure.INVALID_COMMAND:
  79. self.failure_msg = "Sorry - I don't think I understand the command '!%s'... " % self.msg.command \
  80. + "I'll try to figure it out and get back to you!"
  81. elif self.failure == RollbotFailure.MISSING_SUBCOMMAND:
  82. self.failure_msg = "Sorry - !%s requires a sub-command." % self.msg.command
  83. elif self.failure == RollbotFailure.INVALID_SUBCOMMAND:
  84. self.failure_msg = "Sorry - the sub-command you used for %s was not valid." % self.msg.command
  85. elif self.failure == RollbotFailure.INVALID_ARGUMENTS:
  86. self.failure_msg = "Sorry - %s cannot use those arguments!" % self.msg.command
  87. elif self.failure == RollbotFailure.SERVICE_DOWN:
  88. self.failure_msg = "Sorry - %s relies on a service I couldn't reach!" % self.msg.command
  89. elif self.failure == RollbotFailure.PERMISSIONS:
  90. self.failure_msg = "Sorry - you don't have permission to use that command or sub-command in this chat!"
  91. if self.debugging is not None and "explain" in self.debugging:
  92. self.failure_msg += " " + self.debugging["explain"]
  93. class RollbotPlugin:
  94. def __init__(self, command, logger=logging.getLogger(__name__)):
  95. self.command = command
  96. self.logger = logger
  97. self.logger.info(f"Intializing {type(self).__name__} matching {command}")
  98. def on_start(self, db):
  99. self.logger.info(f"No on_start initialization of {type(self).__name__}")
  100. def on_shutdown(self, db):
  101. self.logger.info(f"No on_shutdown de-initialization of {type(self).__name__}")
  102. def on_command(self, db, message):
  103. raise NotImplementedError
  104. def as_plugin(command):
  105. if isinstance(command, str):
  106. command_name = command
  107. else:
  108. command_name = command.__name__
  109. def init_standin(self, logger=logging.getLogger(__name__)):
  110. RollbotPlugin.__init__(self, command_name, logger)
  111. def decorator(fn):
  112. return type(
  113. f"AutoGenerated`{fn.__name__}`Command",
  114. (RollbotPlugin,),
  115. dict(
  116. __init__=init_standin,
  117. on_command=(lambda _, *a: fn(*a))
  118. )
  119. )
  120. if isinstance(command, str):
  121. return decorator
  122. else:
  123. return decorator(command)