platform_config.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. """
  2. Chatto Platform Config
  3. """
  4. import sys
  5. import os
  6. from pathlib import Path
  7. # 1. Den absoluten Pfad zum 'vendor'-Ordner in diesem Plugin ermitteln
  8. current_dir = Path(__file__).parent
  9. vendor_dir = current_dir / "vendor"
  10. # 2. Den vendor-Ordner an den Anfang des Suchpfads (sys.path) setzen
  11. if str(vendor_dir) not in sys.path:
  12. sys.path.insert(0, str(vendor_dir))
  13. import logging
  14. from typing import Any, Dict, Generic, Optional, TypeVar
  15. from gateway.config import PlatformConfig
  16. import utils
  17. # Absolute import — the vendor dir is on sys.path (see above) and chattolib's
  18. # own modules import each other absolutely ("from chattolib.x import y").
  19. # Importing it relatively as well would load a *second* copy of every module
  20. # under a different name, so isinstance() checks across the two would fail.
  21. from chattolib.client import ChattoClient
  22. logger = logging.getLogger(__name__)
  23. # --------------------------------------------------------------------------- #
  24. # Constants
  25. # --------------------------------------------------------------------------- #
  26. class ChattoConstants:
  27. """
  28. Chatto Platform Constants
  29. """
  30. def __init__(self):
  31. pass
  32. PLATFORM_NAME: str = "chatto-platform"
  33. PLATFORM_LABEL: str = "Chatto"
  34. INSTALL_HINT = "Requires a Chatto server. See https://docs.chatto.run"
  35. EXTRA_ENV_MAPPING = {
  36. "base_url": "CHATTO_BASE_URL",
  37. "home_channel": "CHATTO_HOME_CHANNEL",
  38. "require_mention": "CHATTO_REQUIRE_MENTION",
  39. "free_response_channels": "CHATTO_FREE_RESPONSE_CHANNELS",
  40. "auto_thread": "CHATTO_AUTO_THREAD",
  41. "allow_all_users": "CHATTO_ALLOW_ALL_USERS",
  42. "allowed_users": "CHATTO_ALLOWED_USERS",
  43. }
  44. MAX_MESSAGE_LENGTH = 10000
  45. SEEN_CAP = 500
  46. # WebSocket / realtime protocol
  47. WS_PATH = "/api/realtime"
  48. WS_AUTH_TIMEOUT = 20.0
  49. WS_MAX_MESSAGE_BYTES = 4_000_000
  50. WS_PING_INTERVAL = 30.0
  51. WS_PING_FAILURE_THRESHOLD = 3
  52. WS_RECONNECT_INITIAL_BACKOFF = 1.0
  53. WS_RECONNECT_MAX_BACKOFF = 30.0
  54. # HTTP timeout used for outbound URL fetches
  55. HTTP_TIMEOUT = 30
  56. # Emoji shortcode mapping (Chatto uses shortcode names, not unicode emoji)
  57. # Emoji shortcode mapping (Chatto uses shortcode names, not unicode emoji)
  58. EMOJI_TO_SHORTCODE: Dict[str, str] = {
  59. "👍": "thumbsup",
  60. "👎": "thumbsdown",
  61. "❤️": "heart",
  62. "❤": "heart",
  63. "✅": "white_check_mark",
  64. "❌": "x",
  65. "👀": "eyes",
  66. "🎉": "tada",
  67. "😂": "joy",
  68. "🚀": "rocket",
  69. "🔥": "fire",
  70. "💯": "100",
  71. "🤔": "thinking",
  72. "👏": "clap",
  73. "🙏": "pray",
  74. "😅": "sweat_smile",
  75. "😴": "sleeping",
  76. "⏳": "hourglass",
  77. }
  78. # Chunk size for asset uploads (256 KB)
  79. UPLOAD_CHUNK_SIZE = 256 * 1024
  80. def _get_env_or_extra_str_opt(env_var: str, extra_val: Optional[str | bool], default: Optional[str] = None) -> Optional[str]:
  81. """Get a value from environment variable or extra config."""
  82. env_value = os.getenv(env_var)
  83. if env_value is not None:
  84. return env_value.strip()
  85. if extra_val is not None:
  86. if isinstance(extra_val, str):
  87. return extra_val.strip()
  88. elif isinstance(extra_val, bool):
  89. return str(extra_val)
  90. else:
  91. logger.error("extra_val: %s is supposed to be str, but %s was found.", extra_val, str(type(extra_val)))
  92. if default is not None:
  93. logger.debug("Chatto: Defaulting to '%s'", default)
  94. return default.strip()
  95. return None
  96. def _get_env_or_extra_str(env_var: str, extra_val: Optional[str | bool], default: Optional[str] = None) -> str:
  97. """Get a value from environment variable or extra config."""
  98. my_string = _get_env_or_extra_str_opt(env_var, extra_val, default)
  99. if my_string:
  100. return my_string
  101. return ""
  102. def _get_env_or_extra_truthy(env_var: str, extra_val: Optional[str | bool], default: bool = False) -> bool:
  103. """Get a boolean value from environment variable or extra config."""
  104. return utils.is_truthy_value(_get_env_or_extra_str(env_var, extra_val, str(default)), default)
  105. def _split_str_to_list(mystring: str) -> list:
  106. logger.info("mystring: %s", mystring)
  107. return list(c for c in mystring.split(","))
  108. def _get_env_or_extra_list(env_var: str, extra_val: Optional[list[str]]) -> list[str]:
  109. """Get a list of values from environment variable or extra config."""
  110. env_value = os.getenv(env_var)
  111. if env_value is not None:
  112. return _split_str_to_list(env_value)
  113. if extra_val is not None:
  114. #logger.info("extra_val is1: " + str(type(extra_val)))
  115. #logger.info("extra_val is2: " + str(extra_val))
  116. if isinstance(extra_val, list):
  117. return list(
  118. c for c in extra_val
  119. )
  120. if isinstance(extra_val, str):
  121. return _split_str_to_list(extra_val)
  122. return []
  123. from typing import TypeVar, Generic, Any
  124. # 1. Define a generic Type Variable
  125. T = TypeVar('T')
  126. # 2. Inherit from Generic[T]
  127. class ConfigField(Generic[T]):
  128. """Repräsentiert ein einzelnes Konfigurationsfeld mit IDE-Support."""
  129. # 3. Type 'value' as T (or T | None since it starts as None)
  130. value: T
  131. field_name: str = ""
  132. env_name: str = ""
  133. # 4. Hint that the init argument should match type T
  134. # We add `| Any` as a fallback because complex types like `list[str]`
  135. # can sometimes confuse older type checkers when used with `type[T]`
  136. def __init__(self, type_: type[T] | Any):
  137. self._type = type_
  138. # 5. Ensure the IDE knows only type T can be assigned
  139. def __set__(self, instance: Any, value: T) -> None:
  140. self.value = value
  141. self._type = type(value)
  142. logger.info("Chatto: configuration field '%s' set to '%s'", self.field_name, str(value))
  143. def __str__(self) -> str:
  144. return str(self.value)
  145. def __set_name__(self, owner: Any, name: str) -> None:
  146. temp: str = str(name).lower().replace("chatto_", "", 1)
  147. self.field_name = temp
  148. self.env_name = f"CHATTO_{temp.upper()}"
  149. @dataclass
  150. class ChattoConfiguration:
  151. """
  152. Chatto Platform Config
  153. Some constants and getting from environment/config.yaml
  154. """
  155. base_url = ConfigField(str)
  156. token = ConfigField(str | None)
  157. login = ConfigField(str)
  158. password = ConfigField(str)
  159. channels = ConfigField(str)
  160. channels_list = ConfigField(list[str])
  161. home_channel = ConfigField(str)
  162. allowed_users = ConfigField(list[str])
  163. require_mention = ConfigField(bool)
  164. free_response_channels_list = ConfigField(list[str])
  165. auto_thread = ConfigField(bool)
  166. allow_all_users = ConfigField(bool)
  167. reactions = ConfigField(bool)
  168. def __init__(self, pconfig: PlatformConfig):
  169. """PlatformConfig from Hermes provides our own configuration within the "extra"
  170. object. But here, we allow overriding via environment variables again.
  171. We take our Configuration from this typed Class, because it is easier access than using extra.get["base_url"].
  172. """
  173. self.base_url.value = _get_env_or_extra_str(self.base_url.env_name,
  174. pconfig.extra.get(self.base_url.field_name), ChattoClient.DEFAULT_BASE_URL)
  175. self.token.value = _get_env_or_extra_str_opt(self.token.env_name, pconfig.extra.get(self.token.field_name))
  176. self.login.value = _get_env_or_extra_str(self.login.env_name, pconfig.extra.get(self.login.field_name))
  177. self.password.value = _get_env_or_extra_str(self.password.env_name, pconfig.extra.get(self.password.field_name))
  178. self.channels.value = str(_get_env_or_extra_list(self.channels.env_name, pconfig.extra.get(self.channels.field_name)))
  179. self.channels_list.value = _get_env_or_extra_list(self.channels.env_name, pconfig.extra.get(self.channels.field_name))
  180. self.home_channel.value = _get_env_or_extra_str(self.home_channel.env_name, pconfig.extra.get(self.home_channel.field_name))
  181. self.allowed_users.value = _get_env_or_extra_list(self.allowed_users.env_name, pconfig.extra.get(self.allowed_users.field_name))
  182. logger.info("self.allowed_users.value: %s", self.allowed_users.value)
  183. self.require_mention.value = _get_env_or_extra_truthy(self.require_mention.env_name, pconfig.extra.get(self.require_mention.field_name), False)
  184. logger.info("self.require_mention.value: %s", self.require_mention.value)
  185. # free_response_channels: room IDs where the bot responds without being mentioned via "@botname" when require_mention is true.
  186. self.free_response_channels_list.value = _get_env_or_extra_list(self.free_response_channels_list.env_name,
  187. pconfig.extra.get(self.free_response_channels_list.field_name))
  188. # Auto-thread: by default, Chatto creates a thread for replies to room
  189. # messages (not DMs, not already in a thread). This keeps conversations
  190. # organized in the room. Can be disabled via extra.auto_thread=false.
  191. self.auto_thread.value = _get_env_or_extra_truthy(self.auto_thread.env_name, pconfig.extra.get(self.auto_thread.field_name), True)
  192. self.allow_all_users.value = _get_env_or_extra_truthy(self.allow_all_users.env_name, pconfig.extra.get(self.allow_all_users.field_name))
  193. self.reactions.value = _get_env_or_extra_truthy(self.reactions.env_name, pconfig.extra.get(self.reactions.field_name), True)
  194. logger.info("ChattoConfiguration: %s", self)