slidge 0.3.0b4__py3-none-any.whl → 0.3.2__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.
@@ -162,15 +162,12 @@ class AttachmentMixin(TextMessageMixin):
162
162
  legacy_file_id=None
163
163
  if attachment.legacy_file_id is None
164
164
  else str(attachment.legacy_file_id),
165
- url=attachment.url,
165
+ url=attachment.url if config.USE_ATTACHMENT_ORIGINAL_URLS else None,
166
166
  )
167
167
 
168
168
  async def __get_url(
169
169
  self, attachment: LegacyAttachment, stored: Attachment
170
170
  ) -> tuple[bool, Optional[Path], str]:
171
- if attachment.url and config.USE_ATTACHMENT_ORIGINAL_URLS:
172
- return False, None, attachment.url
173
-
174
171
  file_name = attachment.name
175
172
  content_type = attachment.content_type
176
173
  file_path = attachment.path
@@ -221,7 +218,9 @@ class AttachmentMixin(TextMessageMixin):
221
218
 
222
219
  assert isinstance(file_path, Path)
223
220
  if config.FIX_FILENAME_SUFFIX_MIME_TYPE:
224
- file_name = str(fix_suffix(file_path, content_type, file_name))
221
+ file_name, content_type = fix_suffix(file_path, content_type, file_name)
222
+ attachment.content_type = content_type
223
+ attachment.name = file_name
225
224
 
226
225
  if config.NO_UPLOAD_PATH:
227
226
  local_path, new_url = await self.__no_upload(
@@ -312,6 +311,50 @@ class AttachmentMixin(TextMessageMixin):
312
311
  stored.sfs = str(sfs)
313
312
  msg.append(sfs)
314
313
 
314
+ async def __set_sfs_and_sims_without_download(
315
+ self, msg: Message, attachment: LegacyAttachment
316
+ ) -> None:
317
+ assert attachment.url is not None
318
+
319
+ if not any(
320
+ (
321
+ attachment.content_type,
322
+ attachment.name,
323
+ attachment.disposition,
324
+ )
325
+ ):
326
+ return
327
+
328
+ sims = self.xmpp.plugin["xep_0385"].stanza.Sims()
329
+ ref = self.xmpp["xep_0372"].stanza.Reference()
330
+
331
+ ref["uri"] = attachment.url
332
+ ref["type"] = "data"
333
+ sims["sources"].append(ref)
334
+ sims.enable("file")
335
+
336
+ xep_0447_stanza = self.xmpp.plugin["xep_0447"].stanza
337
+ sfs = xep_0447_stanza.StatelessFileSharing()
338
+ url_data = xep_0447_stanza.UrlData()
339
+ url_data["target"] = attachment.url
340
+ sfs["sources"].append(url_data)
341
+ sfs.enable("file")
342
+
343
+ if attachment.content_type:
344
+ sims["file"]["media-type"] = attachment.content_type
345
+ sfs["file"]["media-type"] = attachment.content_type
346
+ if attachment.caption:
347
+ sims["file"]["desc"] = attachment.caption
348
+ sfs["file"]["desc"] = attachment.caption
349
+ if attachment.name:
350
+ sims["file"]["name"] = attachment.name
351
+ sfs["file"]["name"] = attachment.name
352
+ if attachment.disposition:
353
+ sfs["disposition"] = attachment.disposition
354
+
355
+ msg.append(sims)
356
+ msg.append(sfs)
357
+
315
358
  def __send_url(
316
359
  self,
317
360
  msg: Message,
@@ -325,6 +368,9 @@ class AttachmentMixin(TextMessageMixin):
325
368
  ) -> list[Message]:
326
369
  msg["oob"]["url"] = uploaded_url
327
370
  msg["body"] = uploaded_url
371
+ if msg.get_plugin("sfs", check=True):
372
+ msg["fallback"].enable("body")
373
+ msg["fallback"]["for"] = self.xmpp.plugin["xep_0447"].stanza.NAMESPACE
328
374
  if caption:
329
375
  m1 = self._send(msg, carbon=carbon, **kwargs)
330
376
  m2 = self.send_text(
@@ -465,7 +511,10 @@ class AttachmentMixin(TextMessageMixin):
465
511
  new_url = stored.url
466
512
  else:
467
513
  is_temp, local_path, new_url = await self.__get_url(attachment, stored)
468
- if new_url is None:
514
+ if new_url is None or (
515
+ local_path is not None and local_path.stat().st_size == 0
516
+ ):
517
+ log.warning("Something went wrong with this attachment: %s", attachment)
469
518
  msg["body"] = (
470
519
  "I tried to send a file, but something went wrong. "
471
520
  "Tell your slidge admin to check the logs."
@@ -474,8 +523,13 @@ class AttachmentMixin(TextMessageMixin):
474
523
  return None, [self._send(msg, **kwargs)]
475
524
 
476
525
  stored.url = new_url
477
- thumbnail = await self.__set_sims(msg, new_url, local_path, attachment, stored)
478
- self.__set_sfs(msg, new_url, local_path, attachment, stored, thumbnail)
526
+ if config.USE_ATTACHMENT_ORIGINAL_URLS and attachment.url:
527
+ await self.__set_sfs_and_sims_without_download(msg, attachment)
528
+ else:
529
+ thumbnail = await self.__set_sims(
530
+ msg, new_url, local_path, attachment, stored
531
+ )
532
+ self.__set_sfs(msg, new_url, local_path, attachment, stored, thumbnail)
479
533
 
480
534
  if self.session is not NotImplemented:
481
535
  with self.xmpp.store.session(expire_on_commit=False) as orm:
slidge/core/mixins/db.py CHANGED
@@ -2,7 +2,7 @@ import logging
2
2
  import typing
3
3
  from contextlib import contextmanager
4
4
 
5
- from ...db.models import Base, Contact, Participant, Room
5
+ from ...db.models import Base, Contact, Room
6
6
 
7
7
  if typing.TYPE_CHECKING:
8
8
  from slidge import BaseGateway
@@ -41,13 +41,35 @@ class UpdateInfoMixin(DBMixin):
41
41
  def __init__(self, *args, **kwargs) -> None:
42
42
  super().__init__(*args, **kwargs)
43
43
  self._updating_info = False
44
+ self.__deserialize()
45
+
46
+ def __deserialize(self):
44
47
  if self.stored.extra_attributes is not None:
45
48
  self.deserialize_extra_attributes(self.stored.extra_attributes)
46
49
 
50
+ def refresh(self) -> None:
51
+ with self.xmpp.store.session(expire_on_commit=False) as orm:
52
+ orm.add(self.stored)
53
+ orm.refresh(self.stored)
54
+ self.__deserialize()
55
+
47
56
  def serialize_extra_attributes(self) -> dict | None:
57
+ """
58
+ If you want custom attributes of your instance to be stored persistently
59
+ to the DB, here is where you have to return them as a dict to be used in
60
+ `deserialize_extra_attributes()`.
61
+
62
+ """
48
63
  return None
49
64
 
50
65
  def deserialize_extra_attributes(self, data: dict) -> None:
66
+ """
67
+ This is where you get the dict that you passed in
68
+ `serialize_extra_attributes()`.
69
+
70
+ ⚠ Since it is serialized as json, dictionary keys are converted to strings!
71
+ Be sure to convert to other types if necessary.
72
+ """
51
73
  pass
52
74
 
53
75
  @contextmanager
@@ -1,7 +1,7 @@
1
+ import uuid
1
2
  import warnings
2
3
  from datetime import datetime, timezone
3
4
  from typing import TYPE_CHECKING, Iterable, Optional, cast
4
- from uuid import uuid4
5
5
 
6
6
  from slixmpp import JID, Message
7
7
  from slixmpp.types import MessageTypes
@@ -95,7 +95,7 @@ class MessageMaker(BaseSender):
95
95
  msg["stanza_id"]["id"] = i
96
96
  msg["stanza_id"]["by"] = self.muc.jid # type: ignore
97
97
  elif self.USE_STANZA_ID:
98
- msg["stanza_id"]["id"] = str(uuid4())
98
+ msg["stanza_id"]["id"] = str(uuid.uuid4())
99
99
  msg["stanza_id"]["by"] = self.muc.jid # type: ignore
100
100
 
101
101
  def _legacy_to_xmpp(self, legacy_id: LegacyMessageType) -> str:
@@ -112,7 +112,7 @@ class MessageMaker(BaseSender):
112
112
  if when.tzinfo is None:
113
113
  when = when.astimezone(timezone.utc)
114
114
  if self.STRIP_SHORT_DELAY:
115
- delay = datetime.now().astimezone(timezone.utc) - when
115
+ delay = (datetime.now().astimezone(timezone.utc) - when).seconds
116
116
  if delay < config.IGNORE_DELAY_THRESHOLD:
117
117
  return
118
118
  msg["delay"].set_stamp(when)
@@ -191,7 +191,7 @@ class TextMessageMixin(MessageMaker):
191
191
  xmpp_id = kwargs.pop("xmpp_id", None)
192
192
  if not xmpp_id:
193
193
  xmpp_id = self._legacy_to_xmpp(legacy_msg_id)
194
- self.xmpp["xep_0444"].set_reactions(msg, to_id=xmpp_id, reactions=emojis)
194
+ self.xmpp["xep_0444"].set_reactions(msg, to_id=xmpp_id, reactions=set(emojis))
195
195
  self.__add_reaction_fallback(msg, legacy_msg_id, emojis)
196
196
  self._send(msg, **kwargs)
197
197
 
@@ -46,6 +46,8 @@ def _clear_last_seen_task(contact_pk: int, _task) -> None:
46
46
  class PresenceMixin(BaseSender, DBMixin):
47
47
  _ONLY_SEND_PRESENCE_CHANGES = False
48
48
 
49
+ # this attribute actually only exists for contacts and not participants
50
+ _updating_info: bool
49
51
  stored: Contact | Participant
50
52
 
51
53
  def __init__(self, *a, **k) -> None:
@@ -55,15 +57,24 @@ class PresenceMixin(BaseSender, DBMixin):
55
57
  # to DB at the end of update_info()
56
58
  self.cached_presence: Optional[CachedPresence] = None
57
59
 
60
+ def __is_contact(self) -> bool:
61
+ return isinstance(self.stored, Contact)
62
+
58
63
  def __stored(self) -> Contact | None:
59
- if isinstance(self.stored, Contact):
64
+ if self.__is_contact():
65
+ assert isinstance(self.stored, Contact)
60
66
  return self.stored
61
67
  else:
68
+ assert isinstance(self.stored, Participant)
62
69
  try:
63
70
  return self.stored.contact
64
71
  except DetachedInstanceError:
65
72
  with self.xmpp.store.session() as orm:
66
73
  orm.add(self.stored)
74
+ if self.stored.contact is None:
75
+ return None
76
+ orm.refresh(self.stored.contact)
77
+ orm.merge(self.stored)
67
78
  return self.stored.contact
68
79
 
69
80
  @property
@@ -85,15 +96,22 @@ class PresenceMixin(BaseSender, DBMixin):
85
96
  )
86
97
 
87
98
  def _store_last_presence(self, new: CachedPresence) -> None:
88
- stored = self.__stored()
89
- if stored is not None:
90
- stored.cached_presence = True
91
- for k, v in new._asdict().items():
92
- setattr(stored, k, v)
99
+ stored_contact = self.__stored()
100
+ if stored_contact is None:
101
+ return
102
+ stored_contact.cached_presence = True
103
+ for k, v in new._asdict().items():
104
+ setattr(stored_contact, k, v)
105
+ if self.__is_contact() and self._updating_info:
106
+ return
107
+ with self.xmpp.store.session(expire_on_commit=False) as orm:
93
108
  try:
94
- self.commit()
109
+ orm.add(stored_contact)
95
110
  except InvalidRequestError:
96
- self.commit(merge=True)
111
+ stored_contact = orm.merge(stored_contact)
112
+ orm.add(stored_contact)
113
+
114
+ orm.commit()
97
115
 
98
116
  def _make_presence(
99
117
  self,
@@ -279,9 +297,9 @@ class PresenceMixin(BaseSender, DBMixin):
279
297
  pass
280
298
 
281
299
 
282
- def get_last_seen_fallback(last_seen: datetime):
300
+ def get_last_seen_fallback(last_seen: datetime) -> tuple[str, bool]:
283
301
  now = datetime.now(tz=timezone.utc)
284
302
  if now - last_seen < timedelta(days=7):
285
- return f"Last seen {last_seen:%A %H:%M GMT}", True
303
+ return f"Last seen {last_seen:%A %H:%M %p GMT}", True
286
304
  else:
287
305
  return f"Last seen {last_seen:%b %-d %Y}", False
slidge/core/pubsub.py CHANGED
@@ -120,7 +120,7 @@ class PubSubComponent(NamedLockMixin, BasePlugin):
120
120
  try:
121
121
  iq = await self.xmpp.plugin["xep_0030"].get_info(from_)
122
122
  except (IqError, IqTimeout):
123
- log.debug("Could get disco#info of %s, ignoring", from_)
123
+ log.debug("Could not get disco#info of %s, ignoring", from_)
124
124
  return []
125
125
  info = iq["disco_info"]
126
126
  return info["features"]
slidge/core/session.py CHANGED
@@ -539,6 +539,17 @@ class BaseSession(
539
539
  """
540
540
  raise NotImplementedError
541
541
 
542
+ async def on_preferences(
543
+ self, previous: dict[str, Any], new: dict[str, Any]
544
+ ) -> None:
545
+ """
546
+ This is called when the user updates their preferences.
547
+
548
+ Override this if you need set custom preferences field and need to trigger
549
+ something when a preference has changed.
550
+ """
551
+ raise NotImplementedError
552
+
542
553
  def __reset_ready(self) -> None:
543
554
  self.ready = self.xmpp.loop.create_future()
544
555
 
@@ -708,6 +719,8 @@ class BaseSession(
708
719
  log.warning("User not found during unregistration")
709
720
  return
710
721
 
722
+ session.cancel_all_tasks()
723
+
711
724
  await cls.xmpp.unregister(session)
712
725
  with cls.xmpp.store.session() as orm:
713
726
  orm.delete(session.user)
slidge/db/models.py CHANGED
@@ -7,7 +7,7 @@ import sqlalchemy as sa
7
7
  from slixmpp import JID
8
8
  from slixmpp.types import MucAffiliation, MucRole
9
9
  from sqlalchemy import JSON, ForeignKey, Index, UniqueConstraint
10
- from sqlalchemy.orm import Mapped, declared_attr, mapped_column, relationship
10
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
11
11
 
12
12
  from ..util.types import ClientType, MucType
13
13
  from .meta import Base, JSONSerializable, JSONSerializableTypes
slidge/db/store.py CHANGED
@@ -7,9 +7,10 @@ from datetime import datetime, timedelta, timezone
7
7
  from mimetypes import guess_extension
8
8
  from typing import Collection, Iterator, Optional, Type
9
9
 
10
+ import sqlalchemy as sa
10
11
  from slixmpp.exceptions import XMPPError
11
12
  from slixmpp.plugins.xep_0231.stanza import BitsOfBinary
12
- from sqlalchemy import Engine, delete, select, update
13
+ from sqlalchemy import Engine, delete, event, select, update
13
14
  from sqlalchemy.exc import InvalidRequestError
14
15
  from sqlalchemy.orm import Session, attributes, sessionmaker
15
16
 
@@ -20,6 +21,7 @@ from .meta import Base
20
21
  from .models import (
21
22
  ArchivedMessage,
22
23
  ArchivedMessageSource,
24
+ Avatar,
23
25
  Bob,
24
26
  Contact,
25
27
  ContactSent,
@@ -213,6 +215,7 @@ class ContactStore(UpdatedMixin):
213
215
  def __init__(self, session: Session) -> None:
214
216
  super().__init__(session)
215
217
  session.execute(update(Contact).values(cached_presence=False))
218
+ session.execute(update(Contact).values(caps_ver=None))
216
219
 
217
220
  @staticmethod
218
221
  def add_to_sent(session: Session, contact_pk: int, msg_id: str) -> None:
@@ -589,4 +592,29 @@ class BobStore:
589
592
  session.add(row)
590
593
 
591
594
 
595
+ @event.listens_for(sa.orm.Session, "after_flush")
596
+ def _check_avatar_orphans(session, flush_context):
597
+ if not session.deleted:
598
+ return
599
+
600
+ potentially_orphaned = set()
601
+ for obj in session.deleted:
602
+ if isinstance(obj, (Contact, Room)) and obj.avatar_id:
603
+ potentially_orphaned.add(obj.avatar_id)
604
+ if not potentially_orphaned:
605
+ return
606
+
607
+ result = session.execute(
608
+ sa.delete(Avatar).where(
609
+ sa.and_(
610
+ Avatar.id.in_(potentially_orphaned),
611
+ sa.not_(sa.exists().where(Contact.avatar_id == Avatar.id)),
612
+ sa.not_(sa.exists().where(Room.avatar_id == Avatar.id)),
613
+ )
614
+ )
615
+ )
616
+ deleted_count = result.rowcount
617
+ log.debug(f"Auto-deleted %s orphaned avatars", deleted_count)
618
+
619
+
592
620
  log = logging.getLogger(__name__)
slidge/group/archive.py CHANGED
@@ -181,6 +181,9 @@ def archivable(msg: Message) -> bool:
181
181
  if msg.get_plugin("displayed", check=True):
182
182
  return True
183
183
 
184
+ if msg["thread"] and msg["subject"]:
185
+ return True
186
+
184
187
  return False
185
188
 
186
189
 
@@ -22,6 +22,7 @@ from ..util.types import (
22
22
  CachedPresence,
23
23
  Hat,
24
24
  LegacyMessageType,
25
+ LegacyThreadType,
25
26
  MessageOrPresenceTypeVar,
26
27
  MucAffiliation,
27
28
  MucRole,
@@ -526,6 +527,24 @@ class LegacyParticipant(
526
527
  msg.xml.append(ET.Element(f"{{{msg.namespace}}}subject"))
527
528
  self._send(msg, full_jid)
528
529
 
530
+ def set_thread_subject(
531
+ self,
532
+ thread: LegacyThreadType,
533
+ subject: str | None,
534
+ when: Optional[datetime] = None,
535
+ ) -> None:
536
+ msg = self._make_message()
537
+ msg["thread"] = str(thread)
538
+ if when is not None:
539
+ msg["delay"].set_stamp(when)
540
+ msg["delay"]["from"] = self.muc.jid
541
+ if subject:
542
+ msg["subject"] = subject
543
+ else:
544
+ # may be simplified if slixmpp lets it do it more easily some day
545
+ msg.xml.append(ET.Element(f"{{{msg.namespace}}}subject"))
546
+ self._send(msg)
547
+
529
548
 
530
549
  def escape_nickname(muc_jid: JID, nickname: str) -> tuple[str, JID]:
531
550
  nickname = nickname_no_illegal = strip_illegal_chars(nickname)
slidge/group/room.py CHANGED
@@ -43,6 +43,7 @@ from ..util.types import (
43
43
  LegacyGroupIdType,
44
44
  LegacyMessageType,
45
45
  LegacyParticipantType,
46
+ LegacyThreadType,
46
47
  LegacyUserIdType,
47
48
  Mention,
48
49
  MucAffiliation,
@@ -295,7 +296,7 @@ class LegacyMUC(
295
296
  self, affiliation: Optional[MucAffiliation] = None
296
297
  ) -> AsyncIterator[LegacyParticipantType]:
297
298
  await self.__fill_participants()
298
- with self.xmpp.store.session(expire_on_commit=False) as orm:
299
+ with self.xmpp.store.session(expire_on_commit=False, autoflush=False) as orm:
299
300
  orm.add(self.stored)
300
301
  for db_participant in self.stored.participants:
301
302
  if (
@@ -523,6 +524,9 @@ class LegacyMUC(
523
524
  if s := self.subject:
524
525
  form.add_field("muc#roominfo_subject", value=s)
525
526
 
527
+ if name := self.name:
528
+ form.add_field("muc#roomconfig_roomname", value=name)
529
+
526
530
  if self._set_avatar_task is not None:
527
531
  await self._set_avatar_task
528
532
  avatar = self.get_avatar()
@@ -651,7 +655,7 @@ class LegacyMUC(
651
655
  with orm.no_autoflush:
652
656
  orm.refresh(self.stored, ["participants"])
653
657
  if not user_participant.is_user:
654
- self.log.warning("is_user flag not set participant on user_participant")
658
+ self.log.warning("is_user flag not set on user_participant")
655
659
  user_participant.is_user = True
656
660
  user_participant.send_initial_presence(
657
661
  user_full_jid,
@@ -1335,6 +1339,17 @@ class LegacyMUC(
1335
1339
  """
1336
1340
  raise NotImplementedError
1337
1341
 
1342
+ async def on_set_thread_subject(
1343
+ self, thread: LegacyThreadType, subject: str
1344
+ ) -> None:
1345
+ """
1346
+ Triggered when the user requests changing the subject of a specific thread.
1347
+
1348
+ :param thread: Legacy identifier of the thread
1349
+ :param subject: The new subject for this thread.
1350
+ """
1351
+ raise NotImplementedError
1352
+
1338
1353
  @property
1339
1354
  def participants_filled(self) -> bool:
1340
1355
  return self.stored.participants_filled
@@ -1,5 +1,6 @@
1
+ from slixmpp import register_stanza_plugin, __version_info__
1
2
  from slixmpp.plugins.base import BasePlugin, register_plugin
2
- from slixmpp.plugins.xep_0292.stanza import NS
3
+ from slixmpp.plugins.xep_0292.stanza import NS, _VCardTextElementBase, VCard4
3
4
 
4
5
 
5
6
  class VCard4Provider(BasePlugin):
@@ -11,4 +12,13 @@ class VCard4Provider(BasePlugin):
11
12
  self.xmpp.plugin["xep_0030"].add_feature(NS)
12
13
 
13
14
 
15
+
16
+
14
17
  register_plugin(VCard4Provider)
18
+
19
+
20
+ if __version_info__[0] <= 1 and __version_info__[1] <= 11:
21
+ class Pronouns(_VCardTextElementBase):
22
+ name = plugin_attrib = "pronouns"
23
+
24
+ register_stanza_plugin(VCard4, Pronouns)
slidge/util/test.py CHANGED
@@ -42,7 +42,7 @@ from slidge import (
42
42
 
43
43
  from ..command import Command
44
44
  from ..core import config
45
- from ..core.config import _TimedeltaSeconds
45
+ from ..core.session import _sessions
46
46
  from ..db import SlidgeStore
47
47
  from ..db.avatar import avatar_cache
48
48
  from ..db.meta import Base
@@ -206,7 +206,7 @@ class SlidgeTest(SlixTestPlus):
206
206
  user_jid_validator = ".*"
207
207
  admins: list[str] = []
208
208
  upload_requester = None
209
- ignore_delay_threshold = _TimedeltaSeconds("300")
209
+ ignore_delay_threshold = 300
210
210
 
211
211
  @classmethod
212
212
  def setUpClass(cls) -> None:
@@ -281,6 +281,7 @@ class SlidgeTest(SlixTestPlus):
281
281
  self.db_engine.echo = False
282
282
  super().tearDown()
283
283
  Base.metadata.drop_all(self.xmpp.store._engine)
284
+ _sessions.clear()
284
285
 
285
286
  def setup_logged_session(self, n_contacts: int = 0) -> None:
286
287
  with self.xmpp.store.session() as orm:
slidge/util/util.py CHANGED
@@ -34,7 +34,9 @@ except ImportError as e:
34
34
  )
35
35
 
36
36
 
37
- def fix_suffix(path: Path, mime_type: Optional[str], file_name: Optional[str]) -> Path:
37
+ def fix_suffix(
38
+ path: Path, mime_type: Optional[str], file_name: Optional[str]
39
+ ) -> tuple[str, str]:
38
40
  guessed = magic.from_file(path, mime=True)
39
41
  if guessed == mime_type:
40
42
  log.debug("Magic and given MIME match")
@@ -53,15 +55,15 @@ def fix_suffix(path: Path, mime_type: Optional[str], file_name: Optional[str]) -
53
55
 
54
56
  if suffix in valid_suffix_list:
55
57
  log.debug("Suffix %s is in %s", suffix, valid_suffix_list)
56
- return name
58
+ return str(name), guessed
57
59
 
58
60
  valid_suffix = mimetypes.guess_extension(mime_type.split(";")[0], strict=False)
59
61
  if valid_suffix is None:
60
62
  log.debug("No valid suffix found")
61
- return name
63
+ return str(name), guessed
62
64
 
63
65
  log.debug("Changing suffix of %s to %s", file_name or path.name, valid_suffix)
64
- return name.with_suffix(valid_suffix)
66
+ return str(name.with_suffix(valid_suffix)), guessed
65
67
 
66
68
 
67
69
  class SubclassableOnce(type):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slidge
3
- Version: 0.3.0b4
3
+ Version: 0.3.2
4
4
  Summary: XMPP bridging framework
5
5
  Author-email: Nicolas Cedilnik <nicoco@nicoco.fr>
6
6
  License-Expression: AGPL-3.0-or-later