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/test.py ADDED
@@ -0,0 +1,295 @@
1
+ # type:ignore
2
+ import tempfile
3
+ import types
4
+ from pathlib import Path
5
+ from typing import Optional, Union
6
+ from xml.dom.minidom import parseString
7
+
8
+ import xmldiff.main
9
+ from slixmpp import (
10
+ ElementBase,
11
+ Iq,
12
+ MatcherId,
13
+ MatchXMLMask,
14
+ MatchXPath,
15
+ Message,
16
+ Presence,
17
+ StanzaPath,
18
+ )
19
+ from slixmpp.stanza.error import Error
20
+ from slixmpp.test import SlixTest, TestTransport
21
+ from slixmpp.xmlstream import highlight, tostring
22
+ from slixmpp.xmlstream.matcher import MatchIDSender
23
+
24
+ from slidge import (
25
+ BaseGateway,
26
+ BaseSession,
27
+ LegacyBookmarks,
28
+ LegacyContact,
29
+ LegacyMUC,
30
+ LegacyParticipant,
31
+ LegacyRoster,
32
+ user_store,
33
+ )
34
+
35
+ from ..command import Command
36
+ from ..core import config
37
+ from ..core.config import _TimedeltaSeconds
38
+
39
+
40
+ class SlixTestPlus(SlixTest):
41
+ def setUp(self):
42
+ super().setUp()
43
+ Error.namespace = "jabber:component:accept"
44
+
45
+ def check(self, stanza, criteria, method="exact", defaults=None, use_values=True):
46
+ """
47
+ Create and compare several stanza objects to a correct XML string.
48
+
49
+ If use_values is False, tests using stanza.values will not be used.
50
+
51
+ Some stanzas provide default values for some interfaces, but
52
+ these defaults can be problematic for testing since they can easily
53
+ be forgotten when supplying the XML string. A list of interfaces that
54
+ use defaults may be provided and the generated stanzas will use the
55
+ default values for those interfaces if needed.
56
+
57
+ However, correcting the supplied XML is not possible for interfaces
58
+ that add or remove XML elements. Only interfaces that map to XML
59
+ attributes may be set using the defaults parameter. The supplied XML
60
+ must take into account any extra elements that are included by default.
61
+
62
+ Arguments:
63
+ stanza -- The stanza object to test.
64
+ criteria -- An expression the stanza must match against.
65
+ method -- The type of matching to use; one of:
66
+ 'exact', 'mask', 'id', 'xpath', and 'stanzapath'.
67
+ Defaults to the value of self.match_method.
68
+ defaults -- A list of stanza interfaces that have default
69
+ values. These interfaces will be set to their
70
+ defaults for the given and generated stanzas to
71
+ prevent unexpected test failures.
72
+ use_values -- Indicates if testing using stanza.values should
73
+ be used. Defaults to True.
74
+ """
75
+ if method is None and hasattr(self, "match_method"):
76
+ method = getattr(self, "match_method")
77
+
78
+ if method != "exact":
79
+ matchers = {
80
+ "stanzapath": StanzaPath,
81
+ "xpath": MatchXPath,
82
+ "mask": MatchXMLMask,
83
+ "idsender": MatchIDSender,
84
+ "id": MatcherId,
85
+ }
86
+ Matcher = matchers.get(method, None)
87
+ if Matcher is None:
88
+ raise ValueError("Unknown matching method.")
89
+ test = Matcher(criteria)
90
+ self.assertTrue(
91
+ test.match(stanza),
92
+ "Stanza did not match using %s method:\n" % method
93
+ + "Criteria:\n%s\n" % str(criteria)
94
+ + "Stanza:\n%s" % str(stanza),
95
+ )
96
+ else:
97
+ stanza_class = stanza.__class__
98
+ # Hack to preserve namespaces instead of having jabber:client
99
+ # everywhere.
100
+ old_ns = stanza_class.namespace
101
+ stanza_class.namespace = stanza.namespace
102
+ if not isinstance(criteria, ElementBase):
103
+ xml = self.parse_xml(criteria)
104
+ else:
105
+ xml = criteria.xml
106
+
107
+ # Ensure that top level namespaces are used, even if they
108
+ # were not provided.
109
+ self.fix_namespaces(stanza.xml)
110
+ self.fix_namespaces(xml)
111
+
112
+ stanza2 = stanza_class(xml=xml)
113
+
114
+ if use_values:
115
+ # Using stanza.values will add XML for any interface that
116
+ # has a default value. We need to set those defaults on
117
+ # the existing stanzas and XML so that they will compare
118
+ # correctly.
119
+ default_stanza = stanza_class()
120
+ if defaults is None:
121
+ known_defaults = {Message: ["type"], Presence: ["priority"]}
122
+ defaults = known_defaults.get(stanza_class, [])
123
+ for interface in defaults:
124
+ stanza[interface] = stanza[interface]
125
+ stanza2[interface] = stanza2[interface]
126
+ # Can really only automatically add defaults for top
127
+ # level attribute values. Anything else must be accounted
128
+ # for in the provided XML string.
129
+ if interface not in xml.attrib:
130
+ if interface in default_stanza.xml.attrib:
131
+ value = default_stanza.xml.attrib[interface]
132
+ xml.attrib[interface] = value
133
+
134
+ values = stanza2.values
135
+ stanza3 = stanza_class()
136
+ stanza3.values = values
137
+
138
+ debug = "Three methods for creating stanzas do not match.\n"
139
+ debug += "Given XML:\n%s\n" % highlight(tostring(xml))
140
+ debug += "Given stanza:\n%s\n" % format_stanza(stanza)
141
+ debug += "Generated stanza:\n%s\n" % highlight(tostring(stanza2.xml))
142
+ debug += "Second generated stanza:\n%s\n" % highlight(
143
+ tostring(stanza3.xml)
144
+ )
145
+ result = self.compare(xml, stanza.xml, stanza2.xml, stanza3.xml)
146
+ else:
147
+ debug = "Two methods for creating stanzas do not match.\n"
148
+ debug += "Given XML:\n%s\n" % highlight(tostring(xml))
149
+ debug += "Given stanza:\n%s\n" % format_stanza(stanza)
150
+ debug += "Generated stanza:\n%s\n" % highlight(tostring(stanza2.xml))
151
+ result = self.compare(xml, stanza.xml, stanza2.xml)
152
+ stanza_class.namespace = old_ns
153
+
154
+ if not result:
155
+ debug += str(
156
+ xmldiff.main.diff_texts(tostring(xml), tostring(stanza.xml))
157
+ )
158
+ if use_values:
159
+ debug += str(
160
+ xmldiff.main.diff_texts(tostring(xml), tostring(stanza2.xml))
161
+ )
162
+ self.assertTrue(result, debug)
163
+
164
+ def next_sent(self, timeout=0.05) -> Optional[Union[Message, Iq, Presence]]:
165
+ self.wait_for_send_queue()
166
+ sent = self.xmpp.socket.next_sent(timeout=timeout)
167
+ if sent is None:
168
+ return None
169
+ xml = self.parse_xml(sent)
170
+ self.fix_namespaces(xml, "jabber:component:accept")
171
+ sent = self.xmpp._build_stanza(xml, "jabber:component:accept")
172
+ return sent
173
+
174
+
175
+ class SlidgeTest(SlixTestPlus):
176
+ plugin: Union[types.ModuleType, dict]
177
+
178
+ class Config:
179
+ jid = "aim.shakespeare.lit"
180
+ secret = "test"
181
+ server = "shakespeare.lit"
182
+ port = 5222
183
+ upload_service = "upload.test"
184
+ home_dir = Path(tempfile.mkdtemp())
185
+ user_jid_validator = ".*"
186
+ admins: list[str] = []
187
+ no_roster_push = False
188
+ upload_requester = None
189
+ ignore_delay_threshold = _TimedeltaSeconds("300")
190
+
191
+ @classmethod
192
+ def setUpClass(cls):
193
+ user_store.set_file(Path(tempfile.mkdtemp()) / "test.db")
194
+ for k, v in vars(cls.Config).items():
195
+ setattr(config, k.upper(), v)
196
+
197
+ def setUp(self):
198
+ if hasattr(self, "plugin"):
199
+ BaseGateway._subclass = find_subclass(self.plugin, BaseGateway)
200
+ BaseSession._subclass = find_subclass(self.plugin, BaseSession)
201
+ LegacyRoster._subclass = find_subclass(
202
+ self.plugin, LegacyRoster, base_ok=True
203
+ )
204
+ LegacyContact._subclass = find_subclass(
205
+ self.plugin, LegacyContact, base_ok=True
206
+ )
207
+ LegacyMUC._subclass = find_subclass(self.plugin, LegacyMUC, base_ok=True)
208
+ LegacyBookmarks._subclass = find_subclass(
209
+ self.plugin, LegacyBookmarks, base_ok=True
210
+ )
211
+
212
+ self.xmpp = BaseGateway.get_self_or_unique_subclass()()
213
+
214
+ self.xmpp._always_send_everything = True
215
+
216
+ self.xmpp.connection_made(TestTransport(self.xmpp))
217
+ self.xmpp.session_bind_event.set()
218
+ # Remove unique ID prefix to make it easier to test
219
+ self.xmpp._id_prefix = ""
220
+ self.xmpp.default_lang = None
221
+ self.xmpp.peer_default_lang = None
222
+
223
+ def new_id():
224
+ self.xmpp._id += 1
225
+ return str(self.xmpp._id)
226
+
227
+ self.xmpp._id = 0
228
+ self.xmpp.new_id = new_id
229
+
230
+ # Must have the stream header ready for xmpp.process() to work.
231
+ header = self.xmpp.stream_header
232
+
233
+ self.xmpp.data_received(header)
234
+ self.wait_for_send_queue()
235
+
236
+ self.xmpp.socket.next_sent()
237
+ self.xmpp.socket.next_sent()
238
+
239
+ # Some plugins require messages to have ID values. Set
240
+ # this to True in tests related to those plugins.
241
+ self.xmpp.use_message_ids = False
242
+ self.xmpp.use_presence_ids = False
243
+ Error.namespace = "jabber:component:accept"
244
+
245
+ @classmethod
246
+ def tearDownClass(cls):
247
+ reset_subclasses()
248
+ user_store._users = None
249
+
250
+
251
+ def format_stanza(stanza):
252
+ return highlight(
253
+ "\n".join(parseString(tostring(stanza.xml)).toprettyxml().split("\n")[1:])
254
+ )
255
+
256
+
257
+ def find_subclass(o, parent, base_ok=False):
258
+ try:
259
+ vals = vars(o).values()
260
+ except TypeError:
261
+ vals = o.values()
262
+ for x in vals:
263
+ try:
264
+ if issubclass(x, parent) and x is not parent:
265
+ return x
266
+ except TypeError:
267
+ pass
268
+ else:
269
+ if base_ok:
270
+ return parent
271
+ else:
272
+ raise RuntimeError
273
+
274
+
275
+ def reset_subclasses():
276
+ """
277
+ Reset registered subclasses between test classes.
278
+
279
+ Needed because these classes are meant to only be subclassed once and raise
280
+ exceptions otherwise.
281
+ """
282
+ BaseSession.reset_subclass()
283
+ BaseGateway.reset_subclass()
284
+ LegacyRoster.reset_subclass()
285
+ LegacyContact.reset_subclass()
286
+ LegacyMUC.reset_subclass()
287
+ LegacyBookmarks.reset_subclass()
288
+ LegacyParticipant.reset_subclass()
289
+ # reset_commands()
290
+
291
+
292
+ def reset_commands():
293
+ Command.subclasses = [
294
+ c for c in Command.subclasses if str(c).startswith("<class 'slidge.core")
295
+ ]
slidge/util/types.py ADDED
@@ -0,0 +1,180 @@
1
+ """
2
+ Typing stuff
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+ from enum import IntEnum
7
+ from pathlib import Path
8
+ from typing import (
9
+ IO,
10
+ TYPE_CHECKING,
11
+ Any,
12
+ Generic,
13
+ Hashable,
14
+ Literal,
15
+ NamedTuple,
16
+ Optional,
17
+ TypedDict,
18
+ TypeVar,
19
+ Union,
20
+ )
21
+
22
+ from slixmpp import Message, Presence
23
+ from slixmpp.types import PresenceShows
24
+
25
+ if TYPE_CHECKING:
26
+ from ..contact import LegacyContact
27
+ from ..core.pubsub import PepItem
28
+ from ..core.session import BaseSession
29
+ from ..group.participant import LegacyMUC, LegacyParticipant
30
+ from .db import GatewayUser
31
+
32
+ AnyBaseSession = BaseSession[Any, Any]
33
+ else:
34
+ AnyBaseSession = None
35
+
36
+
37
+ class URL(str):
38
+ pass
39
+
40
+
41
+ LegacyGroupIdType = TypeVar("LegacyGroupIdType", bound=Hashable)
42
+ """
43
+ Type of the unique identifier for groups, usually a str or an int,
44
+ but anything hashable should work.
45
+ """
46
+ LegacyMessageType = TypeVar("LegacyMessageType", bound=Hashable)
47
+ LegacyThreadType = TypeVar("LegacyThreadType", bound=Hashable)
48
+ LegacyUserIdType = TypeVar("LegacyUserIdType", bound=Hashable)
49
+
50
+ LegacyContactType = TypeVar("LegacyContactType", bound="LegacyContact")
51
+ LegacyMUCType = TypeVar("LegacyMUCType", bound="LegacyMUC")
52
+ LegacyParticipantType = TypeVar("LegacyParticipantType", bound="LegacyParticipant")
53
+
54
+ PepItemType = TypeVar("PepItemType", bound="PepItem")
55
+
56
+ Recipient = Union["LegacyMUC", "LegacyContact"]
57
+ RecipientType = TypeVar("RecipientType", bound=Recipient)
58
+ Sender = Union["LegacyContact", "LegacyParticipant"]
59
+ AvatarType = Union[bytes, str, Path]
60
+ LegacyFileIdType = Union[int, str]
61
+ AvatarIdType = Union[LegacyFileIdType, URL]
62
+
63
+ ChatState = Literal["active", "composing", "gone", "inactive", "paused"]
64
+ ProcessingHint = Literal["no-store", "markable", "store"]
65
+ Marker = Literal["acknowledged", "received", "displayed"]
66
+ FieldType = Literal[
67
+ "boolean",
68
+ "fixed",
69
+ "text-single",
70
+ "jid-single",
71
+ "jid-multi",
72
+ "list-single",
73
+ "list-multi",
74
+ "text-private",
75
+ ]
76
+ MucAffiliation = Literal["owner", "admin", "member", "outcast", "none"]
77
+ MucRole = Literal["visitor", "participant", "moderator", "none"]
78
+
79
+
80
+ @dataclass
81
+ class MessageReference(Generic[LegacyMessageType]):
82
+ """
83
+ A "message reply", ie a "quoted message" (:xep:`0461`)
84
+
85
+ At the very minimum, the legacy message ID attribute must be set, but to
86
+ ensure that the quote is displayed in all XMPP clients, the author must also
87
+ be set.
88
+ The body is used as a fallback for XMPP clients that do not support :xep:`0461`
89
+ of that failed to find the referenced message.
90
+ """
91
+
92
+ legacy_id: LegacyMessageType
93
+ author: Optional[Union["GatewayUser", "LegacyParticipant", "LegacyContact"]] = None
94
+ body: Optional[str] = None
95
+
96
+
97
+ @dataclass
98
+ class LegacyAttachment:
99
+ """
100
+ A file attachment to a message
101
+
102
+ At the minimum, one of the ``path``, ``steam``, ``data`` or ``url`` attribute
103
+ has to be set
104
+
105
+ To be used with :meth:`.LegacyContact.send_files` or
106
+ :meth:`.LegacyParticipant.send_files`
107
+ """
108
+
109
+ path: Optional[Union[Path, str]] = None
110
+ name: Optional[Union[str]] = None
111
+ stream: Optional[IO[bytes]] = None
112
+ data: Optional[bytes] = None
113
+ content_type: Optional[str] = None
114
+ legacy_file_id: Optional[Union[str, int]] = None
115
+ url: Optional[str] = None
116
+ caption: Optional[str] = None
117
+ """
118
+ A caption for this specific image. For a global caption for a list of attachments,
119
+ use the ``body`` parameter of :meth:`.AttachmentMixin.send_files`
120
+ """
121
+
122
+ def __post_init__(self):
123
+ if not any(
124
+ x is not None for x in (self.path, self.stream, self.data, self.url)
125
+ ):
126
+ raise TypeError("There is not data in this attachment", self)
127
+
128
+
129
+ class MucType(IntEnum):
130
+ """
131
+ The type of group, private, public, anonymous or not.
132
+ """
133
+
134
+ GROUP = 0
135
+ """
136
+ A private group, members-only and non-anonymous, eg a family group.
137
+ """
138
+ CHANNEL = 1
139
+ """
140
+ A public group, aka an anonymous channel.
141
+ """
142
+ CHANNEL_NON_ANONYMOUS = 2
143
+ """
144
+ A public group where participants' legacy IDs are visible to everybody.
145
+ """
146
+
147
+
148
+ PseudoPresenceShow = Union[PresenceShows, Literal[""]]
149
+
150
+
151
+ class ResourceDict(TypedDict):
152
+ show: PseudoPresenceShow
153
+ status: str
154
+ priority: int
155
+
156
+
157
+ MessageOrPresenceTypeVar = TypeVar(
158
+ "MessageOrPresenceTypeVar", bound=Union[Message, Presence]
159
+ )
160
+
161
+
162
+ class LinkPreview(NamedTuple):
163
+ about: str
164
+ title: Optional[str]
165
+ description: Optional[str]
166
+ url: Optional[str]
167
+ image: Optional[str]
168
+ type: Optional[str]
169
+ site_name: Optional[str]
170
+
171
+
172
+ class Mention(NamedTuple):
173
+ contact: "LegacyContact"
174
+ start: int
175
+ end: int
176
+
177
+
178
+ class Hat(NamedTuple):
179
+ uri: str
180
+ title: str