hyperot-adapter-onebot 2.0.0__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.
@@ -0,0 +1,193 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections import OrderedDict
5
+ from contextlib import suppress
6
+ from typing import TypeVar
7
+
8
+ from pydantic import JsonValue
9
+
10
+ from hyperot.v2.actions import Action
11
+ from hyperot.v2.adapter import (
12
+ ActionRegistry,
13
+ AdapterManifest,
14
+ ExtensionRegistry,
15
+ load_manifest,
16
+ )
17
+ from hyperot.v2.common import FileId, GroupId, MessageId, SceneType
18
+ from hyperot.v2.events import Event, GroupInvitationReceivedEvent, GroupJoinRequestedEvent
19
+
20
+ from .actions import OneBotActions
21
+ from .api import OneBotAPI
22
+ from .config import OneBotConfig
23
+ from .events import (
24
+ OneBotFileUploadedEvent,
25
+ OneBotMessageReceivedEvent,
26
+ translate_event,
27
+ )
28
+ from .segments import OneBotFile, OneBotSegmentCodec
29
+ from .transport import ActionTransport, HTTPPostTransport, build_transport
30
+
31
+ ResultT = TypeVar("ResultT")
32
+
33
+
34
+ class _ExtensionContext:
35
+ def __init__(self, adapter: OneBotAdapter) -> None:
36
+ self.adapter = adapter
37
+
38
+ async def execute(self, action: Action[ResultT]) -> ResultT:
39
+ return await self.adapter.execute(action)
40
+
41
+
42
+ class OneBotAdapter:
43
+ def __init__(self) -> None:
44
+ self.manifest: AdapterManifest = load_manifest("hyperot_adapter_onebot")
45
+ self.config_type = OneBotConfig
46
+ self.segment_codec = OneBotSegmentCodec()
47
+ self.actions = ActionRegistry()
48
+ self.extensions = ExtensionRegistry(_ExtensionContext(self))
49
+ self.api_type = OneBotAPI
50
+ self._config: OneBotConfig | None = None
51
+ self._transports: list[ActionTransport] = []
52
+ self._action_transport: ActionTransport | None = None
53
+ self._running = False
54
+ self._message_groups: OrderedDict[str, GroupId] = OrderedDict()
55
+ self._request_subtypes: OrderedDict[str, str] = OrderedDict()
56
+ self._file_context: OrderedDict[str, dict[str, JsonValue]] = OrderedDict()
57
+
58
+ async def start(self, config: OneBotConfig) -> None:
59
+ if self._running:
60
+ return
61
+ self._config = config
62
+ self._transports = [build_transport(connection) for connection in config.connections]
63
+ if not self._transports:
64
+ raise ValueError("OneBot adapter requires at least one connection")
65
+ started: list[ActionTransport] = []
66
+ try:
67
+ for transport in self._transports:
68
+ await transport.start()
69
+ started.append(transport)
70
+ except Exception:
71
+ for transport in reversed(started):
72
+ with suppress(Exception):
73
+ await transport.stop()
74
+ raise
75
+ self._action_transport = next(
76
+ (transport for transport in self._transports if not isinstance(transport, HTTPPostTransport)),
77
+ None,
78
+ )
79
+ self.actions = ActionRegistry()
80
+ if self._action_transport is not None:
81
+ builder = OneBotActions(
82
+ self._action_transport,
83
+ self.segment_codec,
84
+ config.action_timeout,
85
+ message_group_lookup=self._message_group_lookup,
86
+ message_group_remember=self._remember_message_group,
87
+ request_subtype_lookup=self._request_subtype_lookup,
88
+ file_context_lookup=self._file_context_lookup,
89
+ )
90
+ builder.register_all(self.actions)
91
+ self._running = True
92
+
93
+ async def stop(self) -> None:
94
+ if not self._running:
95
+ return
96
+ self._running = False
97
+ for transport in reversed(self._transports):
98
+ await transport.stop()
99
+ self._transports.clear()
100
+ self._action_transport = None
101
+ self.extensions.clear()
102
+
103
+ async def receive(self) -> Event:
104
+ if not self._transports:
105
+ from hyperot.v2.common import AdapterDisconnectedError
106
+
107
+ raise AdapterDisconnectedError("OneBot adapter is not started")
108
+ while True:
109
+ if len(self._transports) == 1:
110
+ payload = await self._transports[0].receive()
111
+ else:
112
+ tasks = [asyncio.create_task(transport.receive()) for transport in self._transports]
113
+ done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
114
+ for task in pending:
115
+ task.cancel()
116
+ if pending:
117
+ await asyncio.gather(*pending, return_exceptions=True)
118
+ payload = done.pop().result()
119
+ event = translate_event(payload, self.segment_codec, self._message_group_lookup)
120
+ if event is not None:
121
+ self._remember_event_context(event)
122
+ return event
123
+
124
+ async def execute(self, action: Action[ResultT]) -> ResultT:
125
+ handler = self.actions.get(type(action))
126
+ return await handler(action)
127
+
128
+ def _require_action_transport(self) -> ActionTransport:
129
+ if self._action_transport is None:
130
+ raise RuntimeError("OneBot adapter has no action transport")
131
+ return self._action_transport
132
+
133
+ def _remember_message_group(self, message_id: MessageId, group_id: GroupId) -> None:
134
+ self._message_groups[str(message_id)] = group_id
135
+ self._message_groups.move_to_end(str(message_id))
136
+ while len(self._message_groups) > 8192:
137
+ self._message_groups.popitem(last=False)
138
+
139
+ def _message_group_lookup(self, message_id: MessageId) -> GroupId | None:
140
+ group_id = self._message_groups.get(str(message_id))
141
+ if group_id is not None:
142
+ self._message_groups.move_to_end(str(message_id))
143
+ return group_id
144
+
145
+ def _remember_request_subtype(self, request_id: str, sub_type: str) -> None:
146
+ self._request_subtypes[request_id] = sub_type
147
+ self._request_subtypes.move_to_end(request_id)
148
+ while len(self._request_subtypes) > 1024:
149
+ self._request_subtypes.popitem(last=False)
150
+
151
+ def _request_subtype_lookup(self, request_id: str) -> str | None:
152
+ sub_type = self._request_subtypes.get(request_id)
153
+ if sub_type is not None:
154
+ self._request_subtypes.move_to_end(request_id)
155
+ return sub_type
156
+
157
+ def _remember_file_context(self, file_id: FileId, **values: JsonValue) -> None:
158
+ key = str(file_id)
159
+ context = self._file_context.setdefault(key, {})
160
+ context.update({name: value for name, value in values.items() if value is not None})
161
+ self._file_context.move_to_end(key)
162
+ while len(self._file_context) > 4096:
163
+ self._file_context.popitem(last=False)
164
+
165
+ def _file_context_lookup(self, file_id: FileId) -> dict[str, JsonValue] | None:
166
+ context = self._file_context.get(str(file_id))
167
+ if context is not None:
168
+ self._file_context.move_to_end(str(file_id))
169
+ return dict(context)
170
+ return None
171
+
172
+ def _remember_event_context(self, event: Event) -> None:
173
+ if isinstance(event, OneBotMessageReceivedEvent):
174
+ if event.scene_type == SceneType.GROUP:
175
+ self._remember_message_group(event.message_id, GroupId(str(event.scene_id)))
176
+ for segment in event.message:
177
+ if isinstance(segment, OneBotFile) and segment.file_id is not None:
178
+ self._remember_file_context(segment.file_id, file_hash=segment.file_hash)
179
+ elif isinstance(event, OneBotFileUploadedEvent):
180
+ if str(event.file.file_id):
181
+ self._remember_file_context(
182
+ event.file.file_id,
183
+ busid=event.busid,
184
+ file_hash=event.file_hash,
185
+ )
186
+ elif isinstance(event, GroupJoinRequestedEvent):
187
+ self._remember_request_subtype(str(event.request_id), "add")
188
+ elif isinstance(event, GroupInvitationReceivedEvent):
189
+ self._remember_request_subtype(str(event.request_id), "invite")
190
+
191
+
192
+ def create_adapter() -> OneBotAdapter:
193
+ return OneBotAdapter()
@@ -0,0 +1,177 @@
1
+ from __future__ import annotations
2
+
3
+ from typing_extensions import override
4
+
5
+ from hyperot.v2.actions import (
6
+ CookieInfo,
7
+ CsrfTokenInfo,
8
+ FileReference,
9
+ FileUrl,
10
+ MuteMemberAction,
11
+ ReactMessageAction,
12
+ )
13
+ from hyperot.v2.api import BotAPI, ClientAPI, FileAPI, GroupAPI, GroupMemberAPI, MessageAPI, UserAPI
14
+ from hyperot.v2.common import CapabilityNotSupportedError, FileId, GroupId, MessageId, SceneId, SceneType, UserId
15
+
16
+ from .actions import (
17
+ GetGroupFileUrlAction,
18
+ GetPrivateFileUrlAction,
19
+ GroupReactionAction,
20
+ OneBotKickMemberAction,
21
+ OneBotLeaveGroupAction,
22
+ OneBotSetMemberTitleAction,
23
+ )
24
+
25
+
26
+ class OneBotUserAPI(UserAPI):
27
+ async def send_like(self, times: int = 1) -> None:
28
+ from hyperot.v2.actions import SendLikeAction
29
+
30
+ await self._context.execute(SendLikeAction(user_id=self.user_id, times=times))
31
+
32
+
33
+ class OneBotGroupMemberAPI(GroupMemberAPI):
34
+ @override
35
+ async def kick(self, reject_add_request: bool = False) -> None:
36
+ await self._context.execute(
37
+ OneBotKickMemberAction(
38
+ group_id=self.group_id,
39
+ user_id=self.user_id,
40
+ reject_add_request=reject_add_request,
41
+ )
42
+ )
43
+
44
+ @override
45
+ async def mute(self, duration: int = 1800) -> None:
46
+ await self._context.execute(
47
+ MuteMemberAction(
48
+ group_id=self.group_id,
49
+ user_id=self.user_id,
50
+ duration=duration,
51
+ )
52
+ )
53
+
54
+ @override
55
+ async def set_card(self, card: str) -> None:
56
+ from hyperot.v2.actions import SetMemberCardAction
57
+
58
+ await self._context.execute(
59
+ SetMemberCardAction(
60
+ group_id=self.group_id,
61
+ user_id=self.user_id,
62
+ card=card,
63
+ )
64
+ )
65
+
66
+ @override
67
+ async def set_title(self, title: str, duration: int = -1) -> None:
68
+ await self._context.execute(
69
+ OneBotSetMemberTitleAction(
70
+ group_id=self.group_id,
71
+ user_id=self.user_id,
72
+ title=title,
73
+ duration=duration,
74
+ )
75
+ )
76
+
77
+
78
+ class OneBotGroupAPI(GroupAPI):
79
+ @override
80
+ def member(self, user_id: str | int) -> OneBotGroupMemberAPI:
81
+ return OneBotGroupMemberAPI(self._context, self.group_id, UserId(str(user_id)))
82
+
83
+ async def reaction(self, message_id: str | int, reaction: str) -> None:
84
+ await self._context.execute(
85
+ GroupReactionAction(
86
+ group_id=self.group_id,
87
+ message_id=MessageId(str(message_id)),
88
+ reaction=reaction,
89
+ enabled=True,
90
+ )
91
+ )
92
+
93
+ @override
94
+ async def leave(self, is_dismiss: bool = False) -> None:
95
+ await self._context.execute(
96
+ OneBotLeaveGroupAction(
97
+ group_id=self.group_id,
98
+ is_dismiss=is_dismiss,
99
+ )
100
+ )
101
+
102
+
103
+ class OneBotMessageAPI(MessageAPI):
104
+ async def group_reaction(self, reaction: str) -> None:
105
+ await self._context.execute(
106
+ ReactMessageAction(
107
+ message_id=self.message_id,
108
+ reaction=reaction,
109
+ enabled=True,
110
+ )
111
+ )
112
+
113
+
114
+ class OneBotFileAPI(FileAPI):
115
+ @override
116
+ async def info(self) -> FileReference:
117
+ raise CapabilityNotSupportedError("OneBot has no generic file info endpoint")
118
+
119
+ @override
120
+ async def download(self) -> FileUrl:
121
+ raise CapabilityNotSupportedError("OneBot has no generic file download endpoint")
122
+
123
+ async def group_url(self, group_id: str | int) -> FileUrl:
124
+ return await self._context.execute(
125
+ GetGroupFileUrlAction(
126
+ group_id=GroupId(str(group_id)),
127
+ file_id=self.file_id,
128
+ )
129
+ )
130
+
131
+ async def private_url(self, user_id: str | int, file_hash: str | None = None) -> FileUrl:
132
+ return await self._context.execute(
133
+ GetPrivateFileUrlAction(
134
+ user_id=UserId(str(user_id)),
135
+ file_id=self.file_id,
136
+ file_hash=file_hash,
137
+ )
138
+ )
139
+
140
+
141
+ class OneBotBotAPI(BotAPI):
142
+ async def cookies(self, domain: str) -> CookieInfo:
143
+ from hyperot.v2.actions import GetCookieAction
144
+
145
+ return await self._context.execute(GetCookieAction(domain=domain))
146
+
147
+ async def csrf_token(self) -> CsrfTokenInfo:
148
+ from hyperot.v2.actions import GetCsrfTokenAction
149
+
150
+ return await self._context.execute(GetCsrfTokenAction())
151
+
152
+
153
+ class OneBotAPI(ClientAPI):
154
+ @override
155
+ def scene(self, scene_type: SceneType, scene_id: str | int):
156
+ return super().scene(scene_type, scene_id)
157
+
158
+ @override
159
+ def user(self, user_id: str | int) -> OneBotUserAPI:
160
+ return OneBotUserAPI(self._context, SceneType.USER, SceneId(str(user_id)), UserId(str(user_id)))
161
+
162
+ @override
163
+ def group(self, group_id: str | int) -> OneBotGroupAPI:
164
+ return OneBotGroupAPI(self._context, SceneType.GROUP, SceneId(str(group_id)), GroupId(str(group_id)))
165
+
166
+ @override
167
+ def message(self, message_id: str | int) -> OneBotMessageAPI:
168
+ return OneBotMessageAPI(self._context, MessageId(str(message_id)))
169
+
170
+ @override
171
+ def file(self, file_id: str | int) -> OneBotFileAPI:
172
+ return OneBotFileAPI(self._context, FileId(str(file_id)))
173
+
174
+ @override
175
+ @property
176
+ def bot(self) -> OneBotBotAPI:
177
+ return OneBotBotAPI(self._context)
@@ -0,0 +1,51 @@
1
+ from typing import Annotated, Literal
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+
6
+ class ConnectionConfig(BaseModel):
7
+ model_config = ConfigDict(frozen=True, extra="forbid")
8
+
9
+ access_token: str = ""
10
+
11
+
12
+ class ForwardWebSocketConfig(ConnectionConfig):
13
+ type: Literal["ForwardWebSocket"] = "ForwardWebSocket"
14
+ url: str
15
+ ping_interval: float | None = 20.0
16
+ ping_timeout: float | None = 20.0
17
+
18
+
19
+ class ReverseWebSocketConfig(ConnectionConfig):
20
+ type: Literal["ReverseWebSocket"] = "ReverseWebSocket"
21
+ host: str = "127.0.0.1"
22
+ port: int = 6700
23
+ api_path: str = "/api"
24
+ event_path: str = "/event"
25
+
26
+
27
+ class HTTPConfig(ConnectionConfig):
28
+ type: Literal["HTTP"] = "HTTP"
29
+ url: str
30
+ timeout: float = 30.0
31
+
32
+
33
+ class HTTPPostConfig(ConnectionConfig):
34
+ type: Literal["HTTPPost"] = "HTTPPost"
35
+ host: str = "127.0.0.1"
36
+ port: int = 6701
37
+ endpoint: str = "/"
38
+ secret: str = ""
39
+
40
+
41
+ OneBotConnectionConfig = Annotated[
42
+ ForwardWebSocketConfig | ReverseWebSocketConfig | HTTPConfig | HTTPPostConfig,
43
+ Field(discriminator="type"),
44
+ ]
45
+
46
+
47
+ class OneBotConfig(BaseModel):
48
+ model_config = ConfigDict(frozen=True, extra="forbid")
49
+
50
+ connections: tuple[OneBotConnectionConfig, ...] = (ForwardWebSocketConfig(url="ws://127.0.0.1:5004"),)
51
+ action_timeout: float = 30.0