platform_config.py 8.7 KB

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