slidge 0.1.2__py3-none-any.whl → 0.2.0a0__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 +3 -5
- slidge/__main__.py +2 -196
- slidge/__version__.py +5 -0
- slidge/command/adhoc.py +8 -1
- slidge/command/admin.py +5 -6
- slidge/command/base.py +1 -2
- slidge/command/register.py +32 -16
- slidge/command/user.py +85 -5
- slidge/contact/contact.py +93 -31
- slidge/contact/roster.py +54 -39
- slidge/core/config.py +13 -7
- slidge/core/gateway/base.py +139 -34
- slidge/core/gateway/disco.py +2 -4
- slidge/core/gateway/mam.py +1 -4
- slidge/core/gateway/ping.py +2 -3
- slidge/core/gateway/presence.py +1 -1
- slidge/core/gateway/registration.py +32 -21
- slidge/core/gateway/search.py +3 -5
- slidge/core/gateway/session_dispatcher.py +109 -51
- slidge/core/gateway/vcard_temp.py +6 -4
- slidge/core/mixins/__init__.py +11 -1
- slidge/core/mixins/attachment.py +15 -10
- slidge/core/mixins/avatar.py +66 -18
- slidge/core/mixins/base.py +8 -2
- slidge/core/mixins/message.py +11 -7
- slidge/core/mixins/message_maker.py +17 -9
- slidge/core/mixins/presence.py +14 -4
- slidge/core/pubsub.py +54 -212
- slidge/core/session.py +65 -33
- slidge/db/__init__.py +4 -0
- slidge/db/alembic/env.py +64 -0
- slidge/db/alembic/script.py.mako +26 -0
- slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
- slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
- slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +133 -0
- slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +76 -0
- slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
- slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
- slidge/db/avatar.py +224 -0
- slidge/db/meta.py +65 -0
- slidge/db/models.py +365 -0
- slidge/db/store.py +976 -0
- slidge/group/archive.py +13 -14
- slidge/group/bookmarks.py +59 -56
- slidge/group/participant.py +81 -29
- slidge/group/room.py +242 -142
- slidge/main.py +201 -0
- slidge/migration.py +30 -0
- slidge/slixfix/__init__.py +35 -2
- slidge/slixfix/roster.py +11 -4
- slidge/slixfix/xep_0292/vcard4.py +1 -0
- slidge/util/db.py +1 -47
- slidge/util/test.py +21 -4
- slidge/util/types.py +24 -4
- {slidge-0.1.2.dist-info → slidge-0.2.0a0.dist-info}/METADATA +3 -1
- slidge-0.2.0a0.dist-info/RECORD +108 -0
- slidge/core/cache.py +0 -183
- slidge/util/schema.sql +0 -126
- slidge/util/sql.py +0 -508
- slidge-0.1.2.dist-info/RECORD +0 -96
- {slidge-0.1.2.dist-info → slidge-0.2.0a0.dist-info}/LICENSE +0 -0
- {slidge-0.1.2.dist-info → slidge-0.2.0a0.dist-info}/WHEEL +0 -0
- {slidge-0.1.2.dist-info → slidge-0.2.0a0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,214 @@
|
|
1
|
+
"""Move everything to persistent DB
|
2
|
+
|
3
|
+
Revision ID: b33993e87db3
|
4
|
+
Revises: e91195719c2c
|
5
|
+
Create Date: 2024-06-25 16:09:36.663953
|
6
|
+
|
7
|
+
"""
|
8
|
+
|
9
|
+
import shutil
|
10
|
+
from typing import Sequence, Union
|
11
|
+
|
12
|
+
import sqlalchemy as sa
|
13
|
+
from alembic import op
|
14
|
+
|
15
|
+
import slidge.db.meta
|
16
|
+
from slidge import global_config
|
17
|
+
|
18
|
+
# revision identifiers, used by Alembic.
|
19
|
+
revision: str = "b33993e87db3"
|
20
|
+
down_revision: Union[str, None] = "e91195719c2c"
|
21
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
22
|
+
depends_on: Union[str, Sequence[str], None] = None
|
23
|
+
|
24
|
+
|
25
|
+
def upgrade() -> None:
|
26
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
27
|
+
op.create_table(
|
28
|
+
"avatar",
|
29
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
30
|
+
sa.Column("filename", sa.String(), nullable=False),
|
31
|
+
sa.Column("hash", sa.String(), nullable=False),
|
32
|
+
sa.Column("height", sa.Integer(), nullable=False),
|
33
|
+
sa.Column("width", sa.Integer(), nullable=False),
|
34
|
+
sa.Column("legacy_id", sa.String(), nullable=True),
|
35
|
+
sa.Column("url", sa.String(), nullable=True),
|
36
|
+
sa.Column("etag", sa.String(), nullable=True),
|
37
|
+
sa.Column("last_modified", sa.String(), nullable=True),
|
38
|
+
sa.PrimaryKeyConstraint("id"),
|
39
|
+
sa.UniqueConstraint("filename"),
|
40
|
+
sa.UniqueConstraint("hash"),
|
41
|
+
sa.UniqueConstraint("legacy_id"),
|
42
|
+
)
|
43
|
+
op.create_table(
|
44
|
+
"attachment",
|
45
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
46
|
+
sa.Column("user_account_id", sa.Integer(), nullable=False),
|
47
|
+
sa.Column("legacy_file_id", sa.String(), nullable=True),
|
48
|
+
sa.Column("url", sa.String(), nullable=False),
|
49
|
+
sa.Column("sims", sa.String(), nullable=True),
|
50
|
+
sa.Column("sfs", sa.String(), nullable=True),
|
51
|
+
sa.ForeignKeyConstraint(
|
52
|
+
["user_account_id"],
|
53
|
+
["user_account.id"],
|
54
|
+
),
|
55
|
+
sa.PrimaryKeyConstraint("id"),
|
56
|
+
)
|
57
|
+
op.create_index(
|
58
|
+
op.f("ix_attachment_legacy_file_id"),
|
59
|
+
"attachment",
|
60
|
+
["legacy_file_id"],
|
61
|
+
unique=False,
|
62
|
+
)
|
63
|
+
op.create_index(op.f("ix_attachment_url"), "attachment", ["url"], unique=False)
|
64
|
+
op.create_table(
|
65
|
+
"contact",
|
66
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
67
|
+
sa.Column("user_account_id", sa.Integer(), nullable=False),
|
68
|
+
sa.Column("legacy_id", sa.String(), nullable=False),
|
69
|
+
sa.Column("jid", slidge.db.meta.JIDType(), nullable=False),
|
70
|
+
sa.Column("avatar_id", sa.Integer(), nullable=True),
|
71
|
+
sa.Column("nick", sa.String(), nullable=True),
|
72
|
+
sa.Column("cached_presence", sa.Boolean(), nullable=False),
|
73
|
+
sa.Column("last_seen", sa.DateTime(), nullable=True),
|
74
|
+
sa.Column("ptype", sa.String(), nullable=True),
|
75
|
+
sa.Column("pstatus", sa.String(), nullable=True),
|
76
|
+
sa.Column("pshow", sa.String(), nullable=True),
|
77
|
+
sa.ForeignKeyConstraint(
|
78
|
+
["avatar_id"],
|
79
|
+
["avatar.id"],
|
80
|
+
),
|
81
|
+
sa.ForeignKeyConstraint(
|
82
|
+
["user_account_id"],
|
83
|
+
["user_account.id"],
|
84
|
+
),
|
85
|
+
sa.PrimaryKeyConstraint("id"),
|
86
|
+
sa.UniqueConstraint("user_account_id", "jid"),
|
87
|
+
sa.UniqueConstraint("user_account_id", "legacy_id"),
|
88
|
+
)
|
89
|
+
op.create_table(
|
90
|
+
"legacy_ids_multi",
|
91
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
92
|
+
sa.Column("user_account_id", sa.Integer(), nullable=False),
|
93
|
+
sa.Column("legacy_id", sa.String(), nullable=False),
|
94
|
+
sa.ForeignKeyConstraint(
|
95
|
+
["user_account_id"],
|
96
|
+
["user_account.id"],
|
97
|
+
),
|
98
|
+
sa.PrimaryKeyConstraint("id"),
|
99
|
+
)
|
100
|
+
op.create_index(
|
101
|
+
"legacy_ids_multi_user_account_id_legacy_id",
|
102
|
+
"legacy_ids_multi",
|
103
|
+
["user_account_id", "legacy_id"],
|
104
|
+
unique=True,
|
105
|
+
)
|
106
|
+
op.create_table(
|
107
|
+
"room",
|
108
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
109
|
+
sa.Column("user_account_id", sa.Integer(), nullable=False),
|
110
|
+
sa.Column("legacy_id", sa.String(), nullable=False),
|
111
|
+
sa.Column("jid", slidge.db.meta.JIDType(), nullable=False),
|
112
|
+
sa.Column("avatar_id", sa.Integer(), nullable=True),
|
113
|
+
sa.Column("name", sa.String(), nullable=True),
|
114
|
+
sa.ForeignKeyConstraint(
|
115
|
+
["avatar_id"],
|
116
|
+
["avatar.id"],
|
117
|
+
),
|
118
|
+
sa.ForeignKeyConstraint(
|
119
|
+
["user_account_id"],
|
120
|
+
["user_account.id"],
|
121
|
+
),
|
122
|
+
sa.PrimaryKeyConstraint("id"),
|
123
|
+
sa.UniqueConstraint("jid"),
|
124
|
+
sa.UniqueConstraint("legacy_id"),
|
125
|
+
)
|
126
|
+
op.create_table(
|
127
|
+
"xmpp_to_legacy_ids",
|
128
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
129
|
+
sa.Column("user_account_id", sa.Integer(), nullable=False),
|
130
|
+
sa.Column("xmpp_id", sa.String(), nullable=False),
|
131
|
+
sa.Column("legacy_id", sa.String(), nullable=False),
|
132
|
+
sa.Column(
|
133
|
+
"type",
|
134
|
+
sa.Enum("DM", "GROUP_CHAT", "THREAD", name="xmpptolegacyenum"),
|
135
|
+
nullable=False,
|
136
|
+
),
|
137
|
+
sa.ForeignKeyConstraint(
|
138
|
+
["user_account_id"],
|
139
|
+
["user_account.id"],
|
140
|
+
),
|
141
|
+
sa.PrimaryKeyConstraint("id"),
|
142
|
+
)
|
143
|
+
op.create_index(
|
144
|
+
"xmpp_legacy",
|
145
|
+
"xmpp_to_legacy_ids",
|
146
|
+
["user_account_id", "xmpp_id", "legacy_id"],
|
147
|
+
unique=True,
|
148
|
+
)
|
149
|
+
op.create_table(
|
150
|
+
"mam",
|
151
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
152
|
+
sa.Column("room_id", sa.Integer(), nullable=False),
|
153
|
+
sa.Column("stanza_id", sa.String(), nullable=False),
|
154
|
+
sa.Column("timestamp", sa.DateTime(), nullable=False),
|
155
|
+
sa.Column("author_jid", slidge.db.meta.JIDType(), nullable=False),
|
156
|
+
sa.Column("stanza", sa.String(), nullable=False),
|
157
|
+
sa.ForeignKeyConstraint(
|
158
|
+
["room_id"],
|
159
|
+
["room.id"],
|
160
|
+
),
|
161
|
+
sa.PrimaryKeyConstraint("id"),
|
162
|
+
sa.UniqueConstraint("room_id", "stanza_id"),
|
163
|
+
)
|
164
|
+
op.create_table(
|
165
|
+
"xmpp_ids_multi",
|
166
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
167
|
+
sa.Column("user_account_id", sa.Integer(), nullable=False),
|
168
|
+
sa.Column("xmpp_id", sa.String(), nullable=False),
|
169
|
+
sa.Column("legacy_ids_multi_id", sa.Integer(), nullable=False),
|
170
|
+
sa.ForeignKeyConstraint(
|
171
|
+
["legacy_ids_multi_id"],
|
172
|
+
["legacy_ids_multi.id"],
|
173
|
+
),
|
174
|
+
sa.ForeignKeyConstraint(
|
175
|
+
["user_account_id"],
|
176
|
+
["user_account.id"],
|
177
|
+
),
|
178
|
+
sa.PrimaryKeyConstraint("id"),
|
179
|
+
)
|
180
|
+
op.create_index(
|
181
|
+
"legacy_ids_multi_user_account_id_xmpp_id",
|
182
|
+
"xmpp_ids_multi",
|
183
|
+
["user_account_id", "xmpp_id"],
|
184
|
+
unique=True,
|
185
|
+
)
|
186
|
+
|
187
|
+
try:
|
188
|
+
shutil.rmtree(global_config.HOME_DIR / "slidge_avatars_v2")
|
189
|
+
except (FileNotFoundError, AttributeError):
|
190
|
+
pass
|
191
|
+
|
192
|
+
# ### end Alembic commands ###
|
193
|
+
|
194
|
+
|
195
|
+
def downgrade() -> None:
|
196
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
197
|
+
op.drop_index(
|
198
|
+
"legacy_ids_multi_user_account_id_xmpp_id", table_name="xmpp_ids_multi"
|
199
|
+
)
|
200
|
+
op.drop_table("xmpp_ids_multi")
|
201
|
+
op.drop_table("mam")
|
202
|
+
op.drop_index("xmpp_legacy", table_name="xmpp_to_legacy_ids")
|
203
|
+
op.drop_table("xmpp_to_legacy_ids")
|
204
|
+
op.drop_table("room")
|
205
|
+
op.drop_index(
|
206
|
+
"legacy_ids_multi_user_account_id_legacy_id", table_name="legacy_ids_multi"
|
207
|
+
)
|
208
|
+
op.drop_table("legacy_ids_multi")
|
209
|
+
op.drop_table("contact")
|
210
|
+
op.drop_index(op.f("ix_attachment_url"), table_name="attachment")
|
211
|
+
op.drop_index(op.f("ix_attachment_legacy_file_id"), table_name="attachment")
|
212
|
+
op.drop_table("attachment")
|
213
|
+
op.drop_table("avatar")
|
214
|
+
# ### end Alembic commands ###
|
@@ -0,0 +1,26 @@
|
|
1
|
+
"""Store users' avatars' hashes persistently
|
2
|
+
|
3
|
+
Revision ID: e91195719c2c
|
4
|
+
Revises: aa9d82a7f6ef
|
5
|
+
Create Date: 2024-06-01 14:14:51.984943
|
6
|
+
|
7
|
+
"""
|
8
|
+
|
9
|
+
from typing import Sequence, Union
|
10
|
+
|
11
|
+
import sqlalchemy as sa
|
12
|
+
from alembic import op
|
13
|
+
|
14
|
+
# revision identifiers, used by Alembic.
|
15
|
+
revision: str = "e91195719c2c"
|
16
|
+
down_revision: Union[str, None] = "aa9d82a7f6ef"
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
18
|
+
depends_on: Union[str, Sequence[str], None] = None
|
19
|
+
|
20
|
+
|
21
|
+
def upgrade() -> None:
|
22
|
+
op.add_column("user_account", sa.Column("avatar_hash", sa.String(), nullable=True))
|
23
|
+
|
24
|
+
|
25
|
+
def downgrade() -> None:
|
26
|
+
op.drop_column("user_account", "avatar_hash")
|
slidge/db/avatar.py
ADDED
@@ -0,0 +1,224 @@
|
|
1
|
+
import asyncio
|
2
|
+
import hashlib
|
3
|
+
import io
|
4
|
+
import logging
|
5
|
+
import uuid
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
7
|
+
from dataclasses import dataclass
|
8
|
+
from http import HTTPStatus
|
9
|
+
from pathlib import Path
|
10
|
+
from typing import Any, Callable, Optional
|
11
|
+
|
12
|
+
import aiohttp
|
13
|
+
from multidict import CIMultiDictProxy
|
14
|
+
from PIL.Image import Image
|
15
|
+
from PIL.Image import open as open_image
|
16
|
+
from sqlalchemy import select
|
17
|
+
|
18
|
+
from slidge.core import config
|
19
|
+
from slidge.db.models import Avatar
|
20
|
+
from slidge.db.store import AvatarStore
|
21
|
+
from slidge.util.types import URL, AvatarType, LegacyFileIdType
|
22
|
+
|
23
|
+
|
24
|
+
@dataclass
|
25
|
+
class CachedAvatar:
|
26
|
+
pk: int
|
27
|
+
filename: str
|
28
|
+
hash: str
|
29
|
+
height: int
|
30
|
+
width: int
|
31
|
+
root: Path
|
32
|
+
etag: Optional[str] = None
|
33
|
+
last_modified: Optional[str] = None
|
34
|
+
|
35
|
+
@property
|
36
|
+
def data(self):
|
37
|
+
return self.path.read_bytes()
|
38
|
+
|
39
|
+
@property
|
40
|
+
def path(self):
|
41
|
+
return self.root / self.filename
|
42
|
+
|
43
|
+
@staticmethod
|
44
|
+
def from_store(stored: Avatar, root_dir: Path):
|
45
|
+
return CachedAvatar(
|
46
|
+
pk=stored.id,
|
47
|
+
filename=stored.filename,
|
48
|
+
hash=stored.hash,
|
49
|
+
height=stored.height,
|
50
|
+
width=stored.width,
|
51
|
+
etag=stored.etag,
|
52
|
+
root=root_dir,
|
53
|
+
last_modified=stored.last_modified,
|
54
|
+
)
|
55
|
+
|
56
|
+
|
57
|
+
class NotModified(Exception):
|
58
|
+
pass
|
59
|
+
|
60
|
+
|
61
|
+
class AvatarCache:
|
62
|
+
dir: Path
|
63
|
+
http: aiohttp.ClientSession
|
64
|
+
store: AvatarStore
|
65
|
+
legacy_avatar_type: Callable[[str], Any] = str
|
66
|
+
|
67
|
+
def __init__(self):
|
68
|
+
self._thread_pool = ThreadPoolExecutor(config.AVATAR_RESAMPLING_THREADS)
|
69
|
+
|
70
|
+
def set_dir(self, path: Path):
|
71
|
+
self.dir = path
|
72
|
+
self.dir.mkdir(exist_ok=True)
|
73
|
+
|
74
|
+
def close(self):
|
75
|
+
self._thread_pool.shutdown(cancel_futures=True)
|
76
|
+
|
77
|
+
def __get_http_headers(self, cached: Optional[CachedAvatar | Avatar]):
|
78
|
+
headers = {}
|
79
|
+
if cached and (self.dir / cached.filename).exists():
|
80
|
+
if last_modified := cached.last_modified:
|
81
|
+
headers["If-Modified-Since"] = last_modified
|
82
|
+
if etag := cached.etag:
|
83
|
+
headers["If-None-Match"] = etag
|
84
|
+
return headers
|
85
|
+
|
86
|
+
async def __download(
|
87
|
+
self,
|
88
|
+
url: str,
|
89
|
+
headers: dict[str, str],
|
90
|
+
) -> tuple[Image, CIMultiDictProxy[str]]:
|
91
|
+
async with self.http.get(url, headers=headers) as response:
|
92
|
+
if response.status == HTTPStatus.NOT_MODIFIED:
|
93
|
+
log.debug("Using avatar cache for %s", url)
|
94
|
+
raise NotModified
|
95
|
+
return (
|
96
|
+
open_image(io.BytesIO(await response.read())),
|
97
|
+
response.headers,
|
98
|
+
)
|
99
|
+
|
100
|
+
async def __is_modified(self, url, headers) -> bool:
|
101
|
+
async with self.http.head(url, headers=headers) as response:
|
102
|
+
return response.status != HTTPStatus.NOT_MODIFIED
|
103
|
+
|
104
|
+
async def url_modified(self, url: URL) -> bool:
|
105
|
+
cached = self.store.get_by_url(url)
|
106
|
+
if cached is None:
|
107
|
+
return True
|
108
|
+
headers = self.__get_http_headers(cached)
|
109
|
+
return await self.__is_modified(url, headers)
|
110
|
+
|
111
|
+
def get(self, unique_id: LegacyFileIdType | URL) -> Optional[CachedAvatar]:
|
112
|
+
if isinstance(unique_id, URL):
|
113
|
+
stored = self.store.get_by_url(unique_id)
|
114
|
+
else:
|
115
|
+
stored = self.store.get_by_legacy_id(str(unique_id))
|
116
|
+
if stored is None:
|
117
|
+
return None
|
118
|
+
return CachedAvatar.from_store(stored, self.dir)
|
119
|
+
|
120
|
+
def get_by_pk(self, pk: int) -> CachedAvatar:
|
121
|
+
stored = self.store.get_by_pk(pk)
|
122
|
+
assert stored is not None
|
123
|
+
return CachedAvatar.from_store(stored, self.dir)
|
124
|
+
|
125
|
+
@staticmethod
|
126
|
+
async def _get_image(avatar: AvatarType) -> Image:
|
127
|
+
if isinstance(avatar, bytes):
|
128
|
+
return open_image(io.BytesIO(avatar))
|
129
|
+
elif isinstance(avatar, Path):
|
130
|
+
return open_image(avatar)
|
131
|
+
raise TypeError("Avatar must be bytes or a Path", avatar)
|
132
|
+
|
133
|
+
async def convert_or_get(
|
134
|
+
self,
|
135
|
+
avatar: AvatarType,
|
136
|
+
unique_id: Optional[LegacyFileIdType],
|
137
|
+
) -> CachedAvatar:
|
138
|
+
if unique_id is not None:
|
139
|
+
cached = self.get(str(unique_id))
|
140
|
+
if cached is not None:
|
141
|
+
return cached
|
142
|
+
|
143
|
+
if isinstance(avatar, (URL, str)):
|
144
|
+
if unique_id is None:
|
145
|
+
stored = self.store.get_by_url(avatar)
|
146
|
+
try:
|
147
|
+
img, response_headers = await self.__download(
|
148
|
+
avatar, self.__get_http_headers(stored)
|
149
|
+
)
|
150
|
+
except NotModified:
|
151
|
+
assert stored is not None
|
152
|
+
return CachedAvatar.from_store(stored, self.dir)
|
153
|
+
|
154
|
+
else:
|
155
|
+
img, _ = await self.__download(avatar, {})
|
156
|
+
response_headers = None
|
157
|
+
else:
|
158
|
+
img = await self._get_image(avatar)
|
159
|
+
response_headers = None
|
160
|
+
with self.store.session() as orm:
|
161
|
+
stored = orm.execute(
|
162
|
+
select(Avatar).where(Avatar.legacy_id == str(unique_id))
|
163
|
+
).scalar()
|
164
|
+
if stored is not None and stored.url is None:
|
165
|
+
return CachedAvatar.from_store(stored, self.dir)
|
166
|
+
|
167
|
+
resize = (size := config.AVATAR_SIZE) and any(x > size for x in img.size)
|
168
|
+
if resize:
|
169
|
+
await asyncio.get_event_loop().run_in_executor(
|
170
|
+
self._thread_pool, img.thumbnail, (size, size)
|
171
|
+
)
|
172
|
+
log.debug("Resampled image to %s", img.size)
|
173
|
+
|
174
|
+
filename = str(uuid.uuid1()) + ".png"
|
175
|
+
file_path = self.dir / filename
|
176
|
+
|
177
|
+
if (
|
178
|
+
not resize
|
179
|
+
and img.format == "PNG"
|
180
|
+
and isinstance(unique_id, str)
|
181
|
+
and (path := Path(unique_id))
|
182
|
+
and path.exists()
|
183
|
+
):
|
184
|
+
img_bytes = path.read_bytes()
|
185
|
+
else:
|
186
|
+
with io.BytesIO() as f:
|
187
|
+
img.save(f, format="PNG")
|
188
|
+
img_bytes = f.getvalue()
|
189
|
+
|
190
|
+
with file_path.open("wb") as file:
|
191
|
+
file.write(img_bytes)
|
192
|
+
|
193
|
+
hash_ = hashlib.sha1(img_bytes).hexdigest()
|
194
|
+
|
195
|
+
stored = orm.execute(select(Avatar).where(Avatar.hash == hash_)).scalar()
|
196
|
+
|
197
|
+
if stored is not None:
|
198
|
+
return CachedAvatar.from_store(stored, self.dir)
|
199
|
+
|
200
|
+
stored = Avatar(
|
201
|
+
filename=filename,
|
202
|
+
hash=hash_,
|
203
|
+
height=img.height,
|
204
|
+
width=img.width,
|
205
|
+
legacy_id=None if unique_id is None else str(unique_id),
|
206
|
+
url=avatar if isinstance(avatar, (URL, str)) else None,
|
207
|
+
)
|
208
|
+
if response_headers:
|
209
|
+
stored.etag = response_headers.get("etag")
|
210
|
+
stored.last_modified = response_headers.get("last-modified")
|
211
|
+
|
212
|
+
orm.add(stored)
|
213
|
+
orm.commit()
|
214
|
+
return CachedAvatar.from_store(stored, self.dir)
|
215
|
+
|
216
|
+
|
217
|
+
avatar_cache = AvatarCache()
|
218
|
+
log = logging.getLogger(__name__)
|
219
|
+
_download_lock = asyncio.Lock()
|
220
|
+
|
221
|
+
__all__ = (
|
222
|
+
"CachedAvatar",
|
223
|
+
"avatar_cache",
|
224
|
+
)
|
slidge/db/meta.py
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import json
|
4
|
+
from typing import Union
|
5
|
+
|
6
|
+
import sqlalchemy as sa
|
7
|
+
from slixmpp import JID
|
8
|
+
|
9
|
+
|
10
|
+
class JIDType(sa.TypeDecorator[JID]):
|
11
|
+
"""
|
12
|
+
Custom SQLAlchemy type for JIDs
|
13
|
+
"""
|
14
|
+
|
15
|
+
impl = sa.types.TEXT
|
16
|
+
cache_ok = True
|
17
|
+
|
18
|
+
def process_bind_param(self, value: JID | None, dialect: sa.Dialect) -> str | None:
|
19
|
+
if value is None:
|
20
|
+
return value
|
21
|
+
return str(value)
|
22
|
+
|
23
|
+
def process_result_value(
|
24
|
+
self, value: str | None, dialect: sa.Dialect
|
25
|
+
) -> JID | None:
|
26
|
+
if value is None:
|
27
|
+
return value
|
28
|
+
return JID(value)
|
29
|
+
|
30
|
+
|
31
|
+
class JSONEncodedDict(sa.TypeDecorator):
|
32
|
+
"""
|
33
|
+
Custom SQLAlchemy type for dictionaries stored as JSON
|
34
|
+
|
35
|
+
Note that mutations of the dictionary are not detected by SQLAlchemy,
|
36
|
+
which is why use ``attributes.flag_modified()`` in ``UserStore.update()``
|
37
|
+
"""
|
38
|
+
|
39
|
+
impl = sa.VARCHAR
|
40
|
+
|
41
|
+
cache_ok = True
|
42
|
+
|
43
|
+
def process_bind_param(self, value, dialect):
|
44
|
+
if value is not None:
|
45
|
+
value = json.dumps(value)
|
46
|
+
|
47
|
+
return value
|
48
|
+
|
49
|
+
def process_result_value(self, value, dialect):
|
50
|
+
if value is not None:
|
51
|
+
value = json.loads(value)
|
52
|
+
return value
|
53
|
+
|
54
|
+
|
55
|
+
JSONSerializableTypes = Union[str, float, None, "JSONSerializable"]
|
56
|
+
JSONSerializable = dict[str, JSONSerializableTypes]
|
57
|
+
|
58
|
+
|
59
|
+
class Base(sa.orm.DeclarativeBase):
|
60
|
+
type_annotation_map = {JSONSerializable: JSONEncodedDict, JID: JIDType}
|
61
|
+
|
62
|
+
|
63
|
+
def get_engine(path: str) -> sa.Engine:
|
64
|
+
engine = sa.create_engine(path)
|
65
|
+
return engine
|