platform_config.py 9.0 KB

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