slidge 0.1.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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",)