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/session.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import tempfile
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from typing import Optional, Union
|
|
5
|
+
|
|
6
|
+
import steam
|
|
7
|
+
from PIL import Image
|
|
8
|
+
from slidge import BaseSession, FormField, SearchResult, global_config
|
|
9
|
+
from slixmpp.exceptions import XMPPError
|
|
10
|
+
|
|
11
|
+
from .client import SteamClient
|
|
12
|
+
from .contact import Contact, Roster
|
|
13
|
+
from .group import MUC, Bookmarks
|
|
14
|
+
from .util import demojize, emojize
|
|
15
|
+
|
|
16
|
+
Recipient = Union[Contact, MUC]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Session(BaseSession[int, Recipient]):
|
|
20
|
+
contacts: Roster
|
|
21
|
+
bookmarks: Bookmarks
|
|
22
|
+
|
|
23
|
+
def __init__(self, *a, **k):
|
|
24
|
+
super().__init__(*a, **k)
|
|
25
|
+
self.steam = SteamClient(self)
|
|
26
|
+
self.login_task: Optional[asyncio.Task] = None
|
|
27
|
+
self.emoticons = dict[str, steam.Emoticon]()
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def xmpp_to_legacy_msg_id(i: str) -> int:
|
|
31
|
+
return int(i)
|
|
32
|
+
|
|
33
|
+
async def __login_task(self):
|
|
34
|
+
refresh_token = self.user.legacy_module_data.get("refresh_token")
|
|
35
|
+
if refresh_token is None:
|
|
36
|
+
refresh_token = (
|
|
37
|
+
global_config.HOME_DIR / str(self.user_jid.bare)
|
|
38
|
+
).read_text()
|
|
39
|
+
self.legacy_module_data_set({"refresh_token": refresh_token})
|
|
40
|
+
assert isinstance(refresh_token, str)
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
async with self.steam:
|
|
44
|
+
await self.steam.login(refresh_token=refresh_token)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
self.log.debug("logged out", exc_info=e)
|
|
47
|
+
self.send_gateway_status("Logged out", show="dnd")
|
|
48
|
+
self.send_gateway_message(
|
|
49
|
+
f"You have been logged out ('{e}').\n"
|
|
50
|
+
f"This may be because you have logged in to steam from another client.\n"
|
|
51
|
+
f"Use the 're-login' command when you want to log back in from here."
|
|
52
|
+
)
|
|
53
|
+
self.steam = SteamClient(self)
|
|
54
|
+
self.logged = False
|
|
55
|
+
|
|
56
|
+
async def login(self) -> None:
|
|
57
|
+
self.login_task = asyncio.create_task(self.__login_task())
|
|
58
|
+
await self.steam.wait_until_ready()
|
|
59
|
+
self.contacts.user_legacy_id = self.steam.user.id
|
|
60
|
+
self.emoticons = {e.name: e for e in self.steam.emoticons}
|
|
61
|
+
|
|
62
|
+
async def logout(self) -> None:
|
|
63
|
+
await self.steam.close()
|
|
64
|
+
|
|
65
|
+
async def on_composing(self, c: Recipient, thread=None) -> None:
|
|
66
|
+
if c.is_group:
|
|
67
|
+
return
|
|
68
|
+
assert isinstance(c, Contact)
|
|
69
|
+
user = await c.get_steam_channel()
|
|
70
|
+
await user.trigger_typing()
|
|
71
|
+
|
|
72
|
+
async def on_text(self, chat: Recipient, text: str, **_kwargs) -> int:
|
|
73
|
+
recipient = await chat.get_steam_channel()
|
|
74
|
+
mid = await self.steam.send(recipient, text)
|
|
75
|
+
return mid
|
|
76
|
+
|
|
77
|
+
async def on_file(self, chat: Recipient, url: str, **_kwargs) -> int:
|
|
78
|
+
recipient = await chat.get_steam_channel()
|
|
79
|
+
mid = await self.steam.send(recipient, url)
|
|
80
|
+
return mid
|
|
81
|
+
|
|
82
|
+
async def on_react(
|
|
83
|
+
self, chat: Recipient, legacy_msg_id: int, emojis: list[str], thread=None
|
|
84
|
+
) -> None:
|
|
85
|
+
msg = await chat.fetch_message(legacy_msg_id)
|
|
86
|
+
|
|
87
|
+
reactions_xmpp = set(emojis)
|
|
88
|
+
reactions_steam = set()
|
|
89
|
+
for r in msg.reactions:
|
|
90
|
+
if emoticon := r.emoticon:
|
|
91
|
+
if r.user == self.steam.user:
|
|
92
|
+
reactions_steam.add(emojize(emoticon))
|
|
93
|
+
|
|
94
|
+
to_remove = reactions_steam - reactions_xmpp
|
|
95
|
+
to_add = reactions_xmpp - reactions_steam
|
|
96
|
+
|
|
97
|
+
self.log.debug("msg: %s", msg)
|
|
98
|
+
|
|
99
|
+
for emoji in to_add:
|
|
100
|
+
emoticon_name = demojize(emoji)
|
|
101
|
+
if emoticon_name is None:
|
|
102
|
+
raise XMPPError("bad-request", f"Forbidden emoji: {emoji}")
|
|
103
|
+
emoticon = self.emoticons[emoticon_name]
|
|
104
|
+
self.log.debug("add emoticon: %r", emoticon)
|
|
105
|
+
await self.steam.add_emoticon(msg, emoticon)
|
|
106
|
+
|
|
107
|
+
for emoji in to_remove:
|
|
108
|
+
emoticon_name = demojize(emoji)
|
|
109
|
+
emoticon = self.emoticons[emoticon_name]
|
|
110
|
+
self.log.debug("rm emoticon: %r", emoticon)
|
|
111
|
+
await self.steam.remove_emoticon(msg, emoticon)
|
|
112
|
+
|
|
113
|
+
async def on_correct(self, *args, **kwargs) -> None:
|
|
114
|
+
raise XMPPError("feature-not-implemented", "No correction in steam")
|
|
115
|
+
|
|
116
|
+
async def on_retract(
|
|
117
|
+
self, chat: Recipient, legacy_msg_id: int, thread=None
|
|
118
|
+
) -> None:
|
|
119
|
+
raise XMPPError("feature-not-implemented", "No retraction in steam")
|
|
120
|
+
|
|
121
|
+
async def on_displayed(self, c: Recipient, legacy_msg_id: int, thread=None) -> None:
|
|
122
|
+
msg = await c.fetch_message(legacy_msg_id)
|
|
123
|
+
await msg.ack()
|
|
124
|
+
|
|
125
|
+
async def on_search(self, form_values: dict[str, str]):
|
|
126
|
+
user = await self.steam.fetch_user_named(form_values["user_name"])
|
|
127
|
+
if not user:
|
|
128
|
+
raise XMPPError("item-not-found")
|
|
129
|
+
return SearchResult(
|
|
130
|
+
fields=[FormField("username"), FormField("jid", type="jid-single")],
|
|
131
|
+
items=[
|
|
132
|
+
{
|
|
133
|
+
"username": form_values["user_name"],
|
|
134
|
+
"jid": (await self.contacts.by_steam_user(user)).jid.bare,
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def on_avatar(
|
|
140
|
+
self, bytes_: Optional[bytes], hash_, type_, width, height
|
|
141
|
+
) -> None:
|
|
142
|
+
if bytes_ is None:
|
|
143
|
+
# Steam forces you to have an avatar, so we cannot remove it AFAIK
|
|
144
|
+
return
|
|
145
|
+
if len(bytes_) > 10e6:
|
|
146
|
+
# steam does not want avatars larger than 1024 KB
|
|
147
|
+
bytes_ = await self.xmpp.loop.run_in_executor(None, resize_image, bytes_)
|
|
148
|
+
# workaround for bug in steam.py preventing use of BytesIO
|
|
149
|
+
# https://github.com/Gobot1234/steam.py/issues/566
|
|
150
|
+
with tempfile.NamedTemporaryFile() as tmp_file:
|
|
151
|
+
tmp_file.write(bytes_)
|
|
152
|
+
await self.steam.user.edit(avatar=steam.Media(tmp_file.name))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def resize_image(bytes_: bytes) -> bytes:
|
|
156
|
+
with BytesIO() as f:
|
|
157
|
+
img = Image.open(BytesIO(bytes_))
|
|
158
|
+
img.thumbnail((500, 500)) # should be < 1024 KB hopefully
|
|
159
|
+
img.save(f, format="JPEG")
|
|
160
|
+
f.flush()
|
|
161
|
+
f.seek(0)
|
|
162
|
+
return f.read()
|
sleamdge/types.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ParentType(IntEnum):
|
|
5
|
+
GROUP = 1
|
|
6
|
+
CLAN = 2
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Parent:
|
|
10
|
+
__slots__ = "type", "id"
|
|
11
|
+
|
|
12
|
+
def __init__(self, type_: ParentType, parent_id: int):
|
|
13
|
+
self.type = type_
|
|
14
|
+
self.id = parent_id
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ChannelId:
|
|
18
|
+
__slots__ = "parent", "channel_id"
|
|
19
|
+
|
|
20
|
+
def __init__(self, parent: Parent, channel_id: int):
|
|
21
|
+
self.parent = parent
|
|
22
|
+
self.channel_id = channel_id
|
|
23
|
+
|
|
24
|
+
def __repr__(self):
|
|
25
|
+
return f"{self.parent.type.value}-{self.parent.id}-{self.channel_id}"
|
|
26
|
+
|
|
27
|
+
def __hash__(self):
|
|
28
|
+
return hash(str(self))
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_str(cls, string):
|
|
32
|
+
parent_type_int, parent_id, channel_id = (int(x) for x in string.split("-"))
|
|
33
|
+
parent = Parent(ParentType(parent_type_int), parent_id)
|
|
34
|
+
return cls(parent, channel_id)
|
sleamdge/util.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import steam
|
|
2
|
+
|
|
3
|
+
EMOJIS = dict[str, str](
|
|
4
|
+
[
|
|
5
|
+
("steamthumbsup", "👍"),
|
|
6
|
+
("steamthumbsdown", "👎"),
|
|
7
|
+
("steambored", "🥱"),
|
|
8
|
+
("steamfacepalm", "🤦"),
|
|
9
|
+
("steamhappy", "😄"),
|
|
10
|
+
("steammocking", "😝"),
|
|
11
|
+
("steamsalty", "🧂"),
|
|
12
|
+
("steamsad", "😔"),
|
|
13
|
+
("steamthis", "⬆"),
|
|
14
|
+
]
|
|
15
|
+
)
|
|
16
|
+
EMOJIS_INVERSE = {v: k for k, v in EMOJIS.items()}
|
|
17
|
+
EMOJIS_SET = set(EMOJIS.values())
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def emojize(emoticon: steam.Emoticon) -> str:
|
|
21
|
+
return EMOJIS.get(emoticon.name, "❔")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def demojize(emoji: str) -> str:
|
|
25
|
+
return EMOJIS_INVERSE[emoji]
|