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