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.
- hyperot_adapter_onebot/__init__.py +5 -0
- hyperot_adapter_onebot/actions.py +650 -0
- hyperot_adapter_onebot/adapter.py +193 -0
- hyperot_adapter_onebot/api.py +177 -0
- hyperot_adapter_onebot/config.py +51 -0
- hyperot_adapter_onebot/events.py +489 -0
- hyperot_adapter_onebot/manifest.json +12 -0
- hyperot_adapter_onebot/segments.py +566 -0
- hyperot_adapter_onebot/transport.py +402 -0
- hyperot_adapter_onebot-2.0.0.dist-info/METADATA +33 -0
- hyperot_adapter_onebot-2.0.0.dist-info/RECORD +14 -0
- hyperot_adapter_onebot-2.0.0.dist-info/WHEEL +5 -0
- hyperot_adapter_onebot-2.0.0.dist-info/entry_points.txt +2 -0
- hyperot_adapter_onebot-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -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
|