slidge 0.1.0rc1__py3-none-any.whl → 0.1.2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (164) hide show
  1. slidge/__init__.py +54 -31
  2. slidge/__main__.py +51 -5
  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 +2 -0
  15. slidge/core/cache.py +121 -39
  16. slidge/core/config.py +116 -11
  17. slidge/core/gateway/__init__.py +3 -0
  18. slidge/core/gateway/base.py +895 -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 +795 -0
  29. slidge/core/gateway/vcard_temp.py +130 -0
  30. slidge/core/mixins/__init__.py +9 -1
  31. slidge/core/mixins/attachment.py +506 -0
  32. slidge/core/mixins/avatar.py +167 -0
  33. slidge/core/mixins/base.py +6 -19
  34. slidge/core/mixins/disco.py +66 -15
  35. slidge/core/mixins/lock.py +31 -0
  36. slidge/core/mixins/message.py +254 -252
  37. slidge/core/mixins/message_maker.py +154 -0
  38. slidge/core/mixins/presence.py +128 -31
  39. slidge/core/mixins/recipient.py +43 -0
  40. slidge/core/pubsub.py +275 -116
  41. slidge/core/session.py +586 -518
  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 +458 -0
  46. slidge/group/room.py +1103 -0
  47. slidge/migration.py +18 -0
  48. slidge/slixfix/__init__.py +68 -0
  49. slidge/{util/xep_0050 → slixfix/link_preview}/__init__.py +4 -5
  50. slidge/slixfix/link_preview/link_preview.py +17 -0
  51. slidge/slixfix/link_preview/stanza.py +99 -0
  52. slidge/slixfix/roster.py +60 -0
  53. slidge/{util → slixfix}/xep_0077/register.py +1 -2
  54. slidge/slixfix/xep_0077/stanza.py +104 -0
  55. slidge/{util → slixfix}/xep_0100/gateway.py +17 -12
  56. slidge/slixfix/xep_0153/__init__.py +10 -0
  57. slidge/slixfix/xep_0153/stanza.py +25 -0
  58. slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
  59. slidge/slixfix/xep_0264/__init__.py +5 -0
  60. slidge/slixfix/xep_0264/stanza.py +36 -0
  61. slidge/slixfix/xep_0264/thumbnail.py +23 -0
  62. slidge/slixfix/xep_0292/__init__.py +5 -0
  63. slidge/slixfix/xep_0292/vcard4.py +100 -0
  64. slidge/slixfix/xep_0313/__init__.py +12 -0
  65. slidge/slixfix/xep_0313/mam.py +262 -0
  66. slidge/slixfix/xep_0313/stanza.py +359 -0
  67. slidge/slixfix/xep_0317/__init__.py +5 -0
  68. slidge/slixfix/xep_0317/hats.py +17 -0
  69. slidge/slixfix/xep_0317/stanza.py +28 -0
  70. slidge/{util → slixfix}/xep_0356_old/privilege.py +9 -7
  71. slidge/slixfix/xep_0424/__init__.py +9 -0
  72. slidge/slixfix/xep_0424/retraction.py +77 -0
  73. slidge/slixfix/xep_0424/stanza.py +28 -0
  74. slidge/slixfix/xep_0490/__init__.py +8 -0
  75. slidge/slixfix/xep_0490/mds.py +47 -0
  76. slidge/slixfix/xep_0490/stanza.py +17 -0
  77. slidge/util/__init__.py +4 -6
  78. slidge/util/archive_msg.py +61 -0
  79. slidge/util/conf.py +25 -4
  80. slidge/util/db.py +23 -69
  81. slidge/util/schema.sql +126 -0
  82. slidge/util/sql.py +508 -0
  83. slidge/util/test.py +136 -86
  84. slidge/util/types.py +155 -14
  85. slidge/util/util.py +225 -51
  86. slidge-0.1.2.dist-info/METADATA +111 -0
  87. slidge-0.1.2.dist-info/RECORD +96 -0
  88. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/WHEEL +1 -1
  89. slidge/core/adhoc.py +0 -492
  90. slidge/core/chat_command.py +0 -197
  91. slidge/core/contact.py +0 -441
  92. slidge/core/disco.py +0 -59
  93. slidge/core/gateway.py +0 -899
  94. slidge/core/muc/__init__.py +0 -3
  95. slidge/core/muc/bookmarks.py +0 -74
  96. slidge/core/muc/participant.py +0 -152
  97. slidge/core/muc/room.py +0 -348
  98. slidge/plugins/discord/__init__.py +0 -121
  99. slidge/plugins/discord/client.py +0 -121
  100. slidge/plugins/discord/session.py +0 -172
  101. slidge/plugins/dummy.py +0 -334
  102. slidge/plugins/facebook.py +0 -591
  103. slidge/plugins/hackernews.py +0 -209
  104. slidge/plugins/mattermost/__init__.py +0 -1
  105. slidge/plugins/mattermost/api.py +0 -288
  106. slidge/plugins/mattermost/gateway.py +0 -417
  107. slidge/plugins/mattermost/websocket.py +0 -248
  108. slidge/plugins/signal/__init__.py +0 -4
  109. slidge/plugins/signal/config.py +0 -4
  110. slidge/plugins/signal/contact.py +0 -104
  111. slidge/plugins/signal/gateway.py +0 -379
  112. slidge/plugins/signal/group.py +0 -76
  113. slidge/plugins/signal/session.py +0 -515
  114. slidge/plugins/signal/txt.py +0 -13
  115. slidge/plugins/signal/util.py +0 -32
  116. slidge/plugins/skype.py +0 -310
  117. slidge/plugins/steam.py +0 -400
  118. slidge/plugins/telegram/__init__.py +0 -6
  119. slidge/plugins/telegram/client.py +0 -325
  120. slidge/plugins/telegram/config.py +0 -21
  121. slidge/plugins/telegram/contact.py +0 -154
  122. slidge/plugins/telegram/gateway.py +0 -182
  123. slidge/plugins/telegram/group.py +0 -184
  124. slidge/plugins/telegram/session.py +0 -275
  125. slidge/plugins/telegram/util.py +0 -153
  126. slidge/plugins/whatsapp/__init__.py +0 -6
  127. slidge/plugins/whatsapp/config.py +0 -17
  128. slidge/plugins/whatsapp/contact.py +0 -33
  129. slidge/plugins/whatsapp/event.go +0 -455
  130. slidge/plugins/whatsapp/gateway.go +0 -156
  131. slidge/plugins/whatsapp/gateway.py +0 -69
  132. slidge/plugins/whatsapp/go.mod +0 -17
  133. slidge/plugins/whatsapp/go.sum +0 -22
  134. slidge/plugins/whatsapp/session.go +0 -371
  135. slidge/plugins/whatsapp/session.py +0 -370
  136. slidge/util/xep_0030/__init__.py +0 -13
  137. slidge/util/xep_0030/disco.py +0 -811
  138. slidge/util/xep_0030/stanza/__init__.py +0 -7
  139. slidge/util/xep_0030/stanza/info.py +0 -270
  140. slidge/util/xep_0030/stanza/items.py +0 -147
  141. slidge/util/xep_0030/static.py +0 -467
  142. slidge/util/xep_0050/adhoc.py +0 -631
  143. slidge/util/xep_0050/stanza.py +0 -180
  144. slidge/util/xep_0077/stanza.py +0 -71
  145. slidge/util/xep_0292/__init__.py +0 -1
  146. slidge/util/xep_0292/stanza.py +0 -167
  147. slidge/util/xep_0292/vcard4.py +0 -74
  148. slidge/util/xep_0356/__init__.py +0 -7
  149. slidge/util/xep_0356/permissions.py +0 -35
  150. slidge/util/xep_0356/privilege.py +0 -160
  151. slidge/util/xep_0356/stanza.py +0 -44
  152. slidge/util/xep_0461/__init__.py +0 -6
  153. slidge/util/xep_0461/reply.py +0 -48
  154. slidge/util/xep_0461/stanza.py +0 -80
  155. slidge-0.1.0rc1.dist-info/METADATA +0 -171
  156. slidge-0.1.0rc1.dist-info/RECORD +0 -99
  157. /slidge/{plugins/__init__.py → py.typed} +0 -0
  158. /slidge/{util → slixfix}/xep_0077/__init__.py +0 -0
  159. /slidge/{util → slixfix}/xep_0100/__init__.py +0 -0
  160. /slidge/{util → slixfix}/xep_0100/stanza.py +0 -0
  161. /slidge/{util → slixfix}/xep_0356_old/__init__.py +0 -0
  162. /slidge/{util → slixfix}/xep_0356_old/stanza.py +0 -0
  163. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/LICENSE +0 -0
  164. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,154 @@
1
+ import warnings
2
+ from copy import copy
3
+ from datetime import datetime, timezone
4
+ from typing import TYPE_CHECKING, Iterable, Optional, cast
5
+ from uuid import uuid4
6
+
7
+ from slixmpp import Message
8
+ from slixmpp.types import MessageTypes
9
+
10
+ from ...slixfix.link_preview.stanza import LinkPreview as LinkPreviewStanza
11
+ from ...util.db import GatewayUser
12
+ from ...util.types import (
13
+ ChatState,
14
+ LegacyMessageType,
15
+ LinkPreview,
16
+ MessageReference,
17
+ ProcessingHint,
18
+ )
19
+ from .. import config
20
+ from .base import BaseSender
21
+
22
+ if TYPE_CHECKING:
23
+ from ...group.participant import LegacyParticipant
24
+
25
+
26
+ class MessageMaker(BaseSender):
27
+ mtype: MessageTypes = NotImplemented
28
+ _can_send_carbon: bool = NotImplemented
29
+ STRIP_SHORT_DELAY = False
30
+ USE_STANZA_ID = False
31
+
32
+ def _make_message(
33
+ self,
34
+ state: Optional[ChatState] = None,
35
+ hints: Iterable[ProcessingHint] = (),
36
+ legacy_msg_id: Optional[LegacyMessageType] = None,
37
+ when: Optional[datetime] = None,
38
+ reply_to: Optional[MessageReference] = None,
39
+ carbon=False,
40
+ link_previews: Optional[Iterable[LinkPreview]] = None,
41
+ **kwargs,
42
+ ):
43
+ body = kwargs.pop("mbody", None)
44
+ mfrom = kwargs.pop("mfrom", self.jid)
45
+ mto = kwargs.pop("mto", None)
46
+ thread = kwargs.pop("thread", None)
47
+ if carbon and self._can_send_carbon:
48
+ # the msg needs to have jabber:client as xmlns, so
49
+ # we don't want to associate with the XML stream
50
+ msg_cls = Message
51
+ else:
52
+ msg_cls = self.xmpp.Message # type:ignore
53
+ msg = msg_cls(
54
+ sfrom=mfrom,
55
+ stype=kwargs.pop("mtype", None) or self.mtype,
56
+ sto=mto,
57
+ **kwargs,
58
+ )
59
+ if body:
60
+ msg["body"] = body
61
+ state = "active"
62
+ if thread:
63
+ known_threads = self.session.threads.inverse # type:ignore
64
+ msg["thread"] = known_threads.get(thread) or str(thread)
65
+ if state:
66
+ msg["chat_state"] = state
67
+ for hint in hints:
68
+ msg.enable(hint)
69
+ self._set_msg_id(msg, legacy_msg_id)
70
+ self._add_delay(msg, when)
71
+ if link_previews:
72
+ self._add_link_previews(msg, link_previews)
73
+ if reply_to:
74
+ self._add_reply_to(msg, reply_to)
75
+ return msg
76
+
77
+ def _set_msg_id(
78
+ self, msg: Message, legacy_msg_id: Optional[LegacyMessageType] = None
79
+ ):
80
+ if legacy_msg_id is not None:
81
+ i = self._legacy_to_xmpp(legacy_msg_id)
82
+ msg.set_id(i)
83
+ if self.USE_STANZA_ID:
84
+ msg["stanza_id"]["id"] = i
85
+ msg["stanza_id"]["by"] = self.muc.jid # type: ignore
86
+ elif self.USE_STANZA_ID:
87
+ msg["stanza_id"]["id"] = str(uuid4())
88
+ msg["stanza_id"]["by"] = self.muc.jid # type: ignore
89
+
90
+ def _legacy_to_xmpp(self, legacy_id: LegacyMessageType):
91
+ return self.session.sent.get(legacy_id) or self.session.legacy_to_xmpp_msg_id(
92
+ legacy_id
93
+ )
94
+
95
+ def _add_delay(self, msg: Message, when: Optional[datetime]):
96
+ if when:
97
+ if when.tzinfo is None:
98
+ when = when.astimezone(timezone.utc)
99
+ if self.STRIP_SHORT_DELAY:
100
+ delay = datetime.now().astimezone(timezone.utc) - when
101
+ if delay < config.IGNORE_DELAY_THRESHOLD:
102
+ return
103
+ msg["delay"].set_stamp(when)
104
+ msg["delay"].set_from(self.xmpp.boundjid.bare)
105
+
106
+ def _add_reply_to(self, msg: Message, reply_to: MessageReference):
107
+ xmpp_id = self._legacy_to_xmpp(reply_to.legacy_id)
108
+ msg["reply"]["id"] = xmpp_id
109
+
110
+ muc = getattr(self, "muc", None)
111
+
112
+ if entity := reply_to.author:
113
+ if isinstance(entity, GatewayUser):
114
+ if muc:
115
+ jid = copy(muc.jid)
116
+ jid.resource = fallback_nick = muc.user_nick
117
+ msg["reply"]["to"] = jid
118
+ else:
119
+ msg["reply"]["to"] = entity.jid
120
+ # TODO: here we should use preferably use the PEP nick of the user
121
+ # (but it doesn't matter much)
122
+ fallback_nick = entity.jid.local
123
+ else:
124
+ if muc:
125
+ if hasattr(entity, "muc"):
126
+ # TODO: accept a Contact here and use muc.get_participant_by_legacy_id()
127
+ # a bit of work because right now this is a sync function
128
+ entity = cast("LegacyParticipant", entity)
129
+ fallback_nick = entity.nickname
130
+ else:
131
+ warnings.warn(
132
+ "The author of a message reference in a MUC must be a"
133
+ " Participant instance, not a Contact"
134
+ )
135
+ fallback_nick = entity.name
136
+ else:
137
+ fallback_nick = entity.name
138
+ msg["reply"]["to"] = entity.jid
139
+ else:
140
+ fallback_nick = None
141
+
142
+ if fallback := reply_to.body:
143
+ msg["reply"].add_quoted_fallback(fallback, fallback_nick)
144
+
145
+ @staticmethod
146
+ def _add_link_previews(msg: Message, link_previews: Iterable[LinkPreview]):
147
+ for preview in link_previews:
148
+ element = LinkPreviewStanza()
149
+ for i, name in enumerate(preview._fields):
150
+ val = preview[i]
151
+ if not val:
152
+ continue
153
+ element[name] = val
154
+ msg.append(element)
@@ -1,43 +1,115 @@
1
- from dataclasses import dataclass
2
- from datetime import datetime, timezone
1
+ import re
2
+ from asyncio import Task, sleep
3
+ from datetime import datetime, timedelta, timezone
3
4
  from typing import Optional
4
5
 
6
+ from slixmpp.types import PresenceShows, PresenceTypes
7
+
8
+ from ...util.sql import CachedPresence, db
5
9
  from .. import config
6
10
  from .base import BaseSender
7
11
 
8
12
 
9
- @dataclass
10
- class _CachedPresence:
11
- presence_kwargs: dict[str, str]
12
- last_seen: Optional[datetime] = None
13
+ class _NoChange(Exception):
14
+ pass
15
+
16
+
17
+ _FRIEND_REQUEST_PRESENCES = {"subscribe", "unsubscribe", "subscribed", "unsubscribed"}
13
18
 
14
19
 
15
20
  class PresenceMixin(BaseSender):
16
- _last_presence: Optional[_CachedPresence] = None
21
+ _ONLY_SEND_PRESENCE_CHANGES = False
22
+
23
+ def __init__(self, *a, **k):
24
+ super().__init__(*a, **k)
25
+ self.__update_last_seen_fallback_task: Optional[Task] = None
26
+
27
+ async def __update_last_seen_fallback(self):
28
+ await sleep(3600 * 7)
29
+ self.send_last_presence(force=True, no_cache_online=False)
30
+
31
+ def _get_last_presence(self) -> Optional[CachedPresence]:
32
+ return db.presence_get(self.jid, self.user)
33
+
34
+ def _store_last_presence(self, new: CachedPresence):
35
+ return db.presence_store(self.jid, new, self.user)
17
36
 
18
37
  def _make_presence(
19
38
  self,
20
39
  *,
21
40
  last_seen: Optional[datetime] = None,
22
- **presence_kwargs,
41
+ force=False,
42
+ bare=False,
43
+ ptype: Optional[PresenceTypes] = None,
44
+ pstatus: Optional[str] = None,
45
+ pshow: Optional[PresenceShows] = None,
23
46
  ):
24
- self._last_presence = _CachedPresence(
25
- last_seen=last_seen, presence_kwargs=presence_kwargs
47
+ if last_seen and last_seen.tzinfo is None:
48
+ last_seen = last_seen.astimezone(timezone.utc)
49
+
50
+ old = self._get_last_presence()
51
+
52
+ if ptype not in _FRIEND_REQUEST_PRESENCES:
53
+ new = CachedPresence(
54
+ last_seen=last_seen, ptype=ptype, pstatus=pstatus, pshow=pshow
55
+ )
56
+ if old != new:
57
+ if hasattr(self, "muc") and ptype == "unavailable":
58
+ db.presence_delete(self.jid, self.user)
59
+ else:
60
+ self._store_last_presence(new)
61
+ if old and not force and self._ONLY_SEND_PRESENCE_CHANGES:
62
+ if old == new:
63
+ self.session.log.debug("Presence is the same as cached")
64
+ raise _NoChange
65
+ self.session.log.debug(
66
+ "Presence is not the same as cached: %s vs %s", old, new
67
+ )
68
+
69
+ p = self.xmpp.make_presence(
70
+ pfrom=self.jid.bare if bare else self.jid,
71
+ ptype=ptype,
72
+ pshow=pshow,
73
+ pstatus=pstatus,
26
74
  )
27
- p = self.xmpp.make_presence(pfrom=self.jid, **presence_kwargs)
28
75
  if last_seen:
29
- if config.LAST_SEEN_FALLBACK and not presence_kwargs.get("pstatus"):
30
- p["status"] = f"Last seen {last_seen:%A %H:%M GMT}"
31
- if last_seen.tzinfo is None:
32
- last_seen = last_seen.astimezone(timezone.utc)
76
+ # it's ugly to check for the presence of this string, but a better fix is more work
77
+ if config.LAST_SEEN_FALLBACK and not re.match(
78
+ ".*Last seen .*", p["status"]
79
+ ):
80
+ last_seen_fallback, recent = get_last_seen_fallback(last_seen)
81
+ if p["status"]:
82
+ p["status"] = p["status"] + " -- " + last_seen_fallback
83
+ else:
84
+ p["status"] = last_seen_fallback
85
+ if recent:
86
+ # if less than a week, we use sth like 'Last seen: Monday, 8:05",
87
+ # but if lasts more than a week, this is not very informative, so
88
+ # we need to force resend an updated presence status
89
+ if self.__update_last_seen_fallback_task:
90
+ self.__update_last_seen_fallback_task.cancel()
91
+ self.__update_last_seen_fallback_task = self.xmpp.loop.create_task(
92
+ self.__update_last_seen_fallback()
93
+ )
33
94
  p["idle"]["since"] = last_seen
34
95
  return p
35
96
 
36
- def _send_last_presence(self):
37
- if (cache := self._last_presence) is None:
97
+ def send_last_presence(self, force=False, no_cache_online=False):
98
+ if (cache := self._get_last_presence()) is None:
99
+ if force:
100
+ if no_cache_online:
101
+ self.online()
102
+ else:
103
+ self.offline()
38
104
  return
39
105
  self._send(
40
- self._make_presence(last_seen=cache.last_seen, **cache.presence_kwargs)
106
+ self._make_presence(
107
+ last_seen=cache.last_seen,
108
+ force=True,
109
+ ptype=cache.ptype,
110
+ pshow=cache.pshow,
111
+ pstatus=cache.pstatus,
112
+ )
41
113
  )
42
114
 
43
115
  def online(
@@ -51,7 +123,10 @@ class PresenceMixin(BaseSender):
51
123
  :param status: Arbitrary text, details of the status, eg: "Listening to Britney Spears"
52
124
  :param last_seen: For :xep:`0319`
53
125
  """
54
- self._send(self._make_presence(pstatus=status, last_seen=last_seen))
126
+ try:
127
+ self._send(self._make_presence(pstatus=status, last_seen=last_seen))
128
+ except _NoChange:
129
+ pass
55
130
 
56
131
  def away(
57
132
  self,
@@ -67,9 +142,12 @@ class PresenceMixin(BaseSender):
67
142
  :param status: Arbitrary text, details of the status, eg: "Gone to fight capitalism"
68
143
  :param last_seen: For :xep:`0319`
69
144
  """
70
- self._send(
71
- self._make_presence(pstatus=status, pshow="away", last_seen=last_seen)
72
- )
145
+ try:
146
+ self._send(
147
+ self._make_presence(pstatus=status, pshow="away", last_seen=last_seen)
148
+ )
149
+ except _NoChange:
150
+ pass
73
151
 
74
152
  def extended_away(
75
153
  self,
@@ -85,7 +163,12 @@ class PresenceMixin(BaseSender):
85
163
  :param status: Arbitrary text, details of the status, eg: "Gone to fight capitalism"
86
164
  :param last_seen: For :xep:`0319`
87
165
  """
88
- self._send(self._make_presence(pstatus=status, pshow="xa", last_seen=last_seen))
166
+ try:
167
+ self._send(
168
+ self._make_presence(pstatus=status, pshow="xa", last_seen=last_seen)
169
+ )
170
+ except _NoChange:
171
+ pass
89
172
 
90
173
  def busy(
91
174
  self,
@@ -93,14 +176,17 @@ class PresenceMixin(BaseSender):
93
176
  last_seen: Optional[datetime] = None,
94
177
  ):
95
178
  """
96
- Send a "busy" presence from this contact to the user,
179
+ Send a "busy" (ie, "dnd") presence from this contact to the user,
97
180
 
98
181
  :param status: eg: "Trying to make sense of XEP-0100"
99
182
  :param last_seen: For :xep:`0319`
100
183
  """
101
- self._send(
102
- self._make_presence(pstatus=status, pshow="busy", last_seen=last_seen)
103
- )
184
+ try:
185
+ self._send(
186
+ self._make_presence(pstatus=status, pshow="dnd", last_seen=last_seen)
187
+ )
188
+ except _NoChange:
189
+ pass
104
190
 
105
191
  def offline(
106
192
  self,
@@ -113,8 +199,19 @@ class PresenceMixin(BaseSender):
113
199
  :param status: eg: "Trying to make sense of XEP-0100"
114
200
  :param last_seen: For :xep:`0319`
115
201
  """
116
- self._send(
117
- self._make_presence(
118
- pstatus=status, ptype="unavailable", last_seen=last_seen
202
+ try:
203
+ self._send(
204
+ self._make_presence(
205
+ pstatus=status, ptype="unavailable", last_seen=last_seen
206
+ )
119
207
  )
120
- )
208
+ except _NoChange:
209
+ pass
210
+
211
+
212
+ def get_last_seen_fallback(last_seen: datetime):
213
+ now = datetime.now(tz=timezone.utc)
214
+ if now - last_seen < timedelta(days=7):
215
+ return f"Last seen {last_seen:%A %H:%M GMT}", True
216
+ else:
217
+ return f"Last seen {last_seen:%b %-d %Y}", False
@@ -0,0 +1,43 @@
1
+ from typing import TYPE_CHECKING, Optional, Union
2
+
3
+ from slixmpp.plugins.xep_0004 import Form
4
+
5
+ from ...util.types import LegacyMessageType
6
+
7
+ if TYPE_CHECKING:
8
+ from ..gateway import BaseGateway
9
+
10
+
11
+ class ReactionRecipientMixin:
12
+ REACTIONS_SINGLE_EMOJI = False
13
+ xmpp: "BaseGateway" = NotImplemented
14
+
15
+ async def restricted_emoji_extended_feature(self):
16
+ available = await self.available_emojis()
17
+ if not self.REACTIONS_SINGLE_EMOJI and available is None:
18
+ return None
19
+
20
+ form = Form()
21
+ form["type"] = "result"
22
+ form.add_field("FORM_TYPE", "hidden", value="urn:xmpp:reactions:0:restrictions")
23
+ if self.REACTIONS_SINGLE_EMOJI:
24
+ form.add_field("max_reactions_per_user", value="1")
25
+ if available:
26
+ form.add_field("allowlist", value=list(available))
27
+ return form
28
+
29
+ async def available_emojis(
30
+ self, legacy_msg_id: Optional[LegacyMessageType] = None
31
+ ) -> Optional[set[str]]:
32
+ """
33
+ Override this to restrict the subset of reactions this recipient
34
+ can handle.
35
+
36
+ :return: A set of emojis or None if any emoji is allowed
37
+ """
38
+ return None
39
+
40
+
41
+ class ThreadRecipientMixin:
42
+ async def create_thread(self, xmpp_id: str) -> Union[int, str]:
43
+ return xmpp_id