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.
- slidge/__init__.py +61 -0
- slidge/__main__.py +192 -0
- slidge/command/__init__.py +28 -0
- slidge/command/adhoc.py +258 -0
- slidge/command/admin.py +193 -0
- slidge/command/base.py +441 -0
- slidge/command/categories.py +3 -0
- slidge/command/chat_command.py +288 -0
- slidge/command/register.py +179 -0
- slidge/command/user.py +250 -0
- slidge/contact/__init__.py +8 -0
- slidge/contact/contact.py +452 -0
- slidge/contact/roster.py +192 -0
- slidge/core/__init__.py +3 -0
- slidge/core/cache.py +183 -0
- slidge/core/config.py +209 -0
- slidge/core/gateway/__init__.py +3 -0
- slidge/core/gateway/base.py +892 -0
- slidge/core/gateway/caps.py +63 -0
- slidge/core/gateway/delivery_receipt.py +52 -0
- slidge/core/gateway/disco.py +80 -0
- slidge/core/gateway/mam.py +75 -0
- slidge/core/gateway/muc_admin.py +35 -0
- slidge/core/gateway/ping.py +66 -0
- slidge/core/gateway/presence.py +95 -0
- slidge/core/gateway/registration.py +53 -0
- slidge/core/gateway/search.py +102 -0
- slidge/core/gateway/session_dispatcher.py +757 -0
- slidge/core/gateway/vcard_temp.py +130 -0
- slidge/core/mixins/__init__.py +19 -0
- slidge/core/mixins/attachment.py +506 -0
- slidge/core/mixins/avatar.py +167 -0
- slidge/core/mixins/base.py +31 -0
- slidge/core/mixins/disco.py +130 -0
- slidge/core/mixins/lock.py +31 -0
- slidge/core/mixins/message.py +398 -0
- slidge/core/mixins/message_maker.py +154 -0
- slidge/core/mixins/presence.py +217 -0
- slidge/core/mixins/recipient.py +43 -0
- slidge/core/pubsub.py +525 -0
- slidge/core/session.py +752 -0
- slidge/group/__init__.py +10 -0
- slidge/group/archive.py +125 -0
- slidge/group/bookmarks.py +163 -0
- slidge/group/participant.py +440 -0
- slidge/group/room.py +1095 -0
- slidge/migration.py +18 -0
- slidge/py.typed +0 -0
- slidge/slixfix/__init__.py +68 -0
- slidge/slixfix/link_preview/__init__.py +10 -0
- slidge/slixfix/link_preview/link_preview.py +17 -0
- slidge/slixfix/link_preview/stanza.py +99 -0
- slidge/slixfix/roster.py +60 -0
- slidge/slixfix/xep_0077/__init__.py +10 -0
- slidge/slixfix/xep_0077/register.py +289 -0
- slidge/slixfix/xep_0077/stanza.py +104 -0
- slidge/slixfix/xep_0100/__init__.py +5 -0
- slidge/slixfix/xep_0100/gateway.py +121 -0
- slidge/slixfix/xep_0100/stanza.py +9 -0
- slidge/slixfix/xep_0153/__init__.py +10 -0
- slidge/slixfix/xep_0153/stanza.py +25 -0
- slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
- slidge/slixfix/xep_0264/__init__.py +5 -0
- slidge/slixfix/xep_0264/stanza.py +36 -0
- slidge/slixfix/xep_0264/thumbnail.py +23 -0
- slidge/slixfix/xep_0292/__init__.py +5 -0
- slidge/slixfix/xep_0292/vcard4.py +100 -0
- slidge/slixfix/xep_0313/__init__.py +12 -0
- slidge/slixfix/xep_0313/mam.py +262 -0
- slidge/slixfix/xep_0313/stanza.py +359 -0
- slidge/slixfix/xep_0317/__init__.py +5 -0
- slidge/slixfix/xep_0317/hats.py +17 -0
- slidge/slixfix/xep_0317/stanza.py +28 -0
- slidge/slixfix/xep_0356_old/__init__.py +7 -0
- slidge/slixfix/xep_0356_old/privilege.py +167 -0
- slidge/slixfix/xep_0356_old/stanza.py +44 -0
- slidge/slixfix/xep_0424/__init__.py +9 -0
- slidge/slixfix/xep_0424/retraction.py +77 -0
- slidge/slixfix/xep_0424/stanza.py +28 -0
- slidge/slixfix/xep_0490/__init__.py +8 -0
- slidge/slixfix/xep_0490/mds.py +47 -0
- slidge/slixfix/xep_0490/stanza.py +17 -0
- slidge/util/__init__.py +15 -0
- slidge/util/archive_msg.py +61 -0
- slidge/util/conf.py +206 -0
- slidge/util/db.py +229 -0
- slidge/util/schema.sql +126 -0
- slidge/util/sql.py +508 -0
- slidge/util/test.py +295 -0
- slidge/util/types.py +180 -0
- slidge/util/util.py +295 -0
- slidge-0.1.0.dist-info/LICENSE +661 -0
- slidge-0.1.0.dist-info/METADATA +109 -0
- slidge-0.1.0.dist-info/RECORD +96 -0
- slidge-0.1.0.dist-info/WHEEL +4 -0
- slidge-0.1.0.dist-info/entry_points.txt +3 -0
slidge/util/util.py
ADDED
@@ -0,0 +1,295 @@
|
|
1
|
+
import logging
|
2
|
+
import mimetypes
|
3
|
+
import re
|
4
|
+
import subprocess
|
5
|
+
import warnings
|
6
|
+
from abc import ABCMeta
|
7
|
+
from pathlib import Path
|
8
|
+
from typing import TYPE_CHECKING, Callable, NamedTuple, Optional, Type
|
9
|
+
|
10
|
+
from .types import Mention, ResourceDict
|
11
|
+
|
12
|
+
if TYPE_CHECKING:
|
13
|
+
from ..contact.contact import LegacyContact
|
14
|
+
|
15
|
+
try:
|
16
|
+
import magic
|
17
|
+
except ImportError as e:
|
18
|
+
magic = None # type:ignore
|
19
|
+
logging.warning(
|
20
|
+
(
|
21
|
+
"Libmagic is not available: %s. "
|
22
|
+
"It's OK if you don't use fix-filename-suffix-mime-type."
|
23
|
+
),
|
24
|
+
e,
|
25
|
+
)
|
26
|
+
|
27
|
+
|
28
|
+
def fix_suffix(path: Path, mime_type: Optional[str], file_name: Optional[str]):
|
29
|
+
guessed = magic.from_file(path, mime=True)
|
30
|
+
if guessed == mime_type:
|
31
|
+
log.debug("Magic and given MIME match")
|
32
|
+
else:
|
33
|
+
log.debug("Magic (%s) and given MIME (%s) differ", guessed, mime_type)
|
34
|
+
mime_type = guessed
|
35
|
+
|
36
|
+
valid_suffix_list = mimetypes.guess_all_extensions(mime_type, strict=False)
|
37
|
+
|
38
|
+
if file_name:
|
39
|
+
name = Path(file_name)
|
40
|
+
else:
|
41
|
+
name = Path(path.name)
|
42
|
+
|
43
|
+
suffix = name.suffix
|
44
|
+
|
45
|
+
if suffix in valid_suffix_list:
|
46
|
+
log.debug("Suffix %s is in %s", suffix, valid_suffix_list)
|
47
|
+
return name
|
48
|
+
|
49
|
+
valid_suffix = mimetypes.guess_extension(mime_type.split(";")[0], strict=False)
|
50
|
+
if valid_suffix is None:
|
51
|
+
log.debug("No valid suffix found")
|
52
|
+
return name
|
53
|
+
|
54
|
+
log.debug("Changing suffix of %s to %s", file_name or path.name, valid_suffix)
|
55
|
+
return name.with_suffix(valid_suffix)
|
56
|
+
|
57
|
+
|
58
|
+
class SubclassableOnce(type):
|
59
|
+
TEST_MODE = False # To allow importing everything, including plugins, during tests
|
60
|
+
|
61
|
+
def __init__(cls, name, bases, dct):
|
62
|
+
for b in bases:
|
63
|
+
if type(b) in (SubclassableOnce, ABCSubclassableOnceAtMost):
|
64
|
+
if hasattr(b, "_subclass") and not cls.TEST_MODE:
|
65
|
+
raise RuntimeError(
|
66
|
+
"This class must be subclassed once at most!",
|
67
|
+
cls,
|
68
|
+
name,
|
69
|
+
bases,
|
70
|
+
dct,
|
71
|
+
)
|
72
|
+
else:
|
73
|
+
log.debug("Setting %s as subclass for %s", cls, b)
|
74
|
+
b._subclass = cls
|
75
|
+
|
76
|
+
super().__init__(name, bases, dct)
|
77
|
+
|
78
|
+
def get_self_or_unique_subclass(cls):
|
79
|
+
try:
|
80
|
+
return cls.get_unique_subclass()
|
81
|
+
except AttributeError:
|
82
|
+
return cls
|
83
|
+
|
84
|
+
def get_unique_subclass(cls):
|
85
|
+
r = getattr(cls, "_subclass", None)
|
86
|
+
if r is None:
|
87
|
+
raise AttributeError("Could not find any subclass", cls)
|
88
|
+
return r
|
89
|
+
|
90
|
+
def reset_subclass(cls):
|
91
|
+
try:
|
92
|
+
log.debug("Resetting subclass of %s", cls)
|
93
|
+
delattr(cls, "_subclass")
|
94
|
+
except AttributeError:
|
95
|
+
log.debug("No subclass were registered for %s", cls)
|
96
|
+
|
97
|
+
|
98
|
+
class ABCSubclassableOnceAtMost(ABCMeta, SubclassableOnce):
|
99
|
+
pass
|
100
|
+
|
101
|
+
|
102
|
+
def is_valid_phone_number(phone: Optional[str]):
|
103
|
+
if phone is None:
|
104
|
+
return False
|
105
|
+
match = re.match(r"\+\d.*", phone)
|
106
|
+
if match is None:
|
107
|
+
return False
|
108
|
+
return match[0] == phone
|
109
|
+
|
110
|
+
|
111
|
+
def strip_illegal_chars(s: str):
|
112
|
+
return ILLEGAL_XML_CHARS_RE.sub("", s)
|
113
|
+
|
114
|
+
|
115
|
+
# from https://stackoverflow.com/a/64570125/5902284 and Link Mauve
|
116
|
+
ILLEGAL = [
|
117
|
+
(0x00, 0x08),
|
118
|
+
(0x0B, 0x0C),
|
119
|
+
(0x0E, 0x1F),
|
120
|
+
(0x7F, 0x84),
|
121
|
+
(0x86, 0x9F),
|
122
|
+
(0xFDD0, 0xFDDF),
|
123
|
+
(0xFFFE, 0xFFFF),
|
124
|
+
(0x1FFFE, 0x1FFFF),
|
125
|
+
(0x2FFFE, 0x2FFFF),
|
126
|
+
(0x3FFFE, 0x3FFFF),
|
127
|
+
(0x4FFFE, 0x4FFFF),
|
128
|
+
(0x5FFFE, 0x5FFFF),
|
129
|
+
(0x6FFFE, 0x6FFFF),
|
130
|
+
(0x7FFFE, 0x7FFFF),
|
131
|
+
(0x8FFFE, 0x8FFFF),
|
132
|
+
(0x9FFFE, 0x9FFFF),
|
133
|
+
(0xAFFFE, 0xAFFFF),
|
134
|
+
(0xBFFFE, 0xBFFFF),
|
135
|
+
(0xCFFFE, 0xCFFFF),
|
136
|
+
(0xDFFFE, 0xDFFFF),
|
137
|
+
(0xEFFFE, 0xEFFFF),
|
138
|
+
(0xFFFFE, 0xFFFFF),
|
139
|
+
(0x10FFFE, 0x10FFFF),
|
140
|
+
]
|
141
|
+
|
142
|
+
ILLEGAL_RANGES = [rf"{chr(low)}-{chr(high)}" for (low, high) in ILLEGAL]
|
143
|
+
XML_ILLEGAL_CHARACTER_REGEX = "[" + "".join(ILLEGAL_RANGES) + "]"
|
144
|
+
ILLEGAL_XML_CHARS_RE = re.compile(XML_ILLEGAL_CHARACTER_REGEX)
|
145
|
+
|
146
|
+
|
147
|
+
# from https://stackoverflow.com/a/35804945/5902284
|
148
|
+
def addLoggingLevel(
|
149
|
+
levelName: str = "TRACE", levelNum: int = logging.DEBUG - 5, methodName=None
|
150
|
+
):
|
151
|
+
"""
|
152
|
+
Comprehensively adds a new logging level to the `logging` module and the
|
153
|
+
currently configured logging class.
|
154
|
+
|
155
|
+
`levelName` becomes an attribute of the `logging` module with the value
|
156
|
+
`levelNum`. `methodName` becomes a convenience method for both `logging`
|
157
|
+
itself and the class returned by `logging.getLoggerClass()` (usually just
|
158
|
+
`logging.Logger`). If `methodName` is not specified, `levelName.lower()` is
|
159
|
+
used.
|
160
|
+
|
161
|
+
To avoid accidental clobberings of existing attributes, this method will
|
162
|
+
raise an `AttributeError` if the level name is already an attribute of the
|
163
|
+
`logging` module or if the method name is already present
|
164
|
+
|
165
|
+
Example
|
166
|
+
-------
|
167
|
+
>>> addLoggingLevel('TRACE', logging.DEBUG - 5)
|
168
|
+
>>> logging.getLogger(__name__).setLevel("TRACE")
|
169
|
+
>>> logging.getLogger(__name__).trace('that worked')
|
170
|
+
>>> logging.trace('so did this')
|
171
|
+
>>> logging.TRACE
|
172
|
+
5
|
173
|
+
|
174
|
+
"""
|
175
|
+
if not methodName:
|
176
|
+
methodName = levelName.lower()
|
177
|
+
|
178
|
+
if hasattr(logging, levelName):
|
179
|
+
log.debug("{} already defined in logging module".format(levelName))
|
180
|
+
return
|
181
|
+
if hasattr(logging, methodName):
|
182
|
+
log.debug("{} already defined in logging module".format(methodName))
|
183
|
+
return
|
184
|
+
if hasattr(logging.getLoggerClass(), methodName):
|
185
|
+
log.debug("{} already defined in logger class".format(methodName))
|
186
|
+
return
|
187
|
+
|
188
|
+
# This method was inspired by the answers to Stack Overflow post
|
189
|
+
# http://stackoverflow.com/q/2183233/2988730, especially
|
190
|
+
# http://stackoverflow.com/a/13638084/2988730
|
191
|
+
def logForLevel(self, message, *args, **kwargs):
|
192
|
+
if self.isEnabledFor(levelNum):
|
193
|
+
self._log(levelNum, message, args, **kwargs)
|
194
|
+
|
195
|
+
def logToRoot(message, *args, **kwargs):
|
196
|
+
logging.log(levelNum, message, *args, **kwargs)
|
197
|
+
|
198
|
+
logging.addLevelName(levelNum, levelName)
|
199
|
+
setattr(logging, levelName, levelNum)
|
200
|
+
setattr(logging.getLoggerClass(), methodName, logForLevel)
|
201
|
+
setattr(logging, methodName, logToRoot)
|
202
|
+
|
203
|
+
|
204
|
+
class SlidgeLogger(logging.Logger):
|
205
|
+
def trace(self):
|
206
|
+
pass
|
207
|
+
|
208
|
+
|
209
|
+
log = logging.getLogger(__name__)
|
210
|
+
|
211
|
+
|
212
|
+
def get_version():
|
213
|
+
try:
|
214
|
+
git = subprocess.check_output(
|
215
|
+
["git", "rev-parse", "HEAD"], stderr=subprocess.DEVNULL
|
216
|
+
).decode()
|
217
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
218
|
+
pass
|
219
|
+
else:
|
220
|
+
return "git-" + git[:10]
|
221
|
+
|
222
|
+
return "NO_VERSION"
|
223
|
+
|
224
|
+
|
225
|
+
def merge_resources(resources: dict[str, ResourceDict]) -> Optional[ResourceDict]:
|
226
|
+
if len(resources) == 0:
|
227
|
+
return None
|
228
|
+
|
229
|
+
if len(resources) == 1:
|
230
|
+
return next(iter(resources.values()))
|
231
|
+
|
232
|
+
by_priority = sorted(resources.values(), key=lambda r: r["priority"], reverse=True)
|
233
|
+
|
234
|
+
if any(r["show"] == "" for r in resources.values()):
|
235
|
+
# if a client is "available", we're "available"
|
236
|
+
show = ""
|
237
|
+
else:
|
238
|
+
for r in by_priority:
|
239
|
+
if r["show"]:
|
240
|
+
show = r["show"]
|
241
|
+
break
|
242
|
+
else:
|
243
|
+
raise RuntimeError()
|
244
|
+
|
245
|
+
# if there are different statuses, we use the highest priority one,
|
246
|
+
# but we ignore resources without status, even with high priority
|
247
|
+
status = ""
|
248
|
+
for r in by_priority:
|
249
|
+
if r["status"]:
|
250
|
+
status = r["status"]
|
251
|
+
break
|
252
|
+
|
253
|
+
return {
|
254
|
+
"show": show, # type:ignore
|
255
|
+
"status": status,
|
256
|
+
"priority": 0,
|
257
|
+
}
|
258
|
+
|
259
|
+
|
260
|
+
def remove_emoji_variation_selector_16(emoji: str):
|
261
|
+
# this is required for compatibility with dino, and maybe other future clients?
|
262
|
+
return bytes(emoji, encoding="utf-8").replace(b"\xef\xb8\x8f", b"").decode()
|
263
|
+
|
264
|
+
|
265
|
+
def deprecated(name: str, new: Callable):
|
266
|
+
# @functools.wraps
|
267
|
+
def wrapped(*args, **kwargs):
|
268
|
+
warnings.warn(
|
269
|
+
f"{name} is deprecated. Use {new.__name__} instead",
|
270
|
+
category=DeprecationWarning,
|
271
|
+
)
|
272
|
+
return new(*args, **kwargs)
|
273
|
+
|
274
|
+
return wrapped
|
275
|
+
|
276
|
+
|
277
|
+
def dict_to_named_tuple(data: dict, cls: Type[NamedTuple]):
|
278
|
+
return cls(*(data.get(f) for f in cls._fields)) # type:ignore
|
279
|
+
|
280
|
+
|
281
|
+
def replace_mentions(
|
282
|
+
text: str,
|
283
|
+
mentions: Optional[list[Mention]],
|
284
|
+
mapping: Callable[["LegacyContact"], str],
|
285
|
+
):
|
286
|
+
if not mentions:
|
287
|
+
return text
|
288
|
+
|
289
|
+
cursor = 0
|
290
|
+
pieces = []
|
291
|
+
for mention in mentions:
|
292
|
+
pieces.extend([text[cursor : mention.start], mapping(mention.contact)])
|
293
|
+
cursor = mention.end
|
294
|
+
pieces.append(text[cursor:])
|
295
|
+
return "".join(pieces)
|