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
@@ -1,209 +0,0 @@
1
- """
2
- Hackernews slidge plugin
3
-
4
- Will poll replies to items you've posted.
5
- For every reply, the chat window '<REPLY_ITEM_ID>@<BRIDGE_JID>' should open.
6
- It will contain your original post (as a carbon) and its reply as a normal chat message.
7
- You can re-reply by replying in your XMPP client directly.
8
- """
9
-
10
- import asyncio
11
- import logging
12
- import re
13
- from datetime import datetime
14
- from typing import Any, Optional
15
-
16
- import aiohttp
17
- from slixmpp import JID
18
- from slixmpp.exceptions import XMPPError
19
-
20
- from slidge import *
21
-
22
-
23
- class Gateway(BaseGateway):
24
- # FIXME: implement proper login process, but we might have to do something to handle captcha
25
- REGISTRATION_INSTRUCTIONS = (
26
- "Enter the hackernews cookie from your browser's dev console "
27
- "(something like your-user-name ampersand XXXXXXXXXXXXXXXXXXXXXXXXX)"
28
- )
29
- REGISTRATION_FIELDS = [
30
- FormField(var="cookie", label="'user' cookie", required=True),
31
- ]
32
-
33
- ROSTER_GROUP = "HN" # Not used, we don't add anything to the roster
34
-
35
- COMPONENT_NAME = "Hackernews (slidge)"
36
- COMPONENT_TYPE = "hackernews"
37
-
38
- COMPONENT_AVATAR = "https://news.ycombinator.com/favicon.ico"
39
-
40
- async def validate(
41
- self, user_jid: JID, registration_form: dict[str, Optional[str]]
42
- ):
43
- if registration_form["cookie"] is None:
44
- raise ValueError("A cookie is required")
45
- async with aiohttp.ClientSession(
46
- cookies={"user": registration_form["cookie"]}
47
- ) as session:
48
- async with session.get(LOGIN_URL, allow_redirects=False) as r:
49
- log.debug("Login response: %s - %s", r.status, await r.text())
50
- if r.status != 302:
51
- raise ValueError("Cookie does not seem valid")
52
-
53
-
54
- class Session(BaseSession):
55
- def __init__(self, user):
56
- super().__init__(user)
57
- self.http_session = aiohttp.ClientSession(
58
- cookies={"user": self.user.registration_form["cookie"]} # type: ignore
59
- )
60
- self.highest_handled_submission_id = 0
61
- self.hn_username = self.user.registration_form["cookie"].split("&")[0] # type: ignore
62
-
63
- async def login(self):
64
- kid_ids: list[int] = []
65
- for submission_id in await self.get_user_submissions():
66
- user_submission = await self.get_item(submission_id)
67
- for kid_id in user_submission.get("kids", []):
68
- kid_ids.append(kid_id)
69
-
70
- if kid_ids:
71
- self.highest_handled_submission_id = max(kid_ids)
72
-
73
- self.xmpp.loop.create_task(self.main_loop())
74
- return f"Logged as {self.hn_username}"
75
-
76
- async def main_loop(self):
77
- kid_ids: list[int] = []
78
- while True:
79
- kid_ids.clear()
80
- for submission_id in await self.get_user_submissions():
81
- try:
82
- user_submission = await self.get_item(submission_id)
83
- except aiohttp.ContentTypeError as e:
84
- log.warning("Hackernews API problem: %s")
85
- log.exception(e)
86
- continue
87
- for kid_id in user_submission.get("kids", []):
88
- if kid_id <= self.highest_handled_submission_id:
89
- continue
90
- await self.send_own_and_reply(user_submission, kid_id)
91
- kid_ids.append(kid_id)
92
- if kid_ids:
93
- self.highest_handled_submission_id = max(kid_ids)
94
- await asyncio.sleep(POLL_INTERVAL)
95
-
96
- async def get_user_submissions(self) -> list[int]:
97
- log.debug("Getting user subs: %s", self.hn_username)
98
- async with self.http_session.get(
99
- f"{API_URL}/user/{self.hn_username}.json"
100
- ) as r:
101
- if r.status != 200:
102
- log.warning("Bad response from API: %s", r)
103
- raise RuntimeError
104
- return (await r.json())["submitted"]
105
-
106
- async def send_own_and_reply(self, user_submission, reply_id):
107
- contact: LegacyContact = await self.contacts.by_legacy_id(reply_id)
108
- date = datetime.fromtimestamp(user_submission["time"])
109
- contact.send_text(
110
- parse_comment_text(user_submission["text"]), when=date, carbon=True
111
- )
112
- kid = await self.get_item(reply_id)
113
- contact.send_text(parse_comment_text(kid["text"]))
114
-
115
- async def get_item(self, item_id):
116
- async with self.http_session.get(f"{API_URL}/item/{item_id}.json") as r:
117
- return await r.json()
118
-
119
- async def logout(self):
120
- pass
121
-
122
- async def send_text(self, text: str, chat: LegacyContact, **k):
123
- goto = f"threads?id={self.hn_username}#{chat.legacy_id}"
124
- url = f"{REPLY_URL}?id={chat.legacy_id}&goto={goto}"
125
- async with self.http_session.get(url) as r:
126
- if r.status != 200:
127
- raise XMPPError(text="Couldn't get the post reply web page from HN")
128
- form_page_content = await r.text()
129
- match = re.search(HMAC_RE, form_page_content)
130
-
131
- if match is None:
132
- raise XMPPError(
133
- text="Couldn't find the HMAC hidden input on the comment reply thread"
134
- )
135
-
136
- await asyncio.sleep(SLEEP_BEFORE_POST)
137
-
138
- form_dict = {
139
- "hmac": match.group(1),
140
- "parent": chat.legacy_id,
141
- "text": text,
142
- "goto": goto,
143
- }
144
-
145
- form = aiohttp.FormData(form_dict)
146
- async with self.http_session.post(REPLY_POST_URL, data=form) as r:
147
- first_attempt_html_content = await r.text()
148
-
149
- log.debug("Reply response #1: %s", r)
150
- if r.status != 200:
151
- raise XMPPError(text=f"Problem replying: {r}")
152
-
153
- if REPOST_TEXT in first_attempt_html_content:
154
- match = re.search(HMAC_RE, first_attempt_html_content)
155
- if match is None:
156
- raise XMPPError(
157
- text="We should repost but haven't found any hmac field on the repost page"
158
- )
159
-
160
- await asyncio.sleep(SLEEP_BEFORE_POST2)
161
- form = aiohttp.FormData(form_dict | {"hmac": match.group(1)})
162
- async with self.http_session.post(REPLY_POST_URL, data=form) as r:
163
- log.debug("Reply response #2: %s", r)
164
- if r.status != 200:
165
- raise XMPPError(text=f"Problem replying: {r}")
166
-
167
- # none of the following make sense in a HN context,
168
- # this is just to avoid raising NotImplementedErrors
169
- async def send_file(self, *a, **k):
170
- pass
171
-
172
- async def active(self, c: LegacyContact):
173
- pass
174
-
175
- async def inactive(self, c: LegacyContact):
176
- pass
177
-
178
- async def composing(self, c: LegacyContact):
179
- pass
180
-
181
- async def paused(self, c: LegacyContact):
182
- pass
183
-
184
- async def displayed(self, legacy_msg_id: Any, c: LegacyContact):
185
- pass
186
-
187
- async def correct(self, text: str, legacy_msg_id: Any, c: LegacyContact):
188
- pass
189
-
190
- async def search(self, form_values: dict[str, str]):
191
- pass
192
-
193
-
194
- def parse_comment_text(text: str):
195
- # TODO: use regex or something more efficient here
196
- return text.replace("<p>", "\n").replace("&#x27;", "'").replace("&#x2F;", "/")
197
-
198
-
199
- LOGIN_URL = "https://news.ycombinator.com/login"
200
- REPLY_URL = "https://news.ycombinator.com/reply"
201
- REPLY_POST_URL = "https://news.ycombinator.com/comment"
202
- API_URL = "https://hacker-news.firebaseio.com/v0"
203
- POLL_INTERVAL = 30 # seconds
204
- SLEEP_BEFORE_POST = 30 # seconds
205
- SLEEP_BEFORE_POST2 = 5 # seconds
206
- REPOST_TEXT = "<tr><td>Please confirm that this is your comment by submitting it one"
207
- HMAC_RE = re.compile(r'name="hmac" value="([a-zA-Z\d]*)"')
208
-
209
- log = logging.getLogger(__name__)
@@ -1 +0,0 @@
1
- from .gateway import Gateway, Session
@@ -1,288 +0,0 @@
1
- import asyncio
2
- import io
3
- import logging
4
-
5
- import aiohttp
6
- import emoji
7
- from mattermost_api_reference_client.api.channels import (
8
- create_direct_channel,
9
- get_channel_members,
10
- get_channels_for_team_for_user,
11
- view_channel,
12
- )
13
- from mattermost_api_reference_client.api.files import get_file, upload_file
14
- from mattermost_api_reference_client.api.posts import (
15
- create_post,
16
- delete_post,
17
- get_posts_for_channel,
18
- update_post,
19
- )
20
- from mattermost_api_reference_client.api.reactions import (
21
- delete_reaction,
22
- get_reactions,
23
- save_reaction,
24
- )
25
- from mattermost_api_reference_client.api.status import get_users_statuses_by_ids
26
- from mattermost_api_reference_client.api.teams import get_teams_for_user
27
- from mattermost_api_reference_client.api.users import (
28
- get_profile_image,
29
- get_user,
30
- get_user_by_username,
31
- get_users_by_ids,
32
- )
33
- from mattermost_api_reference_client.client import AuthenticatedClient
34
- from mattermost_api_reference_client.models import (
35
- Reaction,
36
- Status,
37
- User,
38
- ViewChannelJsonBody,
39
- )
40
- from mattermost_api_reference_client.models.create_post_json_body import (
41
- CreatePostJsonBody,
42
- )
43
- from mattermost_api_reference_client.models.update_post_json_body import (
44
- UpdatePostJsonBody,
45
- )
46
- from mattermost_api_reference_client.models.upload_file_multipart_data import (
47
- UploadFileMultipartData,
48
- )
49
- from mattermost_api_reference_client.types import File, Unset
50
-
51
-
52
- class MattermostException(Exception):
53
- pass
54
-
55
-
56
- class ContactNotFound(MattermostException):
57
- pass
58
-
59
-
60
- class MattermostClient:
61
- # TODO: this should be autogenerated using a template in mattermost_api_reference_client
62
-
63
- def __init__(self, *args, **kwargs):
64
- self.http = AuthenticatedClient(*args, **kwargs)
65
- self.mm_id: asyncio.Future[str] = asyncio.get_running_loop().create_future()
66
- self.me: asyncio.Future[User] = asyncio.get_running_loop().create_future()
67
-
68
- async def login(self):
69
- log.debug("Login")
70
- me = await get_user.asyncio("me", client=self.http)
71
- if me is None:
72
- raise RuntimeError("Could not login")
73
- self.me.set_result(me)
74
- my_id = me.id
75
- if isinstance(my_id, Unset):
76
- raise RuntimeError("Could not login")
77
- self.mm_id.set_result(my_id)
78
- log.debug("Me: %s", me)
79
-
80
- async def get_contacts(self) -> list[str]:
81
- mm = self.http
82
- my_id = await self.mm_id
83
-
84
- contact_mm_ids: list[str] = []
85
-
86
- teams = await get_teams_for_user.asyncio("me", client=mm)
87
-
88
- if teams is None:
89
- raise RuntimeError
90
-
91
- for team in teams:
92
- if isinstance(team.id, Unset):
93
- log.warning("Team without ID")
94
- continue
95
- channels = await get_channels_for_team_for_user.asyncio(
96
- "me", team.id, client=mm
97
- )
98
-
99
- if channels is None:
100
- log.warning("Team without channels")
101
- continue
102
-
103
- for channel in channels:
104
- if isinstance(channel.id, Unset):
105
- log.warning("Channel without ID")
106
- continue
107
- members = await self.get_channel_members(channel.id, per_page=4)
108
- if len(members) == 2:
109
- user_ids = {m.user_id for m in members}
110
- try:
111
- user_ids.remove(my_id)
112
- except KeyError:
113
- log.warning("Weird 2 person channel: %s", members)
114
- else:
115
- contact_id = user_ids.pop()
116
- if not isinstance(contact_id, str):
117
- log.warning("Weird contact: %s", members)
118
- continue
119
- contact_mm_ids.append(contact_id)
120
-
121
- return contact_mm_ids
122
-
123
- async def get_channel_members(
124
- self, channel_id: str, *, page: int = 0, per_page: int = 10
125
- ):
126
- members = await get_channel_members.asyncio(
127
- channel_id, client=self.http, per_page=per_page, page=page
128
- )
129
- if members is None:
130
- raise RuntimeError
131
- return members
132
-
133
- async def get_users_by_ids(self, user_ids: list[str]) -> list[User]:
134
- r = await get_users_by_ids.asyncio(json_body=user_ids, client=self.http)
135
- if r is None:
136
- raise RuntimeError
137
- return r
138
-
139
- async def get_user(self, user_id: str):
140
- r = await get_user.asyncio(user_id, client=self.http)
141
- if r is None:
142
- raise RuntimeError
143
- if isinstance(r.username, Unset):
144
- raise RuntimeError
145
- return r
146
-
147
- async def get_users_statuses_by_ids(self, user_ids: list[str]) -> list[Status]:
148
- r = await get_users_statuses_by_ids.asyncio(
149
- json_body=user_ids, client=self.http
150
- )
151
- if r is None:
152
- raise RuntimeError
153
- return r
154
-
155
- async def send_message_to_user(self, user_id: str, text: str) -> str:
156
- await self.mm_id
157
- mm = self.http
158
-
159
- other = await self.get_user_by_username(user_id)
160
-
161
- direct_channel = await self.get_direct_channel(other.id)
162
-
163
- msg = await create_post.asyncio(
164
- json_body=CreatePostJsonBody(channel_id=direct_channel.id, message=text),
165
- client=mm,
166
- )
167
- if msg is None:
168
- raise RuntimeError
169
-
170
- if isinstance(msg.id, Unset):
171
- raise RuntimeError
172
-
173
- return msg.id
174
-
175
- async def send_message_with_file(self, channel_id: str, file_id: str):
176
- r = await create_post.asyncio(
177
- json_body=CreatePostJsonBody(
178
- channel_id=channel_id, file_ids=[file_id], message=""
179
- ),
180
- client=self.http,
181
- )
182
- if r is None or isinstance(r.id, Unset):
183
- raise RuntimeError(r)
184
- return r.id
185
-
186
- async def get_user_by_username(self, username: str) -> User:
187
- user = await get_user_by_username.asyncio(username, client=self.http)
188
- if user is None or isinstance(user.id, Unset):
189
- raise ContactNotFound("Contact not found")
190
- return user
191
-
192
- async def get_direct_channel(self, user_id):
193
- direct_channel = await create_direct_channel.asyncio(
194
- json_body=[await self.mm_id, user_id], client=self.http
195
- )
196
- if direct_channel is None or isinstance(direct_channel.id, Unset):
197
- raise RuntimeError("Could not create direct channel")
198
- return direct_channel
199
-
200
- async def get_profile_image(self, user_id: str) -> bytes:
201
- resp = await get_profile_image.asyncio_detailed(user_id, client=self.http)
202
- return resp.content
203
-
204
- async def get_file(self, file_id: str):
205
- resp = await get_file.asyncio_detailed(file_id, client=self.http)
206
- return resp.content
207
-
208
- async def delete_post(self, post_id: str):
209
- r = await delete_post.asyncio(post_id, client=self.http)
210
- if r is not None and not isinstance(r, Unset) and r.status != "ok":
211
- raise RuntimeError("Could not delete post %s", post_id)
212
-
213
- async def update_post(self, post_id: str, body: str):
214
- r = await update_post.asyncio(
215
- post_id,
216
- client=self.http,
217
- json_body=UpdatePostJsonBody(id=post_id, message=body),
218
- )
219
- if r is None or isinstance(r, Unset):
220
- raise RuntimeError(r)
221
- return r.id
222
-
223
- async def get_posts_for_channel(self, channel_id: str):
224
- r = await get_posts_for_channel.asyncio(
225
- channel_id, client=self.http, per_page=2
226
- )
227
- if r is None or isinstance(r, Unset):
228
- raise RuntimeError(r)
229
- return r
230
-
231
- async def upload_file(self, channel_id: str, url: str):
232
- async with aiohttp.ClientSession() as s:
233
- async with s.get(url) as get_response:
234
- data = await get_response.read()
235
- req = UploadFileMultipartData(
236
- files=File(file_name=url.split("/")[-1], payload=io.BytesIO(data)),
237
- channel_id=channel_id,
238
- )
239
- r = await upload_file.asyncio(client=self.http, multipart_data=req)
240
- if (
241
- r is None
242
- or isinstance(r, Unset)
243
- or r.file_infos is None
244
- or isinstance(r.file_infos, Unset)
245
- or len(r.file_infos) != 1
246
- ):
247
- raise RuntimeError(r)
248
- return r.file_infos[0].id
249
-
250
- async def react(self, post_id: str, emoji_char: str):
251
- return await save_reaction.asyncio(
252
- client=self.http,
253
- json_body=Reaction(
254
- user_id=await self.mm_id,
255
- post_id=post_id,
256
- emoji_name=demojize(emoji_char),
257
- ),
258
- )
259
-
260
- async def get_reactions(self, post_id: str):
261
- try:
262
- return await get_reactions.asyncio(post_id, client=self.http)
263
- except TypeError:
264
- return []
265
-
266
- async def delete_reaction(self, post_id: str, emoji_name: str):
267
- await delete_reaction.asyncio(
268
- await self.mm_id, post_id, emoji_name=emoji_name, client=self.http
269
- )
270
-
271
- async def view_channel(self, channel_id: str):
272
- await view_channel.asyncio(
273
- await self.mm_id,
274
- client=self.http,
275
- json_body=ViewChannelJsonBody(channel_id=channel_id),
276
- )
277
-
278
-
279
- def demojize(emoji_char: str):
280
- # TODO: find a better when than these non standard emoji aliases replace
281
- return (
282
- emoji.demojize(emoji_char, delimiters=("", ""), language="alias")
283
- .replace("_three_", "_3_")
284
- .replace("thumbsup", "+1")
285
- )
286
-
287
-
288
- log = logging.getLogger(__name__)