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
@@ -0,0 +1,130 @@
|
|
1
|
+
from copy import copy
|
2
|
+
from typing import TYPE_CHECKING
|
3
|
+
|
4
|
+
from slixmpp import CoroutineCallback, Iq, StanzaPath, register_stanza_plugin
|
5
|
+
from slixmpp.exceptions import XMPPError
|
6
|
+
from slixmpp.plugins.xep_0084 import MetaData
|
7
|
+
from slixmpp.plugins.xep_0292.stanza import NS as VCard4NS
|
8
|
+
|
9
|
+
from ...contact import LegacyContact
|
10
|
+
from ...core.session import BaseSession
|
11
|
+
from ...group import LegacyParticipant
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
from .base import BaseGateway
|
15
|
+
|
16
|
+
|
17
|
+
class VCardTemp:
|
18
|
+
def __init__(self, xmpp: "BaseGateway"):
|
19
|
+
self.xmpp = xmpp
|
20
|
+
# remove slixmpp's default handler to replace with our own
|
21
|
+
self.xmpp.remove_handler("VCardTemp")
|
22
|
+
xmpp.register_handler(
|
23
|
+
CoroutineCallback(
|
24
|
+
"VCardTemp",
|
25
|
+
StanzaPath("iq/vcard_temp"),
|
26
|
+
self.__handler, # type:ignore
|
27
|
+
)
|
28
|
+
)
|
29
|
+
# TODO: MR to slixmpp adding this to XEP-0084
|
30
|
+
register_stanza_plugin(
|
31
|
+
self.xmpp.plugin["xep_0060"].stanza.Item,
|
32
|
+
MetaData,
|
33
|
+
)
|
34
|
+
|
35
|
+
async def __handler(self, iq: Iq):
|
36
|
+
if iq["type"] == "get":
|
37
|
+
return await self.__handle_get_vcard_temp(iq)
|
38
|
+
|
39
|
+
if iq["type"] == "set":
|
40
|
+
return await self.__handle_set_vcard_temp(iq)
|
41
|
+
|
42
|
+
async def __fetch_user_avatar(self, session: BaseSession):
|
43
|
+
hash_ = session.avatar_hash
|
44
|
+
if not hash_:
|
45
|
+
raise XMPPError("item-not-found", "This participant has no contact")
|
46
|
+
meta_iq = await self.xmpp.plugin["xep_0060"].get_item(
|
47
|
+
session.user.jid,
|
48
|
+
MetaData.namespace,
|
49
|
+
hash_,
|
50
|
+
ifrom=self.xmpp.boundjid.bare,
|
51
|
+
)
|
52
|
+
info = meta_iq["pubsub"]["items"]["item"]["avatar_metadata"]["info"]
|
53
|
+
type_ = info["type"]
|
54
|
+
data_iq = await self.xmpp.plugin["xep_0084"].retrieve_avatar(
|
55
|
+
session.user.jid, hash_, ifrom=self.xmpp.boundjid.bare
|
56
|
+
)
|
57
|
+
bytes_ = data_iq["pubsub"]["items"]["item"]["avatar_data"]["value"]
|
58
|
+
return bytes_, type_
|
59
|
+
|
60
|
+
async def __handle_get_vcard_temp(self, iq: Iq):
|
61
|
+
session = self.xmpp.get_session_from_stanza(iq)
|
62
|
+
entity = await session.get_contact_or_group_or_participant(iq.get_to())
|
63
|
+
if not entity:
|
64
|
+
raise XMPPError("item-not-found")
|
65
|
+
|
66
|
+
bytes_ = None
|
67
|
+
if isinstance(entity, LegacyParticipant):
|
68
|
+
if entity.is_user:
|
69
|
+
bytes_, type_ = await self.__fetch_user_avatar(session)
|
70
|
+
if not bytes_:
|
71
|
+
raise XMPPError(
|
72
|
+
"internal-server-error",
|
73
|
+
"Could not fetch the slidge user's avatar",
|
74
|
+
)
|
75
|
+
avatar = None
|
76
|
+
vcard = None
|
77
|
+
elif not (contact := entity.contact):
|
78
|
+
raise XMPPError("item-not-found", "This participant has no contact")
|
79
|
+
else:
|
80
|
+
vcard = await self.xmpp.vcard.get_vcard(contact.jid, iq.get_from())
|
81
|
+
avatar = contact.get_avatar()
|
82
|
+
type_ = "image/png"
|
83
|
+
else:
|
84
|
+
avatar = entity.get_avatar()
|
85
|
+
type_ = "image/png"
|
86
|
+
if isinstance(entity, LegacyContact):
|
87
|
+
vcard = await self.xmpp.vcard.get_vcard(entity.jid, iq.get_from())
|
88
|
+
else:
|
89
|
+
vcard = None
|
90
|
+
v = self.xmpp.plugin["xep_0054"].make_vcard()
|
91
|
+
if avatar is not None and avatar.data:
|
92
|
+
bytes_ = avatar.data.get_value()
|
93
|
+
if bytes_:
|
94
|
+
v["PHOTO"]["BINVAL"] = bytes_
|
95
|
+
v["PHOTO"]["TYPE"] = type_
|
96
|
+
if vcard:
|
97
|
+
for el in vcard.xml:
|
98
|
+
new = copy(el)
|
99
|
+
new.tag = el.tag.replace(f"{{{VCard4NS}}}", "")
|
100
|
+
v.append(new)
|
101
|
+
reply = iq.reply()
|
102
|
+
reply.append(v)
|
103
|
+
reply.send()
|
104
|
+
|
105
|
+
async def __handle_set_vcard_temp(self, iq: Iq):
|
106
|
+
muc = await self.xmpp.get_muc_from_stanza(iq)
|
107
|
+
to = iq.get_to()
|
108
|
+
|
109
|
+
if to.resource:
|
110
|
+
raise XMPPError("bad-request", "You cannot set participants avatars")
|
111
|
+
|
112
|
+
data = iq["vcard_temp"]["PHOTO"]["BINVAL"] or None
|
113
|
+
try:
|
114
|
+
legacy_id = await muc.on_avatar(
|
115
|
+
data, iq["vcard_temp"]["PHOTO"]["TYPE"] or None
|
116
|
+
)
|
117
|
+
except XMPPError:
|
118
|
+
raise
|
119
|
+
except Exception as e:
|
120
|
+
raise XMPPError("internal-server-error", str(e))
|
121
|
+
reply = iq.reply(clear=True)
|
122
|
+
reply.enable("vcard_temp")
|
123
|
+
reply.send()
|
124
|
+
|
125
|
+
if not data:
|
126
|
+
await muc.set_avatar(None, blocking=True)
|
127
|
+
return
|
128
|
+
|
129
|
+
if legacy_id:
|
130
|
+
await muc.set_avatar(data, legacy_id, blocking=True)
|
@@ -0,0 +1,19 @@
|
|
1
|
+
"""
|
2
|
+
Mixins
|
3
|
+
"""
|
4
|
+
|
5
|
+
from .avatar import AvatarMixin
|
6
|
+
from .disco import ChatterDiscoMixin
|
7
|
+
from .message import MessageCarbonMixin, MessageMixin
|
8
|
+
from .presence import PresenceMixin
|
9
|
+
|
10
|
+
|
11
|
+
class FullMixin(ChatterDiscoMixin, MessageMixin, PresenceMixin):
|
12
|
+
pass
|
13
|
+
|
14
|
+
|
15
|
+
class FullCarbonMixin(ChatterDiscoMixin, MessageCarbonMixin, PresenceMixin):
|
16
|
+
pass
|
17
|
+
|
18
|
+
|
19
|
+
__all__ = ("AvatarMixin",)
|
@@ -0,0 +1,506 @@
|
|
1
|
+
import functools
|
2
|
+
import logging
|
3
|
+
import os
|
4
|
+
import re
|
5
|
+
import shutil
|
6
|
+
import stat
|
7
|
+
import tempfile
|
8
|
+
import warnings
|
9
|
+
from datetime import datetime
|
10
|
+
from mimetypes import guess_type
|
11
|
+
from pathlib import Path
|
12
|
+
from typing import IO, Collection, Optional, Sequence, Union
|
13
|
+
from urllib.parse import quote as urlquote
|
14
|
+
from uuid import uuid4
|
15
|
+
from xml.etree import ElementTree as ET
|
16
|
+
|
17
|
+
import blurhash
|
18
|
+
from PIL import Image
|
19
|
+
from slixmpp import JID, Message
|
20
|
+
from slixmpp.exceptions import IqError
|
21
|
+
from slixmpp.plugins.xep_0363 import FileUploadError
|
22
|
+
from slixmpp.plugins.xep_0385.stanza import Sims
|
23
|
+
from slixmpp.plugins.xep_0447.stanza import StatelessFileSharing
|
24
|
+
|
25
|
+
from ...util.sql import db
|
26
|
+
from ...util.types import (
|
27
|
+
LegacyAttachment,
|
28
|
+
LegacyMessageType,
|
29
|
+
LegacyThreadType,
|
30
|
+
MessageReference,
|
31
|
+
)
|
32
|
+
from ...util.util import fix_suffix
|
33
|
+
from .. import config
|
34
|
+
from ..cache import avatar_cache
|
35
|
+
from .message_maker import MessageMaker
|
36
|
+
|
37
|
+
|
38
|
+
class AttachmentMixin(MessageMaker):
|
39
|
+
def send_text(self, *_, **k) -> Optional[Message]:
|
40
|
+
raise NotImplementedError
|
41
|
+
|
42
|
+
async def __upload(
|
43
|
+
self,
|
44
|
+
file_path: Path,
|
45
|
+
file_name: Optional[str] = None,
|
46
|
+
content_type: Optional[str] = None,
|
47
|
+
):
|
48
|
+
if file_name and file_path.name != file_name:
|
49
|
+
d = Path(tempfile.mkdtemp())
|
50
|
+
temp = d / file_name
|
51
|
+
temp.symlink_to(file_path)
|
52
|
+
file_path = temp
|
53
|
+
else:
|
54
|
+
d = None
|
55
|
+
if config.UPLOAD_SERVICE:
|
56
|
+
domain = None
|
57
|
+
else:
|
58
|
+
domain = re.sub(r"^.*?\.", "", self.xmpp.boundjid.bare)
|
59
|
+
try:
|
60
|
+
new_url = await self.xmpp.plugin["xep_0363"].upload_file(
|
61
|
+
filename=file_path,
|
62
|
+
content_type=content_type,
|
63
|
+
ifrom=config.UPLOAD_REQUESTER or self.xmpp.boundjid,
|
64
|
+
domain=JID(domain),
|
65
|
+
)
|
66
|
+
except (FileUploadError, IqError) as e:
|
67
|
+
warnings.warn(f"Something is wrong with the upload service: {e!r}")
|
68
|
+
return None
|
69
|
+
finally:
|
70
|
+
if d is not None:
|
71
|
+
file_path.unlink()
|
72
|
+
d.rmdir()
|
73
|
+
|
74
|
+
return new_url
|
75
|
+
|
76
|
+
@staticmethod
|
77
|
+
async def __no_upload(
|
78
|
+
file_path: Path,
|
79
|
+
file_name: Optional[str] = None,
|
80
|
+
legacy_file_id: Optional[Union[str, int]] = None,
|
81
|
+
):
|
82
|
+
file_id = str(uuid4()) if legacy_file_id is None else str(legacy_file_id)
|
83
|
+
assert config.NO_UPLOAD_PATH is not None
|
84
|
+
assert config.NO_UPLOAD_URL_PREFIX is not None
|
85
|
+
destination_dir = Path(config.NO_UPLOAD_PATH) / file_id
|
86
|
+
|
87
|
+
if destination_dir.exists():
|
88
|
+
log.debug("Dest dir exists: %s", destination_dir)
|
89
|
+
files = list(f for f in destination_dir.glob("**/*") if f.is_file())
|
90
|
+
if len(files) == 1:
|
91
|
+
log.debug(
|
92
|
+
"Found the legacy attachment '%s' at '%s'",
|
93
|
+
legacy_file_id,
|
94
|
+
files[0],
|
95
|
+
)
|
96
|
+
name = files[0].name
|
97
|
+
uu = files[0].parent.name # anti-obvious url trick, see below
|
98
|
+
return files[0], "/".join([file_id, uu, name])
|
99
|
+
else:
|
100
|
+
log.warning(
|
101
|
+
(
|
102
|
+
"There are several or zero files in %s, "
|
103
|
+
"slidge doesn't know which one to pick among %s. "
|
104
|
+
"Removing the dir."
|
105
|
+
),
|
106
|
+
destination_dir,
|
107
|
+
files,
|
108
|
+
)
|
109
|
+
shutil.rmtree(destination_dir)
|
110
|
+
|
111
|
+
log.debug("Did not find a file in: %s", destination_dir)
|
112
|
+
# let's use a UUID to avoid URLs being too obvious
|
113
|
+
uu = str(uuid4())
|
114
|
+
destination_dir = destination_dir / uu
|
115
|
+
destination_dir.mkdir(parents=True)
|
116
|
+
|
117
|
+
name = file_name or file_path.name
|
118
|
+
destination = destination_dir / name
|
119
|
+
method = config.NO_UPLOAD_METHOD
|
120
|
+
if method == "copy":
|
121
|
+
shutil.copy2(file_path, destination)
|
122
|
+
elif method == "hardlink":
|
123
|
+
os.link(file_path, destination)
|
124
|
+
elif method == "symlink":
|
125
|
+
os.symlink(file_path, destination, target_is_directory=True)
|
126
|
+
elif method == "move":
|
127
|
+
shutil.move(file_path, destination)
|
128
|
+
else:
|
129
|
+
raise RuntimeError("No upload method not recognized", method)
|
130
|
+
|
131
|
+
if config.NO_UPLOAD_FILE_READ_OTHERS:
|
132
|
+
log.debug("Changing perms of %s", destination)
|
133
|
+
destination.chmod(destination.stat().st_mode | stat.S_IROTH)
|
134
|
+
uploaded_url = "/".join([file_id, uu, name])
|
135
|
+
|
136
|
+
return destination, uploaded_url
|
137
|
+
|
138
|
+
async def __get_url(
|
139
|
+
self,
|
140
|
+
file_path: Optional[Path] = None,
|
141
|
+
data_stream: Optional[IO[bytes]] = None,
|
142
|
+
data: Optional[bytes] = None,
|
143
|
+
file_url: Optional[str] = None,
|
144
|
+
file_name: Optional[str] = None,
|
145
|
+
content_type: Optional[str] = None,
|
146
|
+
legacy_file_id: Optional[Union[str, int]] = None,
|
147
|
+
) -> tuple[bool, Optional[Path], str]:
|
148
|
+
if legacy_file_id:
|
149
|
+
cache = db.attachment_get_url(legacy_file_id)
|
150
|
+
if cache is not None:
|
151
|
+
async with self.session.http.head(cache) as r:
|
152
|
+
if r.status < 400:
|
153
|
+
return False, None, cache
|
154
|
+
else:
|
155
|
+
db.attachment_remove(legacy_file_id)
|
156
|
+
|
157
|
+
if file_url and config.USE_ATTACHMENT_ORIGINAL_URLS:
|
158
|
+
return False, None, file_url
|
159
|
+
|
160
|
+
if file_name and len(file_name) > config.ATTACHMENT_MAXIMUM_FILE_NAME_LENGTH:
|
161
|
+
log.debug("Trimming long filename: %s", file_name)
|
162
|
+
base, ext = os.path.splitext(file_name)
|
163
|
+
file_name = (
|
164
|
+
base[: config.ATTACHMENT_MAXIMUM_FILE_NAME_LENGTH - len(ext)] + ext
|
165
|
+
)
|
166
|
+
|
167
|
+
if file_path is None:
|
168
|
+
file_name = str(uuid4()) if file_name is None else file_name
|
169
|
+
temp_dir = Path(tempfile.mkdtemp())
|
170
|
+
file_path = temp_dir / file_name
|
171
|
+
if file_url:
|
172
|
+
async with self.session.http.get(file_url) as r:
|
173
|
+
with file_path.open("wb") as f:
|
174
|
+
f.write(await r.read())
|
175
|
+
|
176
|
+
else:
|
177
|
+
if data_stream is not None:
|
178
|
+
data = data_stream.read()
|
179
|
+
if data is None:
|
180
|
+
raise RuntimeError
|
181
|
+
|
182
|
+
with file_path.open("wb") as f:
|
183
|
+
f.write(data)
|
184
|
+
|
185
|
+
is_temp = not bool(config.NO_UPLOAD_PATH)
|
186
|
+
else:
|
187
|
+
is_temp = False
|
188
|
+
|
189
|
+
if config.FIX_FILENAME_SUFFIX_MIME_TYPE:
|
190
|
+
file_name = str(fix_suffix(file_path, content_type, file_name))
|
191
|
+
|
192
|
+
if config.NO_UPLOAD_PATH:
|
193
|
+
local_path, new_url = await self.__no_upload(
|
194
|
+
file_path, file_name, legacy_file_id
|
195
|
+
)
|
196
|
+
new_url = (config.NO_UPLOAD_URL_PREFIX or "") + "/" + urlquote(new_url)
|
197
|
+
else:
|
198
|
+
local_path = file_path
|
199
|
+
new_url = await self.__upload(file_path, file_name, content_type)
|
200
|
+
if legacy_file_id:
|
201
|
+
db.attachment_store_url(legacy_file_id, new_url)
|
202
|
+
|
203
|
+
return is_temp, local_path, new_url
|
204
|
+
|
205
|
+
async def __set_sims(
|
206
|
+
self,
|
207
|
+
msg: Message,
|
208
|
+
uploaded_url: str,
|
209
|
+
path: Optional[Path],
|
210
|
+
content_type: Optional[str] = None,
|
211
|
+
caption: Optional[str] = None,
|
212
|
+
file_name: Optional[str] = None,
|
213
|
+
):
|
214
|
+
cache = db.attachment_get_sims(uploaded_url)
|
215
|
+
if cache:
|
216
|
+
msg.append(Sims(xml=ET.fromstring(cache)))
|
217
|
+
return
|
218
|
+
|
219
|
+
if not path:
|
220
|
+
return
|
221
|
+
|
222
|
+
sims = self.xmpp["xep_0385"].get_sims(
|
223
|
+
path, [uploaded_url], content_type, caption
|
224
|
+
)
|
225
|
+
if file_name:
|
226
|
+
sims["sims"]["file"]["name"] = file_name
|
227
|
+
if content_type is not None and content_type.startswith("image"):
|
228
|
+
try:
|
229
|
+
h, x, y = await self.xmpp.loop.run_in_executor(
|
230
|
+
avatar_cache._thread_pool, get_blurhash, path
|
231
|
+
)
|
232
|
+
except Exception as e:
|
233
|
+
log.debug("Could not generate a blurhash", exc_info=e)
|
234
|
+
else:
|
235
|
+
thumbnail = sims["sims"]["file"]["thumbnail"]
|
236
|
+
thumbnail["width"] = x
|
237
|
+
thumbnail["height"] = y
|
238
|
+
thumbnail["media-type"] = "image/blurhash"
|
239
|
+
thumbnail["uri"] = "data:image/blurhash," + urlquote(h)
|
240
|
+
|
241
|
+
db.attachment_store_sims(uploaded_url, str(sims))
|
242
|
+
|
243
|
+
msg.append(sims)
|
244
|
+
|
245
|
+
def __set_sfs(
|
246
|
+
self,
|
247
|
+
msg: Message,
|
248
|
+
uploaded_url: str,
|
249
|
+
path: Optional[Path],
|
250
|
+
content_type: Optional[str] = None,
|
251
|
+
caption: Optional[str] = None,
|
252
|
+
file_name: Optional[str] = None,
|
253
|
+
):
|
254
|
+
cache = db.attachment_get_sfs(uploaded_url)
|
255
|
+
if cache:
|
256
|
+
msg.append(StatelessFileSharing(xml=ET.fromstring(cache)))
|
257
|
+
return
|
258
|
+
|
259
|
+
if not path:
|
260
|
+
return
|
261
|
+
|
262
|
+
sfs = self.xmpp["xep_0447"].get_sfs(path, [uploaded_url], content_type, caption)
|
263
|
+
if file_name:
|
264
|
+
sfs["file"]["name"] = file_name
|
265
|
+
db.attachment_store_sfs(uploaded_url, str(sfs))
|
266
|
+
|
267
|
+
msg.append(sfs)
|
268
|
+
|
269
|
+
def __send_url(
|
270
|
+
self,
|
271
|
+
msg: Message,
|
272
|
+
legacy_msg_id: LegacyMessageType,
|
273
|
+
uploaded_url: str,
|
274
|
+
caption: Optional[str] = None,
|
275
|
+
carbon=False,
|
276
|
+
when: Optional[datetime] = None,
|
277
|
+
**kwargs,
|
278
|
+
) -> list[Message]:
|
279
|
+
msg["oob"]["url"] = uploaded_url
|
280
|
+
msg["body"] = uploaded_url
|
281
|
+
if caption:
|
282
|
+
m1 = self._send(msg, carbon=carbon, **kwargs)
|
283
|
+
m2 = self.send_text(
|
284
|
+
caption, legacy_msg_id=legacy_msg_id, when=when, carbon=carbon, **kwargs
|
285
|
+
)
|
286
|
+
return [m1, m2] if m2 else [m1]
|
287
|
+
else:
|
288
|
+
self._set_msg_id(msg, legacy_msg_id)
|
289
|
+
return [self._send(msg, carbon=carbon, **kwargs)]
|
290
|
+
|
291
|
+
async def send_file(
|
292
|
+
self,
|
293
|
+
file_path: Optional[Union[Path, str]] = None,
|
294
|
+
legacy_msg_id: Optional[LegacyMessageType] = None,
|
295
|
+
*,
|
296
|
+
data_stream: Optional[IO[bytes]] = None,
|
297
|
+
data: Optional[bytes] = None,
|
298
|
+
file_url: Optional[str] = None,
|
299
|
+
file_name: Optional[str] = None,
|
300
|
+
content_type: Optional[str] = None,
|
301
|
+
reply_to: Optional[MessageReference] = None,
|
302
|
+
when: Optional[datetime] = None,
|
303
|
+
caption: Optional[str] = None,
|
304
|
+
legacy_file_id: Optional[Union[str, int]] = None,
|
305
|
+
thread: Optional[LegacyThreadType] = None,
|
306
|
+
**kwargs,
|
307
|
+
) -> tuple[Optional[str], list[Message]]:
|
308
|
+
"""
|
309
|
+
Send a single file from this :term:`XMPP Entity`.
|
310
|
+
|
311
|
+
:param file_path: Path to the attachment
|
312
|
+
:param data_stream: Alternatively, a stream of bytes (such as a File object)
|
313
|
+
:param data: Alternatively, a bytes object
|
314
|
+
:param file_url: Alternatively, a URL
|
315
|
+
:param file_name: How the file should be named.
|
316
|
+
:param content_type: MIME type, inferred from filename if not given
|
317
|
+
:param legacy_msg_id: If you want to be able to transport read markers from the gateway
|
318
|
+
user to the legacy network, specify this
|
319
|
+
:param reply_to: Quote another message (:xep:`0461`)
|
320
|
+
:param when: when the file was sent, for a "delay" tag (:xep:`0203`)
|
321
|
+
:param caption: an optional text that is linked to the file
|
322
|
+
:param legacy_file_id: A unique identifier for the file on the legacy network.
|
323
|
+
Plugins should try their best to provide it, to avoid duplicates.
|
324
|
+
:param thread:
|
325
|
+
"""
|
326
|
+
carbon = kwargs.pop("carbon", False)
|
327
|
+
mto = kwargs.pop("mto", None)
|
328
|
+
store_multi = kwargs.pop("store_multi", True)
|
329
|
+
msg = self._make_message(
|
330
|
+
when=when,
|
331
|
+
reply_to=reply_to,
|
332
|
+
carbon=carbon,
|
333
|
+
mto=mto,
|
334
|
+
thread=thread,
|
335
|
+
)
|
336
|
+
|
337
|
+
if content_type is None and (name := (file_name or file_path or file_url)):
|
338
|
+
content_type, _ = guess_type(name)
|
339
|
+
|
340
|
+
is_temp, local_path, new_url = await self.__get_url(
|
341
|
+
Path(file_path) if file_path else None,
|
342
|
+
data_stream,
|
343
|
+
data,
|
344
|
+
file_url,
|
345
|
+
file_name,
|
346
|
+
content_type,
|
347
|
+
legacy_file_id,
|
348
|
+
)
|
349
|
+
|
350
|
+
if new_url is None:
|
351
|
+
msg["body"] = (
|
352
|
+
"I tried to send a file, but something went wrong. "
|
353
|
+
"Tell your slidge admin to check the logs."
|
354
|
+
)
|
355
|
+
self._set_msg_id(msg, legacy_msg_id)
|
356
|
+
return None, [self._send(msg, **kwargs)]
|
357
|
+
|
358
|
+
await self.__set_sims(
|
359
|
+
msg, new_url, local_path, content_type, caption, file_name
|
360
|
+
)
|
361
|
+
self.__set_sfs(msg, new_url, local_path, content_type, caption, file_name)
|
362
|
+
if is_temp and isinstance(local_path, Path):
|
363
|
+
local_path.unlink()
|
364
|
+
local_path.parent.rmdir()
|
365
|
+
|
366
|
+
msgs = self.__send_url(
|
367
|
+
msg, legacy_msg_id, new_url, caption, carbon, when, **kwargs
|
368
|
+
)
|
369
|
+
if store_multi:
|
370
|
+
self.__store_multi(legacy_msg_id, msgs)
|
371
|
+
return new_url, msgs
|
372
|
+
|
373
|
+
def __send_body(
|
374
|
+
self,
|
375
|
+
body: Optional[str] = None,
|
376
|
+
legacy_msg_id: Optional[LegacyMessageType] = None,
|
377
|
+
reply_to: Optional[MessageReference] = None,
|
378
|
+
when: Optional[datetime] = None,
|
379
|
+
thread: Optional[LegacyThreadType] = None,
|
380
|
+
**kwargs,
|
381
|
+
) -> Optional[Message]:
|
382
|
+
if body:
|
383
|
+
return self.send_text(
|
384
|
+
body,
|
385
|
+
legacy_msg_id,
|
386
|
+
reply_to=reply_to,
|
387
|
+
when=when,
|
388
|
+
thread=thread,
|
389
|
+
**kwargs,
|
390
|
+
)
|
391
|
+
else:
|
392
|
+
return None
|
393
|
+
|
394
|
+
async def send_files(
|
395
|
+
self,
|
396
|
+
attachments: Collection[LegacyAttachment],
|
397
|
+
legacy_msg_id: Optional[LegacyMessageType] = None,
|
398
|
+
body: Optional[str] = None,
|
399
|
+
*,
|
400
|
+
reply_to: Optional[MessageReference] = None,
|
401
|
+
when: Optional[datetime] = None,
|
402
|
+
thread: Optional[LegacyThreadType] = None,
|
403
|
+
body_first=False,
|
404
|
+
correction=False,
|
405
|
+
correction_event_id: Optional[LegacyMessageType] = None,
|
406
|
+
**kwargs,
|
407
|
+
):
|
408
|
+
# TODO: once the epic XEP-0385 vs XEP-0447 battle is over, pick
|
409
|
+
# one and stop sending several attachments this way
|
410
|
+
# we attach the legacy_message ID to the last message we send, because
|
411
|
+
# we don't want several messages with the same ID (especially for MUC MAM)
|
412
|
+
# TODO: refactor this so we limit the number of SQL calls, ie, if
|
413
|
+
# the legacy file ID is known, only fetch the row once, and if it
|
414
|
+
# is new, write it all in a single call
|
415
|
+
if not attachments and not body:
|
416
|
+
# ignoring empty message
|
417
|
+
return
|
418
|
+
send_body = functools.partial(
|
419
|
+
self.__send_body,
|
420
|
+
body=body,
|
421
|
+
reply_to=reply_to,
|
422
|
+
when=when,
|
423
|
+
thread=thread,
|
424
|
+
correction=correction,
|
425
|
+
legacy_msg_id=legacy_msg_id,
|
426
|
+
correction_event_id=correction_event_id,
|
427
|
+
**kwargs,
|
428
|
+
)
|
429
|
+
all_msgs = []
|
430
|
+
if body_first:
|
431
|
+
all_msgs.append(send_body())
|
432
|
+
last_attachment_i = len(attachments) - 1
|
433
|
+
for i, attachment in enumerate(attachments):
|
434
|
+
last = i == last_attachment_i
|
435
|
+
if last and not body:
|
436
|
+
legacy = legacy_msg_id
|
437
|
+
else:
|
438
|
+
legacy = None
|
439
|
+
_url, msgs = await self.send_file(
|
440
|
+
file_path=attachment.path,
|
441
|
+
legacy_msg_id=legacy,
|
442
|
+
file_url=attachment.url,
|
443
|
+
data_stream=attachment.stream,
|
444
|
+
data=attachment.data,
|
445
|
+
reply_to=reply_to,
|
446
|
+
when=when,
|
447
|
+
thread=thread,
|
448
|
+
file_name=attachment.name,
|
449
|
+
content_type=attachment.content_type,
|
450
|
+
legacy_file_id=attachment.legacy_file_id,
|
451
|
+
caption=attachment.caption,
|
452
|
+
store_multi=False,
|
453
|
+
**kwargs,
|
454
|
+
)
|
455
|
+
all_msgs.extend(msgs)
|
456
|
+
if not body_first:
|
457
|
+
all_msgs.append(send_body())
|
458
|
+
self.__store_multi(legacy_msg_id, all_msgs)
|
459
|
+
|
460
|
+
def __store_multi(
|
461
|
+
self,
|
462
|
+
legacy_msg_id: Optional[LegacyMessageType],
|
463
|
+
all_msgs: Sequence[Optional[Message]],
|
464
|
+
):
|
465
|
+
if legacy_msg_id is None:
|
466
|
+
return
|
467
|
+
ids = []
|
468
|
+
for msg in all_msgs:
|
469
|
+
if not msg:
|
470
|
+
continue
|
471
|
+
if stanza_id := msg.get_plugin("stanza_id", check=True):
|
472
|
+
ids.append(stanza_id["id"])
|
473
|
+
else:
|
474
|
+
ids.append(msg.get_id())
|
475
|
+
db.attachment_store_legacy_to_multi_xmpp_msg_ids(legacy_msg_id, ids)
|
476
|
+
|
477
|
+
|
478
|
+
def get_blurhash(path: Path, n=9) -> tuple[str, int, int]:
|
479
|
+
img = Image.open(path)
|
480
|
+
width, height = img.size
|
481
|
+
n = min(width, height, n)
|
482
|
+
if width == height:
|
483
|
+
x = y = n
|
484
|
+
elif width > height:
|
485
|
+
x = n
|
486
|
+
y = round(n * height / width)
|
487
|
+
else:
|
488
|
+
x = round(n * width / height)
|
489
|
+
y = n
|
490
|
+
# There are 2 blurhash-python packages:
|
491
|
+
# https://github.com/woltapp/blurhash-python
|
492
|
+
# https://github.com/halcy/blurhash-python
|
493
|
+
# With this hack we're compatible with both, which is useful for packaging
|
494
|
+
# without using pyproject.toml, as most distro do
|
495
|
+
try:
|
496
|
+
hash_ = blurhash.encode(img, x, y)
|
497
|
+
except TypeError:
|
498
|
+
# We are using halcy's blurhash which expects
|
499
|
+
# the 1st argument to be a 3-dimensional array
|
500
|
+
import numpy # type:ignore
|
501
|
+
|
502
|
+
hash_ = blurhash.encode(numpy.array(img.convert("RGB")), x, y)
|
503
|
+
return hash_, width, height
|
504
|
+
|
505
|
+
|
506
|
+
log = logging.getLogger(__name__)
|