slidge 0.1.0rc1__py3-none-any.whl → 0.1.2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (164) hide show
  1. slidge/__init__.py +54 -31
  2. slidge/__main__.py +51 -5
  3. slidge/command/__init__.py +28 -0
  4. slidge/command/adhoc.py +258 -0
  5. slidge/command/admin.py +193 -0
  6. slidge/command/base.py +441 -0
  7. slidge/command/categories.py +3 -0
  8. slidge/command/chat_command.py +288 -0
  9. slidge/command/register.py +179 -0
  10. slidge/command/user.py +250 -0
  11. slidge/contact/__init__.py +8 -0
  12. slidge/contact/contact.py +452 -0
  13. slidge/contact/roster.py +192 -0
  14. slidge/core/__init__.py +2 -0
  15. slidge/core/cache.py +121 -39
  16. slidge/core/config.py +116 -11
  17. slidge/core/gateway/__init__.py +3 -0
  18. slidge/core/gateway/base.py +895 -0
  19. slidge/core/gateway/caps.py +63 -0
  20. slidge/core/gateway/delivery_receipt.py +52 -0
  21. slidge/core/gateway/disco.py +80 -0
  22. slidge/core/gateway/mam.py +75 -0
  23. slidge/core/gateway/muc_admin.py +35 -0
  24. slidge/core/gateway/ping.py +66 -0
  25. slidge/core/gateway/presence.py +95 -0
  26. slidge/core/gateway/registration.py +53 -0
  27. slidge/core/gateway/search.py +102 -0
  28. slidge/core/gateway/session_dispatcher.py +795 -0
  29. slidge/core/gateway/vcard_temp.py +130 -0
  30. slidge/core/mixins/__init__.py +9 -1
  31. slidge/core/mixins/attachment.py +506 -0
  32. slidge/core/mixins/avatar.py +167 -0
  33. slidge/core/mixins/base.py +6 -19
  34. slidge/core/mixins/disco.py +66 -15
  35. slidge/core/mixins/lock.py +31 -0
  36. slidge/core/mixins/message.py +254 -252
  37. slidge/core/mixins/message_maker.py +154 -0
  38. slidge/core/mixins/presence.py +128 -31
  39. slidge/core/mixins/recipient.py +43 -0
  40. slidge/core/pubsub.py +275 -116
  41. slidge/core/session.py +586 -518
  42. slidge/group/__init__.py +10 -0
  43. slidge/group/archive.py +125 -0
  44. slidge/group/bookmarks.py +163 -0
  45. slidge/group/participant.py +458 -0
  46. slidge/group/room.py +1103 -0
  47. slidge/migration.py +18 -0
  48. slidge/slixfix/__init__.py +68 -0
  49. slidge/{util/xep_0050 → slixfix/link_preview}/__init__.py +4 -5
  50. slidge/slixfix/link_preview/link_preview.py +17 -0
  51. slidge/slixfix/link_preview/stanza.py +99 -0
  52. slidge/slixfix/roster.py +60 -0
  53. slidge/{util → slixfix}/xep_0077/register.py +1 -2
  54. slidge/slixfix/xep_0077/stanza.py +104 -0
  55. slidge/{util → slixfix}/xep_0100/gateway.py +17 -12
  56. slidge/slixfix/xep_0153/__init__.py +10 -0
  57. slidge/slixfix/xep_0153/stanza.py +25 -0
  58. slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
  59. slidge/slixfix/xep_0264/__init__.py +5 -0
  60. slidge/slixfix/xep_0264/stanza.py +36 -0
  61. slidge/slixfix/xep_0264/thumbnail.py +23 -0
  62. slidge/slixfix/xep_0292/__init__.py +5 -0
  63. slidge/slixfix/xep_0292/vcard4.py +100 -0
  64. slidge/slixfix/xep_0313/__init__.py +12 -0
  65. slidge/slixfix/xep_0313/mam.py +262 -0
  66. slidge/slixfix/xep_0313/stanza.py +359 -0
  67. slidge/slixfix/xep_0317/__init__.py +5 -0
  68. slidge/slixfix/xep_0317/hats.py +17 -0
  69. slidge/slixfix/xep_0317/stanza.py +28 -0
  70. slidge/{util → slixfix}/xep_0356_old/privilege.py +9 -7
  71. slidge/slixfix/xep_0424/__init__.py +9 -0
  72. slidge/slixfix/xep_0424/retraction.py +77 -0
  73. slidge/slixfix/xep_0424/stanza.py +28 -0
  74. slidge/slixfix/xep_0490/__init__.py +8 -0
  75. slidge/slixfix/xep_0490/mds.py +47 -0
  76. slidge/slixfix/xep_0490/stanza.py +17 -0
  77. slidge/util/__init__.py +4 -6
  78. slidge/util/archive_msg.py +61 -0
  79. slidge/util/conf.py +25 -4
  80. slidge/util/db.py +23 -69
  81. slidge/util/schema.sql +126 -0
  82. slidge/util/sql.py +508 -0
  83. slidge/util/test.py +136 -86
  84. slidge/util/types.py +155 -14
  85. slidge/util/util.py +225 -51
  86. slidge-0.1.2.dist-info/METADATA +111 -0
  87. slidge-0.1.2.dist-info/RECORD +96 -0
  88. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/WHEEL +1 -1
  89. slidge/core/adhoc.py +0 -492
  90. slidge/core/chat_command.py +0 -197
  91. slidge/core/contact.py +0 -441
  92. slidge/core/disco.py +0 -59
  93. slidge/core/gateway.py +0 -899
  94. slidge/core/muc/__init__.py +0 -3
  95. slidge/core/muc/bookmarks.py +0 -74
  96. slidge/core/muc/participant.py +0 -152
  97. slidge/core/muc/room.py +0 -348
  98. slidge/plugins/discord/__init__.py +0 -121
  99. slidge/plugins/discord/client.py +0 -121
  100. slidge/plugins/discord/session.py +0 -172
  101. slidge/plugins/dummy.py +0 -334
  102. slidge/plugins/facebook.py +0 -591
  103. slidge/plugins/hackernews.py +0 -209
  104. slidge/plugins/mattermost/__init__.py +0 -1
  105. slidge/plugins/mattermost/api.py +0 -288
  106. slidge/plugins/mattermost/gateway.py +0 -417
  107. slidge/plugins/mattermost/websocket.py +0 -248
  108. slidge/plugins/signal/__init__.py +0 -4
  109. slidge/plugins/signal/config.py +0 -4
  110. slidge/plugins/signal/contact.py +0 -104
  111. slidge/plugins/signal/gateway.py +0 -379
  112. slidge/plugins/signal/group.py +0 -76
  113. slidge/plugins/signal/session.py +0 -515
  114. slidge/plugins/signal/txt.py +0 -13
  115. slidge/plugins/signal/util.py +0 -32
  116. slidge/plugins/skype.py +0 -310
  117. slidge/plugins/steam.py +0 -400
  118. slidge/plugins/telegram/__init__.py +0 -6
  119. slidge/plugins/telegram/client.py +0 -325
  120. slidge/plugins/telegram/config.py +0 -21
  121. slidge/plugins/telegram/contact.py +0 -154
  122. slidge/plugins/telegram/gateway.py +0 -182
  123. slidge/plugins/telegram/group.py +0 -184
  124. slidge/plugins/telegram/session.py +0 -275
  125. slidge/plugins/telegram/util.py +0 -153
  126. slidge/plugins/whatsapp/__init__.py +0 -6
  127. slidge/plugins/whatsapp/config.py +0 -17
  128. slidge/plugins/whatsapp/contact.py +0 -33
  129. slidge/plugins/whatsapp/event.go +0 -455
  130. slidge/plugins/whatsapp/gateway.go +0 -156
  131. slidge/plugins/whatsapp/gateway.py +0 -69
  132. slidge/plugins/whatsapp/go.mod +0 -17
  133. slidge/plugins/whatsapp/go.sum +0 -22
  134. slidge/plugins/whatsapp/session.go +0 -371
  135. slidge/plugins/whatsapp/session.py +0 -370
  136. slidge/util/xep_0030/__init__.py +0 -13
  137. slidge/util/xep_0030/disco.py +0 -811
  138. slidge/util/xep_0030/stanza/__init__.py +0 -7
  139. slidge/util/xep_0030/stanza/info.py +0 -270
  140. slidge/util/xep_0030/stanza/items.py +0 -147
  141. slidge/util/xep_0030/static.py +0 -467
  142. slidge/util/xep_0050/adhoc.py +0 -631
  143. slidge/util/xep_0050/stanza.py +0 -180
  144. slidge/util/xep_0077/stanza.py +0 -71
  145. slidge/util/xep_0292/__init__.py +0 -1
  146. slidge/util/xep_0292/stanza.py +0 -167
  147. slidge/util/xep_0292/vcard4.py +0 -74
  148. slidge/util/xep_0356/__init__.py +0 -7
  149. slidge/util/xep_0356/permissions.py +0 -35
  150. slidge/util/xep_0356/privilege.py +0 -160
  151. slidge/util/xep_0356/stanza.py +0 -44
  152. slidge/util/xep_0461/__init__.py +0 -6
  153. slidge/util/xep_0461/reply.py +0 -48
  154. slidge/util/xep_0461/stanza.py +0 -80
  155. slidge-0.1.0rc1.dist-info/METADATA +0 -171
  156. slidge-0.1.0rc1.dist-info/RECORD +0 -99
  157. /slidge/{plugins/__init__.py → py.typed} +0 -0
  158. /slidge/{util → slixfix}/xep_0077/__init__.py +0 -0
  159. /slidge/{util → slixfix}/xep_0100/__init__.py +0 -0
  160. /slidge/{util → slixfix}/xep_0100/stanza.py +0 -0
  161. /slidge/{util → slixfix}/xep_0356_old/__init__.py +0 -0
  162. /slidge/{util → slixfix}/xep_0356_old/stanza.py +0 -0
  163. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/LICENSE +0 -0
  164. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/entry_points.txt +0 -0
slidge/core/cache.py CHANGED
@@ -1,8 +1,10 @@
1
+ import asyncio
1
2
  import hashlib
2
3
  import io
3
4
  import logging
4
5
  import shelve
5
6
  import uuid
7
+ from concurrent.futures import ThreadPoolExecutor
6
8
  from dataclasses import dataclass
7
9
  from http import HTTPStatus
8
10
  from pathlib import Path
@@ -11,7 +13,9 @@ from typing import Optional
11
13
  import aiohttp
12
14
  from multidict import CIMultiDictProxy
13
15
  from PIL import Image
16
+ from slixmpp import JID
14
17
 
18
+ from ..util.types import URL, LegacyFileIdType
15
19
  from . import config
16
20
 
17
21
 
@@ -27,56 +31,126 @@ class CachedAvatar:
27
31
 
28
32
  @property
29
33
  def data(self):
30
- return (self.root / self.filename).read_bytes()
34
+ return self.path.read_bytes()
35
+
36
+ @property
37
+ def path(self):
38
+ return self.root / self.filename
31
39
 
32
40
 
33
41
  class AvatarCache:
34
- _shelf: shelve.Shelf[CachedAvatar]
35
- _dir: Path
42
+ _shelf_path: str
43
+ _jid_to_legacy_path: str
44
+ dir: Path
45
+ http: aiohttp.ClientSession
46
+
47
+ def __init__(self):
48
+ self._thread_pool = ThreadPoolExecutor(config.AVATAR_RESAMPLING_THREADS)
36
49
 
37
50
  def set_dir(self, path: Path):
38
- self._dir = path
39
- self._dir.mkdir(exist_ok=True)
40
- self._shelf = shelve.open(str(path / "slidge_url_avatars.shelf")) # type: ignore
51
+ self.dir = path
52
+ self.dir.mkdir(exist_ok=True)
53
+ self._shelf_path = str(path / "slidge_avatar_cache.shelf")
54
+ self._jid_to_legacy_path = str(path / "jid_to_avatar_unique_id.shelf")
41
55
 
42
56
  def close(self):
43
- self._shelf.sync()
44
- self._shelf.close()
57
+ self._thread_pool.shutdown(cancel_futures=True)
45
58
 
46
- async def get_avatar(self, url: str):
47
- cached = self._shelf.get(url)
59
+ def __get_http_headers(self, cached: Optional[CachedAvatar]):
48
60
  headers = {}
49
- if cached and (self._dir / cached.filename).exists():
61
+ if cached and (self.dir / cached.filename).exists():
50
62
  if last_modified := cached.last_modified:
51
63
  headers["If-Modified-Since"] = last_modified
52
64
  if etag := cached.etag:
53
65
  headers["If-None-Match"] = etag
54
- log.debug("Request headers: %s", headers)
55
-
56
- async with aiohttp.ClientSession() as session:
57
- async with session.get(url, headers=headers) as response:
58
- log.debug("Response headers: %s", response.headers)
59
- if response.status == HTTPStatus.NOT_MODIFIED:
60
- log.debug("Using avatar cache")
61
- return cached
62
- log.debug("Download avatar")
63
- return await self._convert_and_store(
64
- Image.open(io.BytesIO(await response.read())), url, response.headers
65
- )
66
-
67
- async def _convert_and_store(
68
- self, img: Image.Image, url: str, response_headers: CIMultiDictProxy[str]
66
+ return headers
67
+
68
+ async def get_avatar_from_url_alone(self, url: str, jid: JID):
69
+ """
70
+ Used when no avatar unique ID is passed. Store and use http headers
71
+ to avoid fetching ut
72
+ """
73
+ cached = self.get(url)
74
+ headers = self.__get_http_headers(cached)
75
+ async with _download_lock:
76
+ return await self.__download(cached, url, headers, jid)
77
+
78
+ async def __download(
79
+ self,
80
+ cached: Optional[CachedAvatar],
81
+ url: str,
82
+ headers: dict[str, str],
83
+ jid: JID,
69
84
  ):
70
- if (size := config.AVATAR_SIZE) and any(x > size for x in img.size):
71
- img.thumbnail((size, size))
85
+ async with self.http.get(url, headers=headers) as response:
86
+ if response.status == HTTPStatus.NOT_MODIFIED:
87
+ log.debug("Using avatar cache for %s", jid)
88
+ return cached
89
+ log.debug("Download avatar for %s", jid)
90
+ return await self.convert_and_store(
91
+ Image.open(io.BytesIO(await response.read())),
92
+ url,
93
+ jid,
94
+ response.headers,
95
+ )
96
+
97
+ async def url_has_changed(self, url: URL):
98
+ with shelve.open(self._shelf_path) as s:
99
+ cached = s.get(url)
100
+ if cached is None:
101
+ return True
102
+ headers = self.__get_http_headers(cached)
103
+ async with self.http.head(url, headers=headers) as response:
104
+ return response.status != HTTPStatus.NOT_MODIFIED
105
+
106
+ def get(self, unique_id: LegacyFileIdType) -> Optional[CachedAvatar]:
107
+ with shelve.open(self._shelf_path) as s:
108
+ return s.get(str(unique_id))
109
+
110
+ def get_cached_id_for(self, jid: JID) -> Optional[LegacyFileIdType]:
111
+ with shelve.open(self._jid_to_legacy_path) as s:
112
+ return s.get(str(jid))
113
+
114
+ def store_jid(self, jid: JID, uid: LegacyFileIdType):
115
+ with shelve.open(self._jid_to_legacy_path) as s:
116
+ s[str(jid)] = uid
117
+
118
+ def delete_jid(self, jid: JID):
119
+ try:
120
+ with shelve.open(self._jid_to_legacy_path) as s:
121
+ del s[str(jid)]
122
+ except KeyError:
123
+ pass
124
+
125
+ async def convert_and_store(
126
+ self,
127
+ img: Image.Image,
128
+ unique_id: LegacyFileIdType,
129
+ jid: JID,
130
+ response_headers: Optional[CIMultiDictProxy[str]] = None,
131
+ ) -> CachedAvatar:
132
+ resize = (size := config.AVATAR_SIZE) and any(x > size for x in img.size)
133
+ if resize:
134
+ await asyncio.get_event_loop().run_in_executor(
135
+ self._thread_pool, img.thumbnail, (size, size)
136
+ )
72
137
  log.debug("Resampled image to %s", img.size)
73
138
 
74
- filename = str(uuid.uuid1())
75
- file_path = self._dir / filename
76
-
77
- with io.BytesIO() as f:
78
- img.save(f, format="PNG")
79
- img_bytes = f.getvalue()
139
+ filename = str(uuid.uuid1()) + ".png"
140
+ file_path = self.dir / filename
141
+
142
+ if (
143
+ not resize
144
+ and img.format == "PNG"
145
+ and isinstance(unique_id, str)
146
+ and (path := Path(unique_id))
147
+ and path.exists()
148
+ ):
149
+ img_bytes = path.read_bytes()
150
+ else:
151
+ with io.BytesIO() as f:
152
+ img.save(f, format="PNG")
153
+ img_bytes = f.getvalue()
80
154
 
81
155
  with file_path.open("wb") as file:
82
156
  file.write(img_bytes)
@@ -88,14 +162,22 @@ class AvatarCache:
88
162
  hash=hash_,
89
163
  height=img.height,
90
164
  width=img.width,
91
- etag=response_headers.get("etag"),
92
- last_modified=response_headers.get("last-modified"),
93
- root=self._dir,
165
+ root=self.dir,
94
166
  )
95
- self._shelf[url] = avatar
96
- self._shelf.sync()
167
+ if response_headers:
168
+ avatar.etag = response_headers.get("etag")
169
+ avatar.last_modified = response_headers.get("last-modified")
170
+ with shelve.open(self._shelf_path) as s:
171
+ s[str(unique_id)] = avatar
172
+ self.store_jid(jid, unique_id)
97
173
  return avatar
98
174
 
99
175
 
100
176
  avatar_cache = AvatarCache()
101
177
  log = logging.getLogger(__name__)
178
+ _download_lock = asyncio.Lock()
179
+
180
+ __all__ = (
181
+ "CachedAvatar",
182
+ "avatar_cache",
183
+ )
slidge/core/config.py CHANGED
@@ -18,14 +18,18 @@ LEGACY_MODULE__DOC = (
18
18
  "a BaseGateway and a LegacySession subclass"
19
19
  )
20
20
 
21
- SERVER: str
22
- SERVER__DOC = "The XMPP server's host name."
21
+ SERVER: str = "localhost"
22
+ SERVER__DOC = (
23
+ "The XMPP server's host name. Defaults to localhost, which is the "
24
+ "standard way of running slidge, on the same host as the XMPP server. "
25
+ "The 'Jabber Component Protocol' (XEP-0114) does not mention encryption, "
26
+ "so you *should* provide encryption another way, eg via port forwarding, if "
27
+ "you change this."
28
+ )
23
29
  SERVER__SHORT = "s"
24
30
 
25
31
  SECRET: str
26
- SECRET__DOC = (
27
- "The gateway component's secret (required to connect" " to the XMPP server)"
28
- )
32
+ SECRET__DOC = "The gateway component's secret (required to connect to the XMPP server)"
29
33
 
30
34
  JID: JIDType
31
35
  JID__DOC = "The gateway component's JID"
@@ -46,9 +50,9 @@ HOME_DIR__DYNAMIC_DEFAULT = True
46
50
 
47
51
  USER_JID_VALIDATOR: str
48
52
  USER_JID_VALIDATOR__DOC = (
49
- "Regular expression to restrict user that can register to the gateway by JID. "
50
- "Defaults to .*@${SLIDGE_SERVER}, forbids the gateway to JIDs "
51
- "not using the same XMPP server as the gateway"
53
+ "Regular expression to restrict users that can register to the gateway, by JID. "
54
+ "Defaults to .*@${SLIDGE_SERVER}, but since SLIDGE_SERVER is usually localhost, "
55
+ "you probably want to change that to .*@example.com"
52
56
  )
53
57
  USER_JID_VALIDATOR__DYNAMIC_DEFAULT = True
54
58
 
@@ -83,11 +87,42 @@ AVATAR_SIZE__DOC = (
83
87
  "Maximum image size (width and height), image ratio will be preserved"
84
88
  )
85
89
 
90
+ USE_ATTACHMENT_ORIGINAL_URLS = False
91
+ USE_ATTACHMENT_ORIGINAL_URLS__DOC = (
92
+ "For legacy plugins in which attachments are publicly downloadable URLs, "
93
+ "let XMPP clients directly download them from this URL. Note that this will "
94
+ "probably leak your client IP to the legacy network."
95
+ )
96
+
86
97
  UPLOAD_REQUESTER: Optional[str] = None
87
98
  UPLOAD_REQUESTER__DOC = (
88
99
  "Set which JID should request the upload slots. Defaults to the component JID."
89
100
  )
90
101
 
102
+ NO_UPLOAD_PATH: Optional[str] = None
103
+ NO_UPLOAD_PATH__DOC = (
104
+ "Instead of using the XMPP server's HTTP upload component, copy files to this dir. "
105
+ "You need to set NO_UPLOAD_URL_PREFIX too if you use this option, and configure "
106
+ "an web server to serve files in this dir."
107
+ )
108
+
109
+ NO_UPLOAD_URL_PREFIX: Optional[str] = None
110
+ NO_UPLOAD_URL_PREFIX__DOC = (
111
+ "Base URL that servers files in the dir set in the no-upload-path option, "
112
+ "eg https://example.com:666/slidge-attachments/"
113
+ )
114
+
115
+ NO_UPLOAD_METHOD: str = "copy"
116
+ NO_UPLOAD_METHOD__DOC = (
117
+ "Whether to 'copy', 'move', 'hardlink' or 'symlink' the files in no-upload-path."
118
+ )
119
+
120
+ NO_UPLOAD_FILE_READ_OTHERS = False
121
+ NO_UPLOAD_FILE_READ_OTHERS__DOC = (
122
+ "After writing a file in NO_UPLOAD_PATH, change its permission so that 'others' can"
123
+ " read it."
124
+ )
125
+
91
126
  IGNORE_DELAY_THRESHOLD = _TimedeltaSeconds("300")
92
127
  IGNORE_DELAY_THRESHOLD__DOC = (
93
128
  "Threshold, in seconds, below which the <delay> information is stripped "
@@ -102,10 +137,80 @@ PARTIAL_REGISTRATION_TIMEOUT__DOC = (
102
137
 
103
138
  LAST_SEEN_FALLBACK = True
104
139
  LAST_SEEN_FALLBACK__DOC = (
105
- "When using XEP-0319 (Last User Interaction in Presence), use the presence status "
106
- "to display the last seen information in the presence status. Useful for clients that "
107
- "do not implement XEP-0319."
140
+ "When using XEP-0319 (Last User Interaction in Presence), use the presence status"
141
+ " to display the last seen information in the presence status. Useful for clients"
142
+ " that do not implement XEP-0319."
108
143
  )
109
144
 
110
145
  QR_TIMEOUT = 60
111
146
  QR_TIMEOUT__DOC = "Timeout for QR code flashing confirmation."
147
+
148
+ DOWNLOAD_CHUNK_SIZE = 1024
149
+ DOWNLOAD_CHUNK_SIZE__DOC = "Chunk size when slidge needs to download files using HTTP."
150
+
151
+ LAST_MESSAGE_CORRECTION_RETRACTION_WORKAROUND = False
152
+ LAST_MESSAGE_CORRECTION_RETRACTION_WORKAROUND__DOC = (
153
+ "If the legacy service does not support last message correction but supports"
154
+ " message retractions, slidge can 'retract' the edited message when you edit from"
155
+ " an XMPP client, as a workaround. This may only work for editing messages"
156
+ " **once**. If the legacy service does not support retractions and this is set to"
157
+ " true, when XMPP clients attempt to correct, this will send a new message."
158
+ )
159
+
160
+ FIX_FILENAME_SUFFIX_MIME_TYPE = False
161
+ FIX_FILENAME_SUFFIX_MIME_TYPE__DOC = (
162
+ "Fix the Filename suffix based on the Mime Type of the file. Some clients (eg"
163
+ " Conversations) may not inline files that have a wrong suffix for the MIME Type."
164
+ " Therefore the MIME Type of the file is checked, if the suffix is not valid for"
165
+ " that MIME Type, a valid one will be picked."
166
+ )
167
+
168
+ LOG_FILE: Optional[Path] = None
169
+ LOG_FILE__DOC = "Log to a file instead of stdout/err"
170
+
171
+ LOG_FORMAT: str = "%(levelname)s:%(name)s:%(message)s"
172
+ LOG_FORMAT__DOC = (
173
+ "Optionally, a format string for logging messages. Refer to "
174
+ "https://docs.python.org/3/library/logging.html#logrecord-attributes "
175
+ "for available options."
176
+ )
177
+
178
+ MAM_MAX_DAYS = 7
179
+ MAM_MAX_DAYS__DOC = (
180
+ "Maximum number of days for group archive retention. "
181
+ "Since all text content stored in RAM right now, "
182
+ )
183
+
184
+ CORRECTION_EMPTY_BODY_AS_RETRACTION = True
185
+ CORRECTION_EMPTY_BODY_AS_RETRACTION__DOC = (
186
+ "Treat last message correction to empty message as a retraction. "
187
+ "(this is what cheogram do for retraction)"
188
+ )
189
+
190
+ ATTACHMENT_MAXIMUM_FILE_NAME_LENGTH = 200
191
+ ATTACHMENT_MAXIMUM_FILE_NAME_LENGTH__DOC = (
192
+ "Some legacy network provide ridiculously long filenames, strip above this limit, "
193
+ "preserving suffix."
194
+ )
195
+
196
+ ALWAYS_INVITE_WHEN_ADDING_BOOKMARKS = True
197
+ ALWAYS_INVITE_WHEN_ADDING_BOOKMARKS__DOC = (
198
+ "Send an invitation to join MUCs when adding them to the bookmarks. While this "
199
+ "should not be necessary, it helps with clients that do not support :xep:`0402` "
200
+ "or that do not respect the auto-join flag."
201
+ )
202
+
203
+ AVATAR_RESAMPLING_THREADS = 2
204
+ AVATAR_RESAMPLING_THREADS__DOC = (
205
+ "Number of additional threads to use for avatar resampling. Even in a single-core "
206
+ "context, this makes avatar resampling non-blocking."
207
+ )
208
+
209
+ DEV_MODE = False
210
+ DEV_MODE__DOC = (
211
+ "Enables an interactive python shell via chat commands, for admins."
212
+ "Not safe to use in prod, but great during dev."
213
+ )
214
+
215
+ SYNC_AVATAR = True
216
+ SYNC_AVATAR__DOC = "Sync the user XMPP avatar to legacy network (if supported)."
@@ -0,0 +1,3 @@
1
+ from .base import BaseGateway
2
+
3
+ __all__ = ("BaseGateway",)