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
slidge/core/cache.py
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
import asyncio
|
2
|
+
import hashlib
|
3
|
+
import io
|
4
|
+
import logging
|
5
|
+
import shelve
|
6
|
+
import uuid
|
7
|
+
from concurrent.futures import ThreadPoolExecutor
|
8
|
+
from dataclasses import dataclass
|
9
|
+
from http import HTTPStatus
|
10
|
+
from pathlib import Path
|
11
|
+
from typing import Optional
|
12
|
+
|
13
|
+
import aiohttp
|
14
|
+
from multidict import CIMultiDictProxy
|
15
|
+
from PIL import Image
|
16
|
+
from slixmpp import JID
|
17
|
+
|
18
|
+
from ..util.types import URL, LegacyFileIdType
|
19
|
+
from . import config
|
20
|
+
|
21
|
+
|
22
|
+
@dataclass
|
23
|
+
class CachedAvatar:
|
24
|
+
filename: str
|
25
|
+
hash: str
|
26
|
+
height: int
|
27
|
+
width: int
|
28
|
+
root: Path
|
29
|
+
etag: Optional[str] = None
|
30
|
+
last_modified: Optional[str] = None
|
31
|
+
|
32
|
+
@property
|
33
|
+
def data(self):
|
34
|
+
return self.path.read_bytes()
|
35
|
+
|
36
|
+
@property
|
37
|
+
def path(self):
|
38
|
+
return self.root / self.filename
|
39
|
+
|
40
|
+
|
41
|
+
class AvatarCache:
|
42
|
+
_shelf_path: str
|
43
|
+
_jid_to_legacy_path: str
|
44
|
+
dir: Path
|
45
|
+
http: aiohttp.ClientSession
|
46
|
+
|
47
|
+
def __init__(self):
|
48
|
+
self._thread_pool = ThreadPoolExecutor(config.AVATAR_RESAMPLING_THREADS)
|
49
|
+
|
50
|
+
def set_dir(self, path: Path):
|
51
|
+
self.dir = path
|
52
|
+
self.dir.mkdir(exist_ok=True)
|
53
|
+
self._shelf_path = str(path / "slidge_avatar_cache.shelf")
|
54
|
+
self._jid_to_legacy_path = str(path / "jid_to_avatar_unique_id.shelf")
|
55
|
+
|
56
|
+
def close(self):
|
57
|
+
self._thread_pool.shutdown(cancel_futures=True)
|
58
|
+
|
59
|
+
def __get_http_headers(self, cached: Optional[CachedAvatar]):
|
60
|
+
headers = {}
|
61
|
+
if cached and (self.dir / cached.filename).exists():
|
62
|
+
if last_modified := cached.last_modified:
|
63
|
+
headers["If-Modified-Since"] = last_modified
|
64
|
+
if etag := cached.etag:
|
65
|
+
headers["If-None-Match"] = etag
|
66
|
+
return headers
|
67
|
+
|
68
|
+
async def get_avatar_from_url_alone(self, url: str, jid: JID):
|
69
|
+
"""
|
70
|
+
Used when no avatar unique ID is passed. Store and use http headers
|
71
|
+
to avoid fetching ut
|
72
|
+
"""
|
73
|
+
cached = self.get(url)
|
74
|
+
headers = self.__get_http_headers(cached)
|
75
|
+
async with _download_lock:
|
76
|
+
return await self.__download(cached, url, headers, jid)
|
77
|
+
|
78
|
+
async def __download(
|
79
|
+
self,
|
80
|
+
cached: Optional[CachedAvatar],
|
81
|
+
url: str,
|
82
|
+
headers: dict[str, str],
|
83
|
+
jid: JID,
|
84
|
+
):
|
85
|
+
async with self.http.get(url, headers=headers) as response:
|
86
|
+
if response.status == HTTPStatus.NOT_MODIFIED:
|
87
|
+
log.debug("Using avatar cache for %s", jid)
|
88
|
+
return cached
|
89
|
+
log.debug("Download avatar for %s", jid)
|
90
|
+
return await self.convert_and_store(
|
91
|
+
Image.open(io.BytesIO(await response.read())),
|
92
|
+
url,
|
93
|
+
jid,
|
94
|
+
response.headers,
|
95
|
+
)
|
96
|
+
|
97
|
+
async def url_has_changed(self, url: URL):
|
98
|
+
with shelve.open(self._shelf_path) as s:
|
99
|
+
cached = s.get(url)
|
100
|
+
if cached is None:
|
101
|
+
return True
|
102
|
+
headers = self.__get_http_headers(cached)
|
103
|
+
async with self.http.head(url, headers=headers) as response:
|
104
|
+
return response.status != HTTPStatus.NOT_MODIFIED
|
105
|
+
|
106
|
+
def get(self, unique_id: LegacyFileIdType) -> Optional[CachedAvatar]:
|
107
|
+
with shelve.open(self._shelf_path) as s:
|
108
|
+
return s.get(str(unique_id))
|
109
|
+
|
110
|
+
def get_cached_id_for(self, jid: JID) -> Optional[LegacyFileIdType]:
|
111
|
+
with shelve.open(self._jid_to_legacy_path) as s:
|
112
|
+
return s.get(str(jid))
|
113
|
+
|
114
|
+
def store_jid(self, jid: JID, uid: LegacyFileIdType):
|
115
|
+
with shelve.open(self._jid_to_legacy_path) as s:
|
116
|
+
s[str(jid)] = uid
|
117
|
+
|
118
|
+
def delete_jid(self, jid: JID):
|
119
|
+
try:
|
120
|
+
with shelve.open(self._jid_to_legacy_path) as s:
|
121
|
+
del s[str(jid)]
|
122
|
+
except KeyError:
|
123
|
+
pass
|
124
|
+
|
125
|
+
async def convert_and_store(
|
126
|
+
self,
|
127
|
+
img: Image.Image,
|
128
|
+
unique_id: LegacyFileIdType,
|
129
|
+
jid: JID,
|
130
|
+
response_headers: Optional[CIMultiDictProxy[str]] = None,
|
131
|
+
) -> CachedAvatar:
|
132
|
+
resize = (size := config.AVATAR_SIZE) and any(x > size for x in img.size)
|
133
|
+
if resize:
|
134
|
+
await asyncio.get_event_loop().run_in_executor(
|
135
|
+
self._thread_pool, img.thumbnail, (size, size)
|
136
|
+
)
|
137
|
+
log.debug("Resampled image to %s", img.size)
|
138
|
+
|
139
|
+
filename = str(uuid.uuid1()) + ".png"
|
140
|
+
file_path = self.dir / filename
|
141
|
+
|
142
|
+
if (
|
143
|
+
not resize
|
144
|
+
and img.format == "PNG"
|
145
|
+
and isinstance(unique_id, str)
|
146
|
+
and (path := Path(unique_id))
|
147
|
+
and path.exists()
|
148
|
+
):
|
149
|
+
img_bytes = path.read_bytes()
|
150
|
+
else:
|
151
|
+
with io.BytesIO() as f:
|
152
|
+
img.save(f, format="PNG")
|
153
|
+
img_bytes = f.getvalue()
|
154
|
+
|
155
|
+
with file_path.open("wb") as file:
|
156
|
+
file.write(img_bytes)
|
157
|
+
|
158
|
+
hash_ = hashlib.sha1(img_bytes).hexdigest()
|
159
|
+
|
160
|
+
avatar = CachedAvatar(
|
161
|
+
filename=filename,
|
162
|
+
hash=hash_,
|
163
|
+
height=img.height,
|
164
|
+
width=img.width,
|
165
|
+
root=self.dir,
|
166
|
+
)
|
167
|
+
if response_headers:
|
168
|
+
avatar.etag = response_headers.get("etag")
|
169
|
+
avatar.last_modified = response_headers.get("last-modified")
|
170
|
+
with shelve.open(self._shelf_path) as s:
|
171
|
+
s[str(unique_id)] = avatar
|
172
|
+
self.store_jid(jid, unique_id)
|
173
|
+
return avatar
|
174
|
+
|
175
|
+
|
176
|
+
avatar_cache = AvatarCache()
|
177
|
+
log = logging.getLogger(__name__)
|
178
|
+
_download_lock = asyncio.Lock()
|
179
|
+
|
180
|
+
__all__ = (
|
181
|
+
"CachedAvatar",
|
182
|
+
"avatar_cache",
|
183
|
+
)
|
slidge/core/config.py
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
from datetime import timedelta
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
from slixmpp import JID as JIDType
|
6
|
+
|
7
|
+
|
8
|
+
class _TimedeltaSeconds(timedelta):
|
9
|
+
def __new__(cls, s: str):
|
10
|
+
return super().__new__(cls, seconds=int(s))
|
11
|
+
|
12
|
+
|
13
|
+
# REQUIRED, so not default value
|
14
|
+
|
15
|
+
LEGACY_MODULE: str
|
16
|
+
LEGACY_MODULE__DOC = (
|
17
|
+
"Importable python module containing (at least) "
|
18
|
+
"a BaseGateway and a LegacySession subclass"
|
19
|
+
)
|
20
|
+
|
21
|
+
SERVER: str = "localhost"
|
22
|
+
SERVER__DOC = (
|
23
|
+
"The XMPP server's host name. Defaults to localhost, which is the "
|
24
|
+
"standard way of running slidge, on the same host as the XMPP server. "
|
25
|
+
"The 'Jabber Component Protocol' (XEP-0114) does not mention encryption, "
|
26
|
+
"so you *should* provide encryption another way, eg via port forwarding, if "
|
27
|
+
"you change this."
|
28
|
+
)
|
29
|
+
SERVER__SHORT = "s"
|
30
|
+
|
31
|
+
SECRET: str
|
32
|
+
SECRET__DOC = "The gateway component's secret (required to connect to the XMPP server)"
|
33
|
+
|
34
|
+
JID: JIDType
|
35
|
+
JID__DOC = "The gateway component's JID"
|
36
|
+
JID__SHORT = "j"
|
37
|
+
|
38
|
+
PORT: str = "5347"
|
39
|
+
PORT__DOC = "The XMPP server's port for incoming component connections"
|
40
|
+
PORT__SHORT = "p"
|
41
|
+
|
42
|
+
# Dynamic default (depends on other values)
|
43
|
+
|
44
|
+
HOME_DIR: Path
|
45
|
+
HOME_DIR__DOC = (
|
46
|
+
"Shelve file used to store persistent user data. "
|
47
|
+
"Defaults to /var/lib/slidge/${SLIDGE_JID}. "
|
48
|
+
)
|
49
|
+
HOME_DIR__DYNAMIC_DEFAULT = True
|
50
|
+
|
51
|
+
USER_JID_VALIDATOR: str
|
52
|
+
USER_JID_VALIDATOR__DOC = (
|
53
|
+
"Regular expression to restrict users that can register to the gateway, by JID. "
|
54
|
+
"Defaults to .*@${SLIDGE_SERVER}, but since SLIDGE_SERVER is usually localhost, "
|
55
|
+
"you probably want to change that to .*@example.com"
|
56
|
+
)
|
57
|
+
USER_JID_VALIDATOR__DYNAMIC_DEFAULT = True
|
58
|
+
|
59
|
+
# Optional, so default value + type hint if default is None
|
60
|
+
|
61
|
+
ADMINS: tuple[JIDType, ...] = ()
|
62
|
+
ADMINS__DOC = "JIDs of the gateway admins"
|
63
|
+
|
64
|
+
|
65
|
+
UPLOAD_SERVICE: Optional[str] = None
|
66
|
+
UPLOAD_SERVICE__DOC = (
|
67
|
+
"JID of an HTTP upload service the gateway can use. "
|
68
|
+
"This is optional, as it should be automatically determined via service"
|
69
|
+
"discovery."
|
70
|
+
)
|
71
|
+
|
72
|
+
SECRET_KEY: Optional[str] = None
|
73
|
+
SECRET_KEY__DOC = "Encryption for disk storage"
|
74
|
+
|
75
|
+
NO_ROSTER_PUSH = False
|
76
|
+
NO_ROSTER_PUSH__DOC = "Do not fill users' rosters with legacy contacts automatically"
|
77
|
+
|
78
|
+
ROSTER_PUSH_PRESENCE_SUBSCRIPTION_REQUEST_FALLBACK = True
|
79
|
+
ROSTER_PUSH_PRESENCE_SUBSCRIPTION_REQUEST_FALLBACK__DOC = (
|
80
|
+
"If True, legacy contacts will send a presence request subscription "
|
81
|
+
"when privileged roster push does not work, eg, if XEP-0356 (privileged "
|
82
|
+
"entity) is not available for the component."
|
83
|
+
)
|
84
|
+
|
85
|
+
AVATAR_SIZE = 200
|
86
|
+
AVATAR_SIZE__DOC = (
|
87
|
+
"Maximum image size (width and height), image ratio will be preserved"
|
88
|
+
)
|
89
|
+
|
90
|
+
USE_ATTACHMENT_ORIGINAL_URLS = False
|
91
|
+
USE_ATTACHMENT_ORIGINAL_URLS__DOC = (
|
92
|
+
"For legacy plugins in which attachments are publicly downloadable URLs, "
|
93
|
+
"let XMPP clients directly download them from this URL. Note that this will "
|
94
|
+
"probably leak your client IP to the legacy network."
|
95
|
+
)
|
96
|
+
|
97
|
+
UPLOAD_REQUESTER: Optional[str] = None
|
98
|
+
UPLOAD_REQUESTER__DOC = (
|
99
|
+
"Set which JID should request the upload slots. Defaults to the component JID."
|
100
|
+
)
|
101
|
+
|
102
|
+
NO_UPLOAD_PATH: Optional[str] = None
|
103
|
+
NO_UPLOAD_PATH__DOC = (
|
104
|
+
"Instead of using the XMPP server's HTTP upload component, copy files to this dir. "
|
105
|
+
"You need to set NO_UPLOAD_URL_PREFIX too if you use this option, and configure "
|
106
|
+
"an web server to serve files in this dir."
|
107
|
+
)
|
108
|
+
|
109
|
+
NO_UPLOAD_URL_PREFIX: Optional[str] = None
|
110
|
+
NO_UPLOAD_URL_PREFIX__DOC = (
|
111
|
+
"Base URL that servers files in the dir set in the no-upload-path option, "
|
112
|
+
"eg https://example.com:666/slidge-attachments/"
|
113
|
+
)
|
114
|
+
|
115
|
+
NO_UPLOAD_METHOD: str = "copy"
|
116
|
+
NO_UPLOAD_METHOD__DOC = (
|
117
|
+
"Whether to 'copy', 'move', 'hardlink' or 'symlink' the files in no-upload-path."
|
118
|
+
)
|
119
|
+
|
120
|
+
NO_UPLOAD_FILE_READ_OTHERS = False
|
121
|
+
NO_UPLOAD_FILE_READ_OTHERS__DOC = (
|
122
|
+
"After writing a file in NO_UPLOAD_PATH, change its permission so that 'others' can"
|
123
|
+
" read it."
|
124
|
+
)
|
125
|
+
|
126
|
+
IGNORE_DELAY_THRESHOLD = _TimedeltaSeconds("300")
|
127
|
+
IGNORE_DELAY_THRESHOLD__DOC = (
|
128
|
+
"Threshold, in seconds, below which the <delay> information is stripped "
|
129
|
+
"out of emitted stanzas."
|
130
|
+
)
|
131
|
+
|
132
|
+
PARTIAL_REGISTRATION_TIMEOUT = 3600
|
133
|
+
PARTIAL_REGISTRATION_TIMEOUT__DOC = (
|
134
|
+
"Timeout before registration and login. Only useful for legacy networks where "
|
135
|
+
"a single step registration process is not enough."
|
136
|
+
)
|
137
|
+
|
138
|
+
LAST_SEEN_FALLBACK = True
|
139
|
+
LAST_SEEN_FALLBACK__DOC = (
|
140
|
+
"When using XEP-0319 (Last User Interaction in Presence), use the presence status"
|
141
|
+
" to display the last seen information in the presence status. Useful for clients"
|
142
|
+
" that do not implement XEP-0319."
|
143
|
+
)
|
144
|
+
|
145
|
+
QR_TIMEOUT = 60
|
146
|
+
QR_TIMEOUT__DOC = "Timeout for QR code flashing confirmation."
|
147
|
+
|
148
|
+
DOWNLOAD_CHUNK_SIZE = 1024
|
149
|
+
DOWNLOAD_CHUNK_SIZE__DOC = "Chunk size when slidge needs to download files using HTTP."
|
150
|
+
|
151
|
+
LAST_MESSAGE_CORRECTION_RETRACTION_WORKAROUND = False
|
152
|
+
LAST_MESSAGE_CORRECTION_RETRACTION_WORKAROUND__DOC = (
|
153
|
+
"If the legacy service does not support last message correction but supports"
|
154
|
+
" message retractions, slidge can 'retract' the edited message when you edit from"
|
155
|
+
" an XMPP client, as a workaround. This may only work for editing messages"
|
156
|
+
" **once**. If the legacy service does not support retractions and this is set to"
|
157
|
+
" true, when XMPP clients attempt to correct, this will send a new message."
|
158
|
+
)
|
159
|
+
|
160
|
+
FIX_FILENAME_SUFFIX_MIME_TYPE = False
|
161
|
+
FIX_FILENAME_SUFFIX_MIME_TYPE__DOC = (
|
162
|
+
"Fix the Filename suffix based on the Mime Type of the file. Some clients (eg"
|
163
|
+
" Conversations) may not inline files that have a wrong suffix for the MIME Type."
|
164
|
+
" Therefore the MIME Type of the file is checked, if the suffix is not valid for"
|
165
|
+
" that MIME Type, a valid one will be picked."
|
166
|
+
)
|
167
|
+
|
168
|
+
LOG_FILE: Optional[Path] = None
|
169
|
+
LOG_FILE__DOC = "Log to a file instead of stdout/err"
|
170
|
+
|
171
|
+
MAM_MAX_DAYS = 7
|
172
|
+
MAM_MAX_DAYS__DOC = (
|
173
|
+
"Maximum number of days for group archive retention. "
|
174
|
+
"Since all text content stored in RAM right now, "
|
175
|
+
)
|
176
|
+
|
177
|
+
CORRECTION_EMPTY_BODY_AS_RETRACTION = True
|
178
|
+
CORRECTION_EMPTY_BODY_AS_RETRACTION__DOC = (
|
179
|
+
"Treat last message correction to empty message as a retraction. "
|
180
|
+
"(this is what cheogram do for retraction)"
|
181
|
+
)
|
182
|
+
|
183
|
+
ATTACHMENT_MAXIMUM_FILE_NAME_LENGTH = 200
|
184
|
+
ATTACHMENT_MAXIMUM_FILE_NAME_LENGTH__DOC = (
|
185
|
+
"Some legacy network provide ridiculously long filenames, strip above this limit, "
|
186
|
+
"preserving suffix."
|
187
|
+
)
|
188
|
+
|
189
|
+
ALWAYS_INVITE_WHEN_ADDING_BOOKMARKS = True
|
190
|
+
ALWAYS_INVITE_WHEN_ADDING_BOOKMARKS__DOC = (
|
191
|
+
"Send an invitation to join MUCs when adding them to the bookmarks. While this "
|
192
|
+
"should not be necessary, it helps with clients that do not support :xep:`0402` "
|
193
|
+
"or that do not respect the auto-join flag."
|
194
|
+
)
|
195
|
+
|
196
|
+
AVATAR_RESAMPLING_THREADS = 2
|
197
|
+
AVATAR_RESAMPLING_THREADS__DOC = (
|
198
|
+
"Number of additional threads to use for avatar resampling. Even in a single-core "
|
199
|
+
"context, this makes avatar resampling non-blocking."
|
200
|
+
)
|
201
|
+
|
202
|
+
DEV_MODE = False
|
203
|
+
DEV_MODE__DOC = (
|
204
|
+
"Enables an interactive python shell via chat commands, for admins."
|
205
|
+
"Not safe to use in prod, but great during dev."
|
206
|
+
)
|
207
|
+
|
208
|
+
SYNC_AVATAR = True
|
209
|
+
SYNC_AVATAR__DOC = "Sync the user XMPP avatar to legacy network (if supported)."
|