brominecore 0.1__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.
brcore/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from brcore.core import (
2
+ Bromine,
3
+ )
4
+
5
+ __all__ = ["Bromine"]
brcore/core.py ADDED
@@ -0,0 +1,365 @@
1
+ import json
2
+ import asyncio
3
+ import uuid
4
+ import logging
5
+ from functools import partial
6
+ from typing import Any, Callable, NoReturn, Optional, Union, Coroutine
7
+
8
+ import websockets
9
+
10
+
11
+ __all__ = ["Bromine"]
12
+
13
+
14
+ class _BackgroundTasks(set):
15
+ """`runner`のバックグラウンド実行するタスクの管理をする集合"""
16
+ def add(self, element: asyncio.Task) -> None:
17
+ if type(element) is asyncio.Task:
18
+ element.add_done_callback(self.discard)
19
+ return super().add(element)
20
+ else:
21
+ raise TypeError("element is not asyncio.Task")
22
+
23
+ def tasks_cancel(self) -> None:
24
+ """タスク達をキャンセル"""
25
+ for i in self:
26
+ i.cancel()
27
+
28
+
29
+ class Bromine:
30
+ """misskeyのwebsocketAPIを使いやすくしたクラス
31
+
32
+ websocketの実装を一々作らなくても簡単にwebsocketの通信ができるようになります
33
+
34
+ Parameters
35
+ ----------
36
+ instance: str
37
+ インスタンス名
38
+ token: :obj:`str`, optional
39
+ トークン"""
40
+
41
+ def __init__(self, instance: str, token: Optional[str] = None) -> None:
42
+ self.__COOL_TIME = 5
43
+
44
+ # 値を保持するキューとか
45
+ # uuid:[channelname, awaitablefunc, params]
46
+ self.__channels: dict[str, tuple[str, Callable[[dict[str, Any]], Coroutine[Any, Any, None]], dict[str, Any]]] = {}
47
+ # uuid:tuple[isblock, awaitablefunc]
48
+ self.__on_comebacks: dict[str, tuple[bool, Callable[[], Coroutine[Any, Any, None]]]] = {}
49
+ # send_queueはここで作るとエラーが出るので型ヒントのみ
50
+ self.__send_queue: asyncio.Queue[tuple[str, dict]]
51
+
52
+ # 実行中かどうかの変数
53
+ self.__is_running: bool = False
54
+
55
+ # websocketのURL
56
+ if token is not None:
57
+ self.__WS_URL = f'wss://{instance}/streaming?i={token}'
58
+ else:
59
+ # トークンがないときはパラメーターを消しとく
60
+ self.__WS_URL = f'wss://{instance}/streaming'
61
+
62
+ # logger作成
63
+ self.__logger = logging.getLogger("Bromine")
64
+ # logを簡単にできるよう部分適用する
65
+ self.__log = partial(self.__logger.log, logging.DEBUG)
66
+
67
+ @property
68
+ def loglevel(self) -> int:
69
+ """現在のログレベル"""
70
+ return self.__log.args[0]
71
+
72
+ @loglevel.setter
73
+ def loglevel(self, level: int) -> None:
74
+ self.__log = partial(self.__logger.log, level)
75
+
76
+ @property
77
+ def cooltime(self) -> int:
78
+ """websocketの接続が切れた時に再接続まで待つ時間"""
79
+ return self.__COOL_TIME
80
+
81
+ @cooltime.setter
82
+ def cooltime(self, time: int) -> None:
83
+ if time > 0:
84
+ self.__COOL_TIME = time
85
+ else:
86
+ ValueError("負の値です")
87
+
88
+ @property
89
+ def is_running(self) -> bool:
90
+ """メイン関数が実行中かどうか"""
91
+ return self.__is_running
92
+
93
+ async def main(self) -> NoReturn:
94
+ """開始する関数"""
95
+ self.__log("start main.")
96
+ # send_queueをinitで作るとattached to a different loopとかいうゴミでるのでここで宣言
97
+ self.__send_queue = asyncio.Queue()
98
+ self.__is_running = True
99
+ # バックグラウンドタスクの集合
100
+ backgrounds = _BackgroundTasks()
101
+ try:
102
+ await asyncio.create_task(self.__runner(backgrounds))
103
+ finally:
104
+ backgrounds.tasks_cancel()
105
+ self.__is_running = False
106
+ self.__log("finish main.")
107
+
108
+ async def __runner(self, background_tasks: _BackgroundTasks) -> NoReturn:
109
+ """websocketとの交信を行うメインdaemon"""
110
+ # 何回連続で接続に失敗したかのカウンター
111
+ connect_fail_count = 0
112
+ # この変数たちは最初に接続失敗すると未定義になるから保険のため
113
+ # websocket_daemon(__ws_send_d)
114
+ wsd: Union[None, asyncio.Task] = None
115
+ # comebacks(asyncio.gather)
116
+ comebacks: Union[None, asyncio.Future] = None
117
+ while True:
118
+ try:
119
+ async with websockets.connect(self.__WS_URL) as ws:
120
+ # ちゃんと通ってるかpingで確認
121
+ ping_wait = await ws.ping()
122
+ pong_latency = await ping_wait
123
+ self.__log(f"websocket connect success. latency: {pong_latency}s")
124
+
125
+ # 送るdaemonの作成
126
+ wsd = asyncio.create_task(self.__ws_send_d(ws))
127
+
128
+ # comebacksの処理
129
+ cmbs: list[Coroutine[Any, Any, None]] = []
130
+ for i in self.__on_comebacks.values():
131
+ if i[0]:
132
+ # もしブロックしなければいけないcomebackなら待つ
133
+ await i[1]()
134
+ else:
135
+ # でなければ後で処理する
136
+ cmbs.append(i[1]())
137
+ if cmbs != []:
138
+ # 全部一気にgatherで管理
139
+ comebacks = asyncio.gather(*cmbs, return_exceptions=True)
140
+
141
+ # 接続に成功したということでfail_countを0に
142
+ connect_fail_count = 0
143
+ while True:
144
+ # データ受け取り
145
+ data = json.loads(await ws.recv())
146
+ if data['type'] == 'channel':
147
+ for i, v in self.__channels.items():
148
+ if data["body"]["id"] == i:
149
+ background_tasks.add(asyncio.create_task(v[1](data["body"])))
150
+ break
151
+ else:
152
+ self.__log("data come from unknown channel")
153
+ else:
154
+ # たまにchannel以外から来ることがある(謎)
155
+ self.__log(f"data come from not channel, datatype[{data['type']}]")
156
+
157
+ except (
158
+ asyncio.exceptions.TimeoutError,
159
+ websockets.exceptions.ConnectionClosed,
160
+ websockets.exceptions.ConnectionClosedError,
161
+ websockets.exceptions.ConnectionClosedOK,
162
+ ) as e:
163
+ # websocketが死んだりタイムアウトした時の処理
164
+ self.__log(f"error occured:{e}")
165
+ connect_fail_count += 1
166
+ await asyncio.sleep(self.__COOL_TIME)
167
+ if connect_fail_count > 5:
168
+ # Todo: 例外を投げる?
169
+ # 5回以上連続で失敗したとき長く寝るようにする
170
+ # とりあえず30待つようにする
171
+ await asyncio.sleep(30)
172
+
173
+ except Exception as e:
174
+ # 予定外のエラー発生時。
175
+ self.__log(f"fatal Error:{type(e)}, args:{e.args}")
176
+ raise e
177
+
178
+ finally:
179
+ # 再接続する際、いろいろ初期化する
180
+ if type(wsd) is asyncio.Task:
181
+ # __ws_send_dを止める
182
+ wsd.cancel()
183
+ try:
184
+ await wsd
185
+ except asyncio.CancelledError:
186
+ pass
187
+ wsd = None
188
+ if comebacks is not None:
189
+ # ブロックしないcomebacksがもし生きていたら殺す
190
+ comebacks.cancel()
191
+ try:
192
+ await comebacks
193
+ except asyncio.CancelledError:
194
+ pass
195
+ comebacks = None
196
+
197
+ def add_comeback(self,
198
+ func: Callable[[], Coroutine[Any, Any, None]],
199
+ block: bool = False,
200
+ id: Optional[str] = None) -> str:
201
+ """comebackを作る関数
202
+
203
+ Parameters
204
+ ----------
205
+ func: CoroutineFunction
206
+ comeback時に実行する非同期関数
207
+ block: bool
208
+ websocketとの交信をブロッキングして実行するか
209
+ id: :obj:`str`, optional
210
+ 識別id、ない場合自動生成される
211
+
212
+ Returns
213
+ -------
214
+ str
215
+ 識別id
216
+
217
+ Raises
218
+ ------
219
+ ValueError
220
+ 非同期関数funcがcoroutinefunctionでない時
221
+
222
+ Note
223
+ ----
224
+ 返り値の識別idはdel_comebackで使用します"""
225
+ if id is None:
226
+ # もしIDがない時生成する
227
+ id = uuid.uuid4()
228
+ if not asyncio.iscoroutinefunction(func):
229
+ raise ValueError("非同期関数funcがcoroutinefunctionではありません。")
230
+ self.__on_comebacks[id] = (block, func)
231
+ return id
232
+
233
+ def del_comeback(self, id: str) -> None:
234
+ """comeback消すやつ
235
+
236
+ Parameter
237
+ ---------
238
+ id: str
239
+ 識別id
240
+
241
+ Raises
242
+ ------
243
+ KeyError
244
+ 識別idが不適のとき"""
245
+ self.__on_comebacks.pop(id)
246
+
247
+ async def __ws_send_d(self, ws: websockets.WebSocketClientProtocol) -> NoReturn:
248
+ """websocketを送るdaemon"""
249
+ # すでに接続済みのchannelにconnectしたりしないようにするやつ
250
+ already_connected_ids: set[str] = set()
251
+ # まずはchannelsの再接続から始める
252
+ for i, v in self.__channels.items():
253
+ already_connected_ids.add(i)
254
+ await ws.send(json.dumps({
255
+ "type": "connect",
256
+ "body": {
257
+ "channel": v[0],
258
+ "id": i,
259
+ "params": v[2]
260
+ }
261
+ }))
262
+
263
+ # queueの初期化
264
+ while not self.__send_queue.empty():
265
+ type_, body_ = await self.__send_queue.get()
266
+ if type_ == "connect":
267
+ if body_["id"] in already_connected_ids:
268
+ # もうすでに送ったやつなので送らない
269
+ continue
270
+ else:
271
+ # 追加する
272
+ already_connected_ids.add(body_["id"])
273
+
274
+ await ws.send(json.dumps({
275
+ "type": type_,
276
+ "body": body_
277
+ }))
278
+
279
+ # あとはずっとqueueからgetしてそれを送る。
280
+ while True:
281
+ type_, body_ = await self.__send_queue.get()
282
+ await ws.send(json.dumps({
283
+ "type": type_,
284
+ "body": body_
285
+ }))
286
+
287
+ def ws_send(self, type: str, body: dict[str, Any]) -> None:
288
+ """ウェブソケットへ送るキューに情報を追加するやつ
289
+
290
+ Parameters
291
+ ----------
292
+ type: str
293
+ type情報
294
+ body: dict[str, Any]
295
+ body情報"""
296
+ self.__send_queue.put_nowait((type, body))
297
+
298
+ def ws_connect(self,
299
+ channel: str,
300
+ func: Callable[[dict[str, Any]], Coroutine[Any, Any, None]],
301
+ id: Optional[str] = None,
302
+ **params: Any) -> str:
303
+ """channelに接続する関数
304
+
305
+ Parameters
306
+ ----------
307
+ channel: str
308
+ チャンネル名
309
+ func: CoroutineFunction
310
+ 反応があった時に実行される非同期関数
311
+ id: :obj:`str`, optional
312
+ 識別id、もし指定されていない場合、自動生成される
313
+ **params: Any
314
+ 接続する際のパラメーター
315
+
316
+ Returns
317
+ -------
318
+ str
319
+ 識別id
320
+
321
+ Raises
322
+ -------
323
+ ValueError
324
+ 非同期関数funcがcoroutinefunctionでない時
325
+
326
+ Note
327
+ ----
328
+ 返り値の識別idはws_disconnectで使用します"""
329
+ if not asyncio.iscoroutinefunction(func):
330
+ raise ValueError("非同期関数funcがcoroutinefunctionではありません。")
331
+ if id is None:
332
+ # idがなかったら自動生成
333
+ id = str(uuid.uuid4())
334
+ # channelsに追加
335
+ self.__channels[id] = (channel, func, params)
336
+ body = {
337
+ "channel": channel,
338
+ "id": id,
339
+ "params": params
340
+ }
341
+ if self.__is_running:
342
+ # もしsend_queueがある時(実行中の時)
343
+ self.ws_send("connect", body)
344
+ self.__log(f"connect channel: {channel}, id: {id}")
345
+ else:
346
+ # ない時(実行前)
347
+ self.__log(f"connect channel before run: {channel}, id: {id}")
348
+ return id
349
+
350
+ def ws_disconnect(self, id: str) -> None:
351
+ """チャンネルを接続解除する関数
352
+
353
+ Parameters
354
+ ----------
355
+ id: str
356
+ 識別id
357
+
358
+ Raises
359
+ ------
360
+ KeyError
361
+ 識別idが不適のとき"""
362
+ channel = self.__channels.pop(id)[0]
363
+ body = {"id": id}
364
+ self.ws_send("disconnect", body)
365
+ self.__log(f"disconnect channel: {channel}, id: {id}")
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 35enidoi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.1
2
+ Name: brominecore
3
+ Version: 0.1
4
+ Summary: Misskey websocketAPI wrapper.
5
+ Author: iodine53
6
+ Maintainer: iodine53
7
+ License: MIT License
8
+ Project-URL: Repository, https://github.com/35enidoi/BromineCore
9
+ Project-URL: Issues, https://github.com/35enidoi/BromineCore/issues
10
+ Keywords: misskey
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: websockets
20
+
21
+ # BromineCore
22
+ [ぶろみね](https://github.com/35enidoi/bromine35bot)くんのコア部分の実装、そしてmisskeyのwebsocketAPI単体の実装です。
23
+ 一々websocketの実装を作らなくても良くなります!
24
+
25
+ ローカルのノートを講読したり、通知を取得したり。リバーシも頑張れば実装できます。
26
+
27
+ 何か問題が発生したり追加してほしい機能があったらissueに書いてください
28
+ 頑張って実装したり解決します
29
+ # Example
30
+ 簡単なタイムライン閲覧クライアントです。
31
+ トークン無しでタイムラインをリアルタイムで閲覧できます。
32
+ ```py
33
+ import asyncio
34
+ from brcore import Bromine
35
+
36
+
37
+ INSTANCE = "misskey.io"
38
+ TL = "localTimeline"
39
+
40
+
41
+ def note_printer(note: dict) -> None:
42
+ """ノートの情報を受け取って表示する関数"""
43
+ NOBASIBOU_LENGTH = 20
44
+ user = note["user"]
45
+ username = user["name"] if user["name"] is not None else user["username"]
46
+ print("-"*NOBASIBOU_LENGTH)
47
+ if note.get("renoteId") and note["text"] is None:
48
+ # リノートのときはリノート先だけ書く
49
+ print(f"{username}がリノート")
50
+ note_printer(note["renote"])
51
+ # リノートはリアクション数とか書きたくないので
52
+ # ここで返す
53
+ print("-"*NOBASIBOU_LENGTH)
54
+ return
55
+ else:
56
+ # 普通のノート
57
+ print(f"{username}がノート ノートid: {note['id']}")
58
+ if note.get("reply"):
59
+ # リプライがある場合
60
+ print("リプライ:")
61
+ note_printer(note["reply"])
62
+ if note.get("text"):
63
+ print("テキスト:")
64
+ print(note["text"])
65
+ if note.get("renoteId"):
66
+ # 引用
67
+ print("引用:")
68
+ note_printer(note["renote"])
69
+ if len(note["files"]) != 0:
70
+ # ファイルがある時
71
+ print(f"ファイル数: {len(note['files'])}")
72
+ # リアクションとかを書く
73
+ print(f"リプライ数: {note['repliesCount']}, リノート数: {note['renoteCount']}, リアクション数: {note['reactionCount']}")
74
+ reactions = []
75
+ for reactionid, val in note["reactions"].items():
76
+ if reactionid[-3:] == "@.:":
77
+ # ローカルのカスタム絵文字のidはへんなのついてるので
78
+ # それを消す
79
+ reactionid = reactionid[:-3] + ":"
80
+ reactions.append(f"({reactionid}, {val})")
81
+ if len(reactions) != 0:
82
+ print("リアクション達: ", ", ".join(reactions))
83
+ print("-"*NOBASIBOU_LENGTH)
84
+
85
+
86
+ async def note_async(note: dict) -> None:
87
+ """上のprinterの引数を調整するやつ
88
+
89
+ asyncにするのはws_connectでは非同期関数が求められるので(見た目非同期っていう体にしているだけ)"""
90
+ note_printer(note["body"])
91
+ print() # 空白をノート後に入れておく
92
+
93
+
94
+ async def main() -> None:
95
+ brm = Bromine(instance=INSTANCE)
96
+ brm.ws_connect(TL, note_async)
97
+ print("start...")
98
+ await brm.main()
99
+
100
+
101
+ if __name__ == "__main__":
102
+ try:
103
+ asyncio.run(main())
104
+ except KeyboardInterrupt:
105
+ print("fin")
106
+
107
+ ```
@@ -0,0 +1,7 @@
1
+ brcore/__init__.py,sha256=vbZ2-tcnFCu14zO9FuKqMgt7HyEEr3xrFAKqTVSCcQY,64
2
+ brcore/core.py,sha256=4nZ64agGSUwmYrS_XMtxRymcHu4i0kqhZbRmH8Z2zmU,13070
3
+ brominecore-0.1.dist-info/LICENSE,sha256=9z-qttWTySG7CeV72m9XyX8AEO-VG-xogWlFLQVLSQw,1065
4
+ brominecore-0.1.dist-info/METADATA,sha256=_1yRgCtFKssjSKf22HEy0bLNWa_5C_nrM4TWzKaSOt8,3806
5
+ brominecore-0.1.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
6
+ brominecore-0.1.dist-info/top_level.txt,sha256=q-RFjELVjiz1gXVNoUP1MamVSQBUjEd5QZ3swzSU5X8,7
7
+ brominecore-0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (72.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ brcore