slidge 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. slidge/__init__.py +61 -0
  2. slidge/__main__.py +192 -0
  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 +3 -0
  15. slidge/core/cache.py +183 -0
  16. slidge/core/config.py +209 -0
  17. slidge/core/gateway/__init__.py +3 -0
  18. slidge/core/gateway/base.py +892 -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 +757 -0
  29. slidge/core/gateway/vcard_temp.py +130 -0
  30. slidge/core/mixins/__init__.py +19 -0
  31. slidge/core/mixins/attachment.py +506 -0
  32. slidge/core/mixins/avatar.py +167 -0
  33. slidge/core/mixins/base.py +31 -0
  34. slidge/core/mixins/disco.py +130 -0
  35. slidge/core/mixins/lock.py +31 -0
  36. slidge/core/mixins/message.py +398 -0
  37. slidge/core/mixins/message_maker.py +154 -0
  38. slidge/core/mixins/presence.py +217 -0
  39. slidge/core/mixins/recipient.py +43 -0
  40. slidge/core/pubsub.py +525 -0
  41. slidge/core/session.py +752 -0
  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 +440 -0
  46. slidge/group/room.py +1095 -0
  47. slidge/migration.py +18 -0
  48. slidge/py.typed +0 -0
  49. slidge/slixfix/__init__.py +68 -0
  50. slidge/slixfix/link_preview/__init__.py +10 -0
  51. slidge/slixfix/link_preview/link_preview.py +17 -0
  52. slidge/slixfix/link_preview/stanza.py +99 -0
  53. slidge/slixfix/roster.py +60 -0
  54. slidge/slixfix/xep_0077/__init__.py +10 -0
  55. slidge/slixfix/xep_0077/register.py +289 -0
  56. slidge/slixfix/xep_0077/stanza.py +104 -0
  57. slidge/slixfix/xep_0100/__init__.py +5 -0
  58. slidge/slixfix/xep_0100/gateway.py +121 -0
  59. slidge/slixfix/xep_0100/stanza.py +9 -0
  60. slidge/slixfix/xep_0153/__init__.py +10 -0
  61. slidge/slixfix/xep_0153/stanza.py +25 -0
  62. slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
  63. slidge/slixfix/xep_0264/__init__.py +5 -0
  64. slidge/slixfix/xep_0264/stanza.py +36 -0
  65. slidge/slixfix/xep_0264/thumbnail.py +23 -0
  66. slidge/slixfix/xep_0292/__init__.py +5 -0
  67. slidge/slixfix/xep_0292/vcard4.py +100 -0
  68. slidge/slixfix/xep_0313/__init__.py +12 -0
  69. slidge/slixfix/xep_0313/mam.py +262 -0
  70. slidge/slixfix/xep_0313/stanza.py +359 -0
  71. slidge/slixfix/xep_0317/__init__.py +5 -0
  72. slidge/slixfix/xep_0317/hats.py +17 -0
  73. slidge/slixfix/xep_0317/stanza.py +28 -0
  74. slidge/slixfix/xep_0356_old/__init__.py +7 -0
  75. slidge/slixfix/xep_0356_old/privilege.py +167 -0
  76. slidge/slixfix/xep_0356_old/stanza.py +44 -0
  77. slidge/slixfix/xep_0424/__init__.py +9 -0
  78. slidge/slixfix/xep_0424/retraction.py +77 -0
  79. slidge/slixfix/xep_0424/stanza.py +28 -0
  80. slidge/slixfix/xep_0490/__init__.py +8 -0
  81. slidge/slixfix/xep_0490/mds.py +47 -0
  82. slidge/slixfix/xep_0490/stanza.py +17 -0
  83. slidge/util/__init__.py +15 -0
  84. slidge/util/archive_msg.py +61 -0
  85. slidge/util/conf.py +206 -0
  86. slidge/util/db.py +229 -0
  87. slidge/util/schema.sql +126 -0
  88. slidge/util/sql.py +508 -0
  89. slidge/util/test.py +295 -0
  90. slidge/util/types.py +180 -0
  91. slidge/util/util.py +295 -0
  92. slidge-0.1.0.dist-info/LICENSE +661 -0
  93. slidge-0.1.0.dist-info/METADATA +109 -0
  94. slidge-0.1.0.dist-info/RECORD +96 -0
  95. slidge-0.1.0.dist-info/WHEEL +4 -0
  96. slidge-0.1.0.dist-info/entry_points.txt +3 -0
slidge/core/cache.py ADDED
@@ -0,0 +1,183 @@
1
+ import asyncio
2
+ import hashlib
3
+ import io
4
+ import logging
5
+ import shelve
6
+ import uuid
7
+ from concurrent.futures import ThreadPoolExecutor
8
+ from dataclasses import dataclass
9
+ from http import HTTPStatus
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ import aiohttp
14
+ from multidict import CIMultiDictProxy
15
+ from PIL import Image
16
+ from slixmpp import JID
17
+
18
+ from ..util.types import URL, LegacyFileIdType
19
+ from . import config
20
+
21
+
22
+ @dataclass
23
+ class CachedAvatar:
24
+ filename: str
25
+ hash: str
26
+ height: int
27
+ width: int
28
+ root: Path
29
+ etag: Optional[str] = None
30
+ last_modified: Optional[str] = None
31
+
32
+ @property
33
+ def data(self):
34
+ return self.path.read_bytes()
35
+
36
+ @property
37
+ def path(self):
38
+ return self.root / self.filename
39
+
40
+
41
+ class AvatarCache:
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)
49
+
50
+ def set_dir(self, path: Path):
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")
55
+
56
+ def close(self):
57
+ self._thread_pool.shutdown(cancel_futures=True)
58
+
59
+ def __get_http_headers(self, cached: Optional[CachedAvatar]):
60
+ headers = {}
61
+ if cached and (self.dir / cached.filename).exists():
62
+ if last_modified := cached.last_modified:
63
+ headers["If-Modified-Since"] = last_modified
64
+ if etag := cached.etag:
65
+ headers["If-None-Match"] = etag
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,
84
+ ):
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
+ )
137
+ log.debug("Resampled image to %s", img.size)
138
+
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()
154
+
155
+ with file_path.open("wb") as file:
156
+ file.write(img_bytes)
157
+
158
+ hash_ = hashlib.sha1(img_bytes).hexdigest()
159
+
160
+ avatar = CachedAvatar(
161
+ filename=filename,
162
+ hash=hash_,
163
+ height=img.height,
164
+ width=img.width,
165
+ root=self.dir,
166
+ )
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)
173
+ return avatar
174
+
175
+
176
+ avatar_cache = AvatarCache()
177
+ log = logging.getLogger(__name__)
178
+ _download_lock = asyncio.Lock()
179
+
180
+ __all__ = (
181
+ "CachedAvatar",
182
+ "avatar_cache",
183
+ )
slidge/core/config.py ADDED
@@ -0,0 +1,209 @@
1
+ from datetime import timedelta
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ from slixmpp import JID as JIDType
6
+
7
+
8
+ class _TimedeltaSeconds(timedelta):
9
+ def __new__(cls, s: str):
10
+ return super().__new__(cls, seconds=int(s))
11
+
12
+
13
+ # REQUIRED, so not default value
14
+
15
+ LEGACY_MODULE: str
16
+ LEGACY_MODULE__DOC = (
17
+ "Importable python module containing (at least) "
18
+ "a BaseGateway and a LegacySession subclass"
19
+ )
20
+
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
+ )
29
+ SERVER__SHORT = "s"
30
+
31
+ SECRET: str
32
+ SECRET__DOC = "The gateway component's secret (required to connect to the XMPP server)"
33
+
34
+ JID: JIDType
35
+ JID__DOC = "The gateway component's JID"
36
+ JID__SHORT = "j"
37
+
38
+ PORT: str = "5347"
39
+ PORT__DOC = "The XMPP server's port for incoming component connections"
40
+ PORT__SHORT = "p"
41
+
42
+ # Dynamic default (depends on other values)
43
+
44
+ HOME_DIR: Path
45
+ HOME_DIR__DOC = (
46
+ "Shelve file used to store persistent user data. "
47
+ "Defaults to /var/lib/slidge/${SLIDGE_JID}. "
48
+ )
49
+ HOME_DIR__DYNAMIC_DEFAULT = True
50
+
51
+ USER_JID_VALIDATOR: str
52
+ USER_JID_VALIDATOR__DOC = (
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"
56
+ )
57
+ USER_JID_VALIDATOR__DYNAMIC_DEFAULT = True
58
+
59
+ # Optional, so default value + type hint if default is None
60
+
61
+ ADMINS: tuple[JIDType, ...] = ()
62
+ ADMINS__DOC = "JIDs of the gateway admins"
63
+
64
+
65
+ UPLOAD_SERVICE: Optional[str] = None
66
+ UPLOAD_SERVICE__DOC = (
67
+ "JID of an HTTP upload service the gateway can use. "
68
+ "This is optional, as it should be automatically determined via service"
69
+ "discovery."
70
+ )
71
+
72
+ SECRET_KEY: Optional[str] = None
73
+ SECRET_KEY__DOC = "Encryption for disk storage"
74
+
75
+ NO_ROSTER_PUSH = False
76
+ NO_ROSTER_PUSH__DOC = "Do not fill users' rosters with legacy contacts automatically"
77
+
78
+ ROSTER_PUSH_PRESENCE_SUBSCRIPTION_REQUEST_FALLBACK = True
79
+ ROSTER_PUSH_PRESENCE_SUBSCRIPTION_REQUEST_FALLBACK__DOC = (
80
+ "If True, legacy contacts will send a presence request subscription "
81
+ "when privileged roster push does not work, eg, if XEP-0356 (privileged "
82
+ "entity) is not available for the component."
83
+ )
84
+
85
+ AVATAR_SIZE = 200
86
+ AVATAR_SIZE__DOC = (
87
+ "Maximum image size (width and height), image ratio will be preserved"
88
+ )
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
+
97
+ UPLOAD_REQUESTER: Optional[str] = None
98
+ UPLOAD_REQUESTER__DOC = (
99
+ "Set which JID should request the upload slots. Defaults to the component JID."
100
+ )
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
+
126
+ IGNORE_DELAY_THRESHOLD = _TimedeltaSeconds("300")
127
+ IGNORE_DELAY_THRESHOLD__DOC = (
128
+ "Threshold, in seconds, below which the <delay> information is stripped "
129
+ "out of emitted stanzas."
130
+ )
131
+
132
+ PARTIAL_REGISTRATION_TIMEOUT = 3600
133
+ PARTIAL_REGISTRATION_TIMEOUT__DOC = (
134
+ "Timeout before registration and login. Only useful for legacy networks where "
135
+ "a single step registration process is not enough."
136
+ )
137
+
138
+ LAST_SEEN_FALLBACK = True
139
+ LAST_SEEN_FALLBACK__DOC = (
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."
143
+ )
144
+
145
+ QR_TIMEOUT = 60
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
+ MAM_MAX_DAYS = 7
172
+ MAM_MAX_DAYS__DOC = (
173
+ "Maximum number of days for group archive retention. "
174
+ "Since all text content stored in RAM right now, "
175
+ )
176
+
177
+ CORRECTION_EMPTY_BODY_AS_RETRACTION = True
178
+ CORRECTION_EMPTY_BODY_AS_RETRACTION__DOC = (
179
+ "Treat last message correction to empty message as a retraction. "
180
+ "(this is what cheogram do for retraction)"
181
+ )
182
+
183
+ ATTACHMENT_MAXIMUM_FILE_NAME_LENGTH = 200
184
+ ATTACHMENT_MAXIMUM_FILE_NAME_LENGTH__DOC = (
185
+ "Some legacy network provide ridiculously long filenames, strip above this limit, "
186
+ "preserving suffix."
187
+ )
188
+
189
+ ALWAYS_INVITE_WHEN_ADDING_BOOKMARKS = True
190
+ ALWAYS_INVITE_WHEN_ADDING_BOOKMARKS__DOC = (
191
+ "Send an invitation to join MUCs when adding them to the bookmarks. While this "
192
+ "should not be necessary, it helps with clients that do not support :xep:`0402` "
193
+ "or that do not respect the auto-join flag."
194
+ )
195
+
196
+ AVATAR_RESAMPLING_THREADS = 2
197
+ AVATAR_RESAMPLING_THREADS__DOC = (
198
+ "Number of additional threads to use for avatar resampling. Even in a single-core "
199
+ "context, this makes avatar resampling non-blocking."
200
+ )
201
+
202
+ DEV_MODE = False
203
+ DEV_MODE__DOC = (
204
+ "Enables an interactive python shell via chat commands, for admins."
205
+ "Not safe to use in prod, but great during dev."
206
+ )
207
+
208
+ SYNC_AVATAR = True
209
+ 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",)