sleamdge 0.0.0.dev0__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.
- sleamdge/__init__.py +13 -0
- sleamdge/__main__.py +3 -0
- sleamdge/client.py +217 -0
- sleamdge/contact.py +111 -0
- sleamdge/gateway.py +87 -0
- sleamdge/group.py +174 -0
- sleamdge/session.py +162 -0
- sleamdge/types.py +34 -0
- sleamdge/util.py +25 -0
- sleamdge-0.0.0.dev0.dist-info/METADATA +733 -0
- sleamdge-0.0.0.dev0.dist-info/RECORD +14 -0
- sleamdge-0.0.0.dev0.dist-info/WHEEL +5 -0
- sleamdge-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- sleamdge-0.0.0.dev0.dist-info/top_level.txt +1 -0
sleamdge/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from slidge import entrypoint
|
|
2
|
+
from slidge.util.util import get_version # noqa: F401
|
|
3
|
+
|
|
4
|
+
from . import contact, gateway, session
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
entrypoint("sleamdge")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = "contact", "gateway", "session", "main"
|
|
12
|
+
|
|
13
|
+
__version__ = get_version()
|
sleamdge/__main__.py
ADDED
sleamdge/client.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from asyncio import Future, get_running_loop
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import TYPE_CHECKING, Generic, Optional, TypeVar, Union
|
|
6
|
+
|
|
7
|
+
import steam
|
|
8
|
+
from slixmpp.exceptions import XMPPError
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .contact import Contact
|
|
12
|
+
from .group import Participant
|
|
13
|
+
from .session import Session
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
ClanOrGroup = Union[steam.Clan, steam.Group]
|
|
17
|
+
Channel = Union[steam.GroupChannel, steam.ClanChannel]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CredentialsValidation(steam.Client):
|
|
21
|
+
"""
|
|
22
|
+
A client for entering the OTP interactively and validating credentials
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, **k) -> None:
|
|
26
|
+
super().__init__(**k)
|
|
27
|
+
self.code_future: Future[str] = get_running_loop().create_future()
|
|
28
|
+
|
|
29
|
+
async def code(self) -> str:
|
|
30
|
+
return await self.code_future
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SteamClient(steam.Client):
|
|
34
|
+
"""
|
|
35
|
+
The main steam client of registered users
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, session: "Session", **k) -> None:
|
|
39
|
+
self.session = session
|
|
40
|
+
self.__outbox = Pending[int]()
|
|
41
|
+
self.__outbox_reaction = Pending[tuple[int, str, bool]]()
|
|
42
|
+
super().__init__(**k)
|
|
43
|
+
|
|
44
|
+
async def code(self) -> str:
|
|
45
|
+
return await self.session.input(
|
|
46
|
+
"You have been disconnected, please enter the code "
|
|
47
|
+
"you received via email or steam guard"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
async def get_contact_or_participant(
|
|
51
|
+
self, message: steam.Message
|
|
52
|
+
) -> Optional[Union["Contact", "Participant"]]:
|
|
53
|
+
if isinstance(message, steam.UserMessage):
|
|
54
|
+
return await self.session.contacts.by_steam_user(
|
|
55
|
+
message.channel.participant
|
|
56
|
+
)
|
|
57
|
+
elif isinstance(message, (steam.GroupMessage, steam.ClanMessage)):
|
|
58
|
+
muc = await self.session.bookmarks.by_steam_channel(message.channel)
|
|
59
|
+
return await muc.get_participant_by_legacy_id(message.author.id)
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
async def on_typing(self, user: steam.User, when: datetime):
|
|
63
|
+
if user == self.user:
|
|
64
|
+
return
|
|
65
|
+
c = await self.session.contacts.by_steam_user(user)
|
|
66
|
+
c.composing()
|
|
67
|
+
|
|
68
|
+
async def on_message(self, message: steam.Message) -> None:
|
|
69
|
+
if self.__outbox.set_if_pending(message.id):
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
c = await self.get_contact_or_participant(message)
|
|
73
|
+
if c is None:
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
c.send_text(
|
|
77
|
+
message.clean_content, message.id, carbon=message.author == self.user
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
async def on_reaction_add(self, reaction: steam.MessageReaction) -> None:
|
|
81
|
+
if not reaction.emoticon:
|
|
82
|
+
return
|
|
83
|
+
if self.__outbox_reaction.set_if_pending(
|
|
84
|
+
(reaction.message.id, reaction.emoticon.name, True)
|
|
85
|
+
):
|
|
86
|
+
return
|
|
87
|
+
await self.update_reactions(reaction)
|
|
88
|
+
|
|
89
|
+
async def on_reaction_remove(self, reaction: steam.MessageReaction) -> None:
|
|
90
|
+
if not reaction.emoticon:
|
|
91
|
+
return
|
|
92
|
+
if self.__outbox_reaction.set_if_pending(
|
|
93
|
+
(reaction.message.id, reaction.emoticon.name, False)
|
|
94
|
+
):
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
await self.update_reactions(reaction)
|
|
98
|
+
|
|
99
|
+
async def on_clan_update(self, before: steam.Clan, after: steam.Clan, /) -> None:
|
|
100
|
+
await self.on_group_or_clan_update(before, after)
|
|
101
|
+
|
|
102
|
+
async def on_clan_join(self, clan: steam.Clan, /) -> None:
|
|
103
|
+
for c in clan.channels:
|
|
104
|
+
await self.session.bookmarks.by_steam_channel(c)
|
|
105
|
+
|
|
106
|
+
async def on_clan_leave(self, clan: steam.Clan, /) -> None:
|
|
107
|
+
# TODO:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
async def on_group_update(self, before: steam.Group, after: steam.Group, /) -> None:
|
|
111
|
+
await self.on_group_or_clan_update(before, after)
|
|
112
|
+
|
|
113
|
+
async def on_group_or_clan_update(
|
|
114
|
+
self, before: ClanOrGroup, after: ClanOrGroup, /
|
|
115
|
+
) -> None:
|
|
116
|
+
channels_before = set[Channel](before.channels)
|
|
117
|
+
channels_after = set[Channel](after.channels)
|
|
118
|
+
|
|
119
|
+
for channel in channels_after - channels_before:
|
|
120
|
+
muc = await self.session.bookmarks.by_steam_channel(channel)
|
|
121
|
+
if isinstance(after, steam.Group):
|
|
122
|
+
await muc.add_to_bookmarks()
|
|
123
|
+
for channel in channels_before - channels_after:
|
|
124
|
+
muc = await self.session.bookmarks.by_steam_channel(channel)
|
|
125
|
+
muc.get_system_participant().send_text(
|
|
126
|
+
"This channel has been destroyed but this is not yet implemented in slidge…"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
existing_mucs = [
|
|
130
|
+
await self.session.bookmarks.by_steam_channel(c) for c in channels_before
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
for muc in existing_mucs:
|
|
134
|
+
await muc.update_info()
|
|
135
|
+
|
|
136
|
+
async def on_group_join(self, group: steam.Group, /) -> None:
|
|
137
|
+
for c in group.channels:
|
|
138
|
+
muc = await self.session.bookmarks.by_steam_channel(c)
|
|
139
|
+
await muc.add_to_bookmarks()
|
|
140
|
+
|
|
141
|
+
async def on_group_leave(self, group: steam.Group, /) -> None:
|
|
142
|
+
# TODO:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
async def update_reactions(self, reaction: steam.MessageReaction) -> None:
|
|
146
|
+
message = reaction.message
|
|
147
|
+
if not reaction.emoticon:
|
|
148
|
+
return
|
|
149
|
+
c = await self.get_contact_or_participant(message)
|
|
150
|
+
if c is None:
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
self.session.log.debug("Reaction: %s", reaction)
|
|
154
|
+
await c.update_reaction(reaction)
|
|
155
|
+
|
|
156
|
+
async def on_user_update(self, before: steam.User, after: steam.User) -> None:
|
|
157
|
+
if after.id == self.user.id:
|
|
158
|
+
return
|
|
159
|
+
c = await self.session.contacts.by_steam_user(after)
|
|
160
|
+
await c.update_info(after)
|
|
161
|
+
|
|
162
|
+
async def add_emoticon(self, msg: steam.Message, emoticon: steam.Emoticon) -> None:
|
|
163
|
+
e = self.__outbox_reaction.place((msg.id, emoticon.name, True))
|
|
164
|
+
await msg.add_emoticon(emoticon)
|
|
165
|
+
await e.wait()
|
|
166
|
+
|
|
167
|
+
async def remove_emoticon(
|
|
168
|
+
self, msg: steam.Message, emoticon: steam.Emoticon
|
|
169
|
+
) -> None:
|
|
170
|
+
e = self.__outbox_reaction.place((msg.id, emoticon.name, False))
|
|
171
|
+
await msg.remove_emoticon(emoticon)
|
|
172
|
+
await e.wait()
|
|
173
|
+
|
|
174
|
+
async def send(
|
|
175
|
+
self,
|
|
176
|
+
recipient: Union[steam.User, steam.GroupChannel, steam.ClanChannel],
|
|
177
|
+
text: str,
|
|
178
|
+
) -> int:
|
|
179
|
+
message = await recipient.send(text)
|
|
180
|
+
if message is None:
|
|
181
|
+
raise XMPPError("internal-server-error", "No message ID")
|
|
182
|
+
e = self.__outbox.place(message.id)
|
|
183
|
+
await e.wait()
|
|
184
|
+
return message.id
|
|
185
|
+
|
|
186
|
+
async def fetch_user_named(self, name: str):
|
|
187
|
+
id64 = await steam.utils.id64_from_url(
|
|
188
|
+
f"https://steamcommunity.com/id/{name}", self.session.http
|
|
189
|
+
)
|
|
190
|
+
return await self._state.fetch_user(id64) if id64 is not None else None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
T = TypeVar("T")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class Pending(Generic[T]):
|
|
197
|
+
"""
|
|
198
|
+
Helper for the fact that everything we send is echoed, and we mostly
|
|
199
|
+
want to ignore echoes/
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
def __init__(self) -> None:
|
|
203
|
+
self.__data = dict[T, asyncio.Event]()
|
|
204
|
+
|
|
205
|
+
def place(self, key: T) -> asyncio.Event:
|
|
206
|
+
e = self.__data[key] = asyncio.Event()
|
|
207
|
+
return e
|
|
208
|
+
|
|
209
|
+
def set_if_pending(self, key: T) -> bool:
|
|
210
|
+
e = self.__data.pop(key, None)
|
|
211
|
+
if e is None:
|
|
212
|
+
return False
|
|
213
|
+
e.set()
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
log = logging.getLogger(__name__)
|
sleamdge/contact.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, AsyncIterator, Optional
|
|
4
|
+
|
|
5
|
+
import steam
|
|
6
|
+
from slidge import LegacyContact, LegacyRoster
|
|
7
|
+
from slixmpp.exceptions import XMPPError
|
|
8
|
+
from steam.types.id import ID32
|
|
9
|
+
|
|
10
|
+
from .util import EMOJIS_SET, emojize
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .session import Session
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Roster(LegacyRoster[ID32, "Contact"]):
|
|
17
|
+
session: "Session"
|
|
18
|
+
|
|
19
|
+
async def by_steam_user(self, user: steam.User) -> Contact:
|
|
20
|
+
return await self.by_legacy_id(user.id)
|
|
21
|
+
|
|
22
|
+
async def jid_username_to_legacy_id(self, local: str) -> ID32:
|
|
23
|
+
try:
|
|
24
|
+
local_int = int(local)
|
|
25
|
+
except ValueError:
|
|
26
|
+
raise XMPPError("bad-request", f"This is not a valid integer: {local}")
|
|
27
|
+
return ID32(local_int)
|
|
28
|
+
|
|
29
|
+
async def fill(self) -> AsyncIterator["Contact"]:
|
|
30
|
+
for user in await self.session.steam.user.friends():
|
|
31
|
+
c = await self.by_steam_user(user)
|
|
32
|
+
c.is_friend = True
|
|
33
|
+
yield c
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Contact(LegacyContact[ID32]):
|
|
37
|
+
MARKS = False
|
|
38
|
+
CORRECTION = False
|
|
39
|
+
RETRACTION = False
|
|
40
|
+
|
|
41
|
+
session: "Session"
|
|
42
|
+
|
|
43
|
+
async def get_steam_channel(self) -> steam.User:
|
|
44
|
+
u = self.session.steam.get_user(self.legacy_id)
|
|
45
|
+
if u is None:
|
|
46
|
+
raise XMPPError("item-not-found")
|
|
47
|
+
return u
|
|
48
|
+
|
|
49
|
+
async def update_info(self, user: Optional[steam.User] = None) -> None:
|
|
50
|
+
if user is None:
|
|
51
|
+
user = await self.get_steam_channel()
|
|
52
|
+
self.name = user.name
|
|
53
|
+
self.avatar = user.avatar.url
|
|
54
|
+
await self.update_state(user)
|
|
55
|
+
|
|
56
|
+
async def update_state(self, user: Optional[steam.User] = None) -> None:
|
|
57
|
+
if user is None:
|
|
58
|
+
user = await self.get_steam_channel()
|
|
59
|
+
self.log.debug("Rich presence: %s", user.rich_presence)
|
|
60
|
+
match user.state:
|
|
61
|
+
case steam.PersonaState.Online:
|
|
62
|
+
self.online()
|
|
63
|
+
case steam.PersonaState.Offline:
|
|
64
|
+
if user.last_seen_online:
|
|
65
|
+
# workaround for gajim not parsing last presence for "unavailable"
|
|
66
|
+
self.extended_away(last_seen=user.last_seen_online)
|
|
67
|
+
else:
|
|
68
|
+
self.offline()
|
|
69
|
+
case steam.PersonaState.Busy:
|
|
70
|
+
self.busy(last_seen=user.last_seen_online)
|
|
71
|
+
case steam.PersonaState.Away:
|
|
72
|
+
self.away(last_seen=user.last_seen_online)
|
|
73
|
+
case steam.PersonaState.Snooze:
|
|
74
|
+
self.extended_away(last_seen=user.last_seen_online)
|
|
75
|
+
case steam.PersonaState.LookingToPlay:
|
|
76
|
+
self.online(status="Looking to play", last_seen=user.last_seen_online)
|
|
77
|
+
case steam.PersonaState.LookingToTrade:
|
|
78
|
+
self.online(status="Looking to trade", last_seen=user.last_seen_online)
|
|
79
|
+
|
|
80
|
+
async def available_emojis(self, legacy_msg_id=None) -> set[str]:
|
|
81
|
+
return EMOJIS_SET
|
|
82
|
+
|
|
83
|
+
async def update_reaction(self, reaction: steam.MessageReaction) -> None:
|
|
84
|
+
user = self.session.steam.user
|
|
85
|
+
carbon = reaction.user == user
|
|
86
|
+
self.log.debug("Reaction: carbon: %s - %s - %s", user, reaction.user, carbon)
|
|
87
|
+
message = await self.fetch_message(reaction.message.id)
|
|
88
|
+
|
|
89
|
+
reactions = list[str]()
|
|
90
|
+
for r in message.reactions:
|
|
91
|
+
if emoticon := r.emoticon:
|
|
92
|
+
emoji = emojize(emoticon)
|
|
93
|
+
self.log.debug("UPDATING R: %s %s %s", emoji, r.user, user)
|
|
94
|
+
if r.user == user and carbon:
|
|
95
|
+
reactions.append(emoji)
|
|
96
|
+
elif r.user != user and not carbon:
|
|
97
|
+
reactions.append(emoji)
|
|
98
|
+
self.log.debug("UPDATING RS: %s %s %s", message.id, reactions, carbon)
|
|
99
|
+
self.react(message.id, reactions, carbon=carbon)
|
|
100
|
+
|
|
101
|
+
async def fetch_message(self, legacy_msg_id: int) -> steam.UserMessage:
|
|
102
|
+
msg = await (await self.get_steam_channel()).fetch_message(legacy_msg_id)
|
|
103
|
+
if msg is None:
|
|
104
|
+
raise XMPPError(
|
|
105
|
+
"item-not-found", f"Could not find the legacy message {legacy_msg_id}"
|
|
106
|
+
)
|
|
107
|
+
if not isinstance(msg, steam.UserMessage):
|
|
108
|
+
raise XMPPError(
|
|
109
|
+
"internal-server-error", f"This is not a 1:1 message: '{type(msg)}'"
|
|
110
|
+
)
|
|
111
|
+
return msg
|
sleamdge/gateway.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from slidge import BaseGateway, GatewayUser
|
|
6
|
+
from slidge.command.register import FormField, RegistrationType
|
|
7
|
+
from slixmpp import JID
|
|
8
|
+
from slixmpp.exceptions import XMPPError
|
|
9
|
+
|
|
10
|
+
from .client import CredentialsValidation
|
|
11
|
+
from .types import ChannelId
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Gateway(BaseGateway):
|
|
15
|
+
REGISTRATION_INSTRUCTIONS = "Enter steam credentials"
|
|
16
|
+
REGISTRATION_FIELDS = [
|
|
17
|
+
FormField(var="username", label="Steam username", required=True),
|
|
18
|
+
FormField(var="password", label="Password", private=True, required=True),
|
|
19
|
+
]
|
|
20
|
+
REGISTRATION_TYPE = RegistrationType.TWO_FACTOR_CODE
|
|
21
|
+
|
|
22
|
+
ROSTER_GROUP = "Steam"
|
|
23
|
+
|
|
24
|
+
SEARCH_FIELDS = [
|
|
25
|
+
FormField(
|
|
26
|
+
var="user_name",
|
|
27
|
+
label="The name of the user after https://steamcommunity.com/id",
|
|
28
|
+
required=True,
|
|
29
|
+
),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
COMPONENT_NAME = "Steam (slidge)"
|
|
33
|
+
COMPONENT_TYPE = "steam"
|
|
34
|
+
|
|
35
|
+
COMPONENT_AVATAR = "https://logos-download.com/wp-content/uploads/2016/05/Steam_icon_logo_logotype.png"
|
|
36
|
+
|
|
37
|
+
GROUPS = True
|
|
38
|
+
|
|
39
|
+
LEGACY_MSG_ID_TYPE = int
|
|
40
|
+
LEGACY_CONTACT_ID_TYPE = int
|
|
41
|
+
LEGACY_ROOM_ID_TYPE = ChannelId.from_str
|
|
42
|
+
|
|
43
|
+
def __init__(self):
|
|
44
|
+
super().__init__()
|
|
45
|
+
self.__pending = dict[JID, tuple[asyncio.Task, CredentialsValidation]]()
|
|
46
|
+
|
|
47
|
+
async def validate(
|
|
48
|
+
self, user_jid: JID, registration_form: dict[str, Optional[str]]
|
|
49
|
+
):
|
|
50
|
+
username = registration_form["username"]
|
|
51
|
+
password = registration_form["password"]
|
|
52
|
+
|
|
53
|
+
assert isinstance(username, str)
|
|
54
|
+
assert isinstance(password, str)
|
|
55
|
+
|
|
56
|
+
client = CredentialsValidation()
|
|
57
|
+
|
|
58
|
+
async def wrap():
|
|
59
|
+
try:
|
|
60
|
+
await client.login(username, password)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
self.log.exception("Login failed for %s", user_jid, exc_info=e)
|
|
63
|
+
|
|
64
|
+
task = self.xmpp.loop.create_task(wrap())
|
|
65
|
+
self.__pending[JID(user_jid.bare)] = (task, client)
|
|
66
|
+
return {}
|
|
67
|
+
|
|
68
|
+
async def validate_two_factor_code(self, user: GatewayUser, code: str) -> dict:
|
|
69
|
+
task, client = self.__pending.pop(user.jid)
|
|
70
|
+
client.code_future.set_result(code)
|
|
71
|
+
try:
|
|
72
|
+
await asyncio.wait_for(asyncio.shield(client.wait_until_ready()), 5)
|
|
73
|
+
if task.done() and (exc := task.exception()):
|
|
74
|
+
raise XMPPError("not-authorized", f"That didn't work: {exc}")
|
|
75
|
+
await asyncio.wait_for(client.wait_until_ready(), 60)
|
|
76
|
+
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
77
|
+
raise XMPPError("not-authorized", f"Authorization timed out")
|
|
78
|
+
await client.wait_until_ready()
|
|
79
|
+
token = client.refresh_token
|
|
80
|
+
await client.close()
|
|
81
|
+
# For unknown reason, closing and re-opening the connection right away hangs,
|
|
82
|
+
# waiting is a dirty but mostly functional workaround.
|
|
83
|
+
await asyncio.sleep(10)
|
|
84
|
+
return {"refresh_token": token}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
log = logging.getLogger(__name__)
|
sleamdge/group.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, AsyncIterator, Optional, Union
|
|
2
|
+
|
|
3
|
+
from slidge import LegacyBookmarks, LegacyMUC, LegacyParticipant, MucType
|
|
4
|
+
from slidge.util.types import HoleBound
|
|
5
|
+
from slixmpp.exceptions import XMPPError
|
|
6
|
+
from steam import (
|
|
7
|
+
Clan,
|
|
8
|
+
ClanChannel,
|
|
9
|
+
ClanMessage,
|
|
10
|
+
Group,
|
|
11
|
+
GroupChannel,
|
|
12
|
+
GroupMessage,
|
|
13
|
+
MessageReaction,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from .contact import Contact
|
|
17
|
+
from .types import ChannelId, Parent, ParentType
|
|
18
|
+
from .util import EMOJIS_SET, emojize
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from .session import Session
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Bookmarks(LegacyBookmarks[ChannelId, "LegacyMUC"]):
|
|
25
|
+
session: "Session"
|
|
26
|
+
|
|
27
|
+
async def by_steam_channel(self, channel: Union[GroupChannel, ClanChannel]):
|
|
28
|
+
if channel.group:
|
|
29
|
+
parent = Parent(ParentType.GROUP, channel.group.id)
|
|
30
|
+
elif channel.clan:
|
|
31
|
+
parent = Parent(ParentType.CLAN, channel.clan.id)
|
|
32
|
+
else:
|
|
33
|
+
raise XMPPError(
|
|
34
|
+
"internal-server-error", "No group or clan associated to this channel"
|
|
35
|
+
)
|
|
36
|
+
return await self.by_legacy_id(ChannelId(parent, channel.id))
|
|
37
|
+
|
|
38
|
+
async def legacy_id_to_jid_local_part(self, legacy_id: ChannelId) -> str:
|
|
39
|
+
return str(legacy_id)
|
|
40
|
+
|
|
41
|
+
async def jid_local_part_to_legacy_id(self, local_part: str) -> ChannelId:
|
|
42
|
+
try:
|
|
43
|
+
return ChannelId.from_str(local_part)
|
|
44
|
+
except ValueError as e:
|
|
45
|
+
raise XMPPError("bad-request", f"This is not a valid sleamdge MUC id: {e}")
|
|
46
|
+
|
|
47
|
+
async def fill(self):
|
|
48
|
+
for group in self.session.steam.groups:
|
|
49
|
+
for group_channel in group.channels:
|
|
50
|
+
channel_id = ChannelId(
|
|
51
|
+
Parent(ParentType.GROUP, group.id), group_channel.id
|
|
52
|
+
)
|
|
53
|
+
muc = await self.by_legacy_id(channel_id)
|
|
54
|
+
await muc.add_to_bookmarks()
|
|
55
|
+
for clan in self.session.steam.clans:
|
|
56
|
+
for clan_channel in clan.channels:
|
|
57
|
+
channel_id = ChannelId(
|
|
58
|
+
Parent(ParentType.CLAN, clan.id), clan_channel.id
|
|
59
|
+
)
|
|
60
|
+
await self.by_legacy_id(channel_id)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Participant(LegacyParticipant):
|
|
64
|
+
contact: Contact
|
|
65
|
+
muc: "MUC"
|
|
66
|
+
session: "Session"
|
|
67
|
+
|
|
68
|
+
async def update_reaction(self, reaction: MessageReaction) -> None:
|
|
69
|
+
message = await self.muc.fetch_message(reaction.message.id)
|
|
70
|
+
|
|
71
|
+
reactions = list[str]()
|
|
72
|
+
if emoticon := reaction.emoticon:
|
|
73
|
+
reactions.append(emojize(emoticon))
|
|
74
|
+
for r in message.reactions:
|
|
75
|
+
self.log.debug("add reaction: %s", r)
|
|
76
|
+
if emoticon := r.emoticon:
|
|
77
|
+
emoji = emojize(emoticon)
|
|
78
|
+
self.log.debug(
|
|
79
|
+
"add reaction: %s %s %s", emoji, r.user.id, self.contact.legacy_id
|
|
80
|
+
)
|
|
81
|
+
if r.user.id == self.contact.legacy_id:
|
|
82
|
+
reactions.append(emoji)
|
|
83
|
+
self.log.debug("updating reactions: %s %s", message.id, reactions)
|
|
84
|
+
self.react(message.id, reactions)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class MUC(LegacyMUC[ChannelId, int, Participant, int]):
|
|
88
|
+
session: "Session"
|
|
89
|
+
|
|
90
|
+
async def get_steam_channel(self) -> Union[GroupChannel, ClanChannel]:
|
|
91
|
+
parent_id = self.legacy_id.parent
|
|
92
|
+
parent: Optional[Union[Group, Clan]]
|
|
93
|
+
if parent_id.type == ParentType.GROUP:
|
|
94
|
+
parent = self.session.steam.get_group(parent_id.id)
|
|
95
|
+
else:
|
|
96
|
+
parent = self.session.steam.get_clan(parent_id.id)
|
|
97
|
+
if not parent:
|
|
98
|
+
raise XMPPError("internal-server-error", "No parent for this channel")
|
|
99
|
+
channel = parent.get_channel(self.legacy_id.channel_id)
|
|
100
|
+
if not channel:
|
|
101
|
+
raise XMPPError(
|
|
102
|
+
"item-not-found",
|
|
103
|
+
f"Channel {self.legacy_id.channel_id} not found in {parent}",
|
|
104
|
+
)
|
|
105
|
+
return channel
|
|
106
|
+
|
|
107
|
+
async def fetch_message(
|
|
108
|
+
self, legacy_msg_id: int
|
|
109
|
+
) -> Union[GroupMessage, ClanMessage]:
|
|
110
|
+
msg = await (await self.get_steam_channel()).fetch_message(legacy_msg_id)
|
|
111
|
+
if msg is None:
|
|
112
|
+
raise XMPPError(
|
|
113
|
+
"item-not-found", f"Could not find the legacy message {legacy_msg_id}"
|
|
114
|
+
)
|
|
115
|
+
if not isinstance(msg, (GroupMessage, ClanMessage)):
|
|
116
|
+
raise XMPPError(
|
|
117
|
+
"internal-server-error", f"This is not a group message: '{type(msg)}'"
|
|
118
|
+
)
|
|
119
|
+
return msg
|
|
120
|
+
|
|
121
|
+
async def update_info(self):
|
|
122
|
+
channel = await self.get_steam_channel()
|
|
123
|
+
self.log.debug("Channel: %s", channel)
|
|
124
|
+
parent: Union[Group, Clan] = channel.group # type:ignore
|
|
125
|
+
if parent is None:
|
|
126
|
+
self.name = channel.name
|
|
127
|
+
self.type = MucType.GROUP
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
self.name = f"{parent.name}/{channel.name or 'Home'}"
|
|
131
|
+
self.description = parent.tagline
|
|
132
|
+
sha = parent.avatar.sha.hex()
|
|
133
|
+
if self.avatar != sha:
|
|
134
|
+
# TODO: use avatar.url when this is fixed upstream
|
|
135
|
+
await self.set_avatar(
|
|
136
|
+
f"https://avatars.akamai.steamstatic.com/{sha}_256.jpg", sha
|
|
137
|
+
)
|
|
138
|
+
if isinstance(parent, Clan):
|
|
139
|
+
self.type = MucType.CHANNEL_NON_ANONYMOUS
|
|
140
|
+
else:
|
|
141
|
+
self.type = MucType.GROUP
|
|
142
|
+
|
|
143
|
+
async def fill_participants(self) -> AsyncIterator[Participant]:
|
|
144
|
+
g = (await self.get_steam_channel()).group
|
|
145
|
+
if not g:
|
|
146
|
+
return
|
|
147
|
+
try:
|
|
148
|
+
for m in await g.chunk():
|
|
149
|
+
yield await self.get_participant_by_legacy_id(m.id)
|
|
150
|
+
except AttributeError:
|
|
151
|
+
# workaround for https://github.com/Gobot1234/steam.py/issues/565
|
|
152
|
+
for m in g.members:
|
|
153
|
+
yield await self.get_participant_by_legacy_id(m.id)
|
|
154
|
+
|
|
155
|
+
async def backfill(
|
|
156
|
+
self,
|
|
157
|
+
after: Optional[HoleBound] = None,
|
|
158
|
+
before: Optional[HoleBound] = None,
|
|
159
|
+
):
|
|
160
|
+
c = await self.get_steam_channel()
|
|
161
|
+
async for msg in c.history(
|
|
162
|
+
before=None if before is None else before.timestamp,
|
|
163
|
+
after=None if after is None else after.timestamp,
|
|
164
|
+
):
|
|
165
|
+
part = await self.get_participant_by_legacy_id(msg.author.id)
|
|
166
|
+
part.send_text(
|
|
167
|
+
msg.clean_content,
|
|
168
|
+
when=msg.created_at,
|
|
169
|
+
archive_only=True,
|
|
170
|
+
legacy_msg_id=msg.id,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
async def available_emojis(self, legacy_msg_id=None) -> set[str]:
|
|
174
|
+
return EMOJIS_SET
|