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/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)