2021-01-30 19:11:17 +08:00
|
|
|
from datetime import datetime, timedelta
|
2021-01-30 20:40:00 +08:00
|
|
|
from io import BytesIO
|
2021-01-30 05:58:30 +08:00
|
|
|
from ipaddress import IPv4Address
|
2021-01-30 20:40:00 +08:00
|
|
|
from typing import Any, Dict, List, NoReturn, Optional, Tuple
|
2021-01-30 05:58:30 +08:00
|
|
|
|
|
|
|
import httpx
|
2021-01-29 17:37:44 +08:00
|
|
|
|
|
|
|
from nonebot.adapters import Bot as BaseBot
|
2021-01-29 21:19:13 +08:00
|
|
|
from nonebot.adapters import Event as BaseEvent
|
2021-01-30 13:36:31 +08:00
|
|
|
from nonebot.config import Config
|
2021-01-30 19:11:17 +08:00
|
|
|
from nonebot.drivers import Driver, WebSocket
|
2021-01-30 05:58:30 +08:00
|
|
|
from nonebot.exception import RequestDenied
|
2021-01-30 21:51:51 +08:00
|
|
|
from nonebot.exception import ActionFailed as BaseActionFailed
|
2021-01-30 05:58:30 +08:00
|
|
|
from nonebot.log import logger
|
2021-01-29 21:19:13 +08:00
|
|
|
from nonebot.message import handle_event
|
2021-01-29 17:37:44 +08:00
|
|
|
from nonebot.typing import overrides
|
|
|
|
|
2021-01-30 13:36:31 +08:00
|
|
|
from .config import Config as MiraiConfig
|
2021-01-30 20:40:00 +08:00
|
|
|
from .event import Event, FriendMessage, GroupMessage, TempMessage
|
|
|
|
from .message import MessageChain, MessageSegment
|
2021-01-29 21:19:13 +08:00
|
|
|
|
2021-01-30 05:58:30 +08:00
|
|
|
|
2021-01-30 21:51:51 +08:00
|
|
|
class ActionFailed(BaseActionFailed):
|
|
|
|
|
|
|
|
def __init__(self, code: int, message: str = ''):
|
|
|
|
super().__init__('mirai')
|
|
|
|
self.code = code
|
|
|
|
self.message = message
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return f"{self.__class__.__name__}(code={self.code}, message={self.message!r})"
|
|
|
|
|
|
|
|
|
2021-01-30 19:11:17 +08:00
|
|
|
class SessionManager:
|
|
|
|
sessions: Dict[int, Tuple[str, datetime, httpx.AsyncClient]] = {}
|
|
|
|
session_expiry: timedelta = timedelta(minutes=15)
|
2021-01-30 05:58:30 +08:00
|
|
|
|
2021-01-30 19:11:17 +08:00
|
|
|
def __init__(self, session_key: str, client: httpx.AsyncClient):
|
|
|
|
self.session_key, self.client = session_key, client
|
2021-01-30 13:36:31 +08:00
|
|
|
|
2021-01-30 21:51:51 +08:00
|
|
|
@staticmethod
|
|
|
|
def _raise_code(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
|
code = data.get('code', 0)
|
|
|
|
logger.debug(f'Mirai API returned data: {data}')
|
|
|
|
if code != 0:
|
|
|
|
raise ActionFailed(code, message=data['msg'])
|
|
|
|
return data
|
|
|
|
|
2021-01-30 20:40:00 +08:00
|
|
|
async def post(self,
|
|
|
|
path: str,
|
|
|
|
*,
|
|
|
|
params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
2021-01-30 19:11:17 +08:00
|
|
|
params = {**(params or {}), 'sessionKey': self.session_key}
|
2021-01-30 21:51:51 +08:00
|
|
|
response = await self.client.post(path, json=params, timeout=3)
|
2021-01-30 13:36:31 +08:00
|
|
|
response.raise_for_status()
|
2021-01-30 21:51:51 +08:00
|
|
|
return self._raise_code(response.json())
|
2021-01-30 13:36:31 +08:00
|
|
|
|
2021-01-30 20:40:00 +08:00
|
|
|
async def request(self,
|
|
|
|
path: str,
|
|
|
|
*,
|
|
|
|
params: Optional[Dict[str,
|
|
|
|
Any]] = None) -> Dict[str, Any]:
|
|
|
|
response = await self.client.get(path,
|
|
|
|
params={
|
|
|
|
**(params or {}), 'sessionKey':
|
|
|
|
self.session_key
|
2021-01-30 21:51:51 +08:00
|
|
|
},
|
|
|
|
timeout=3)
|
2021-01-30 20:40:00 +08:00
|
|
|
response.raise_for_status()
|
2021-01-30 21:51:51 +08:00
|
|
|
return self._raise_code(response.json())
|
2021-01-30 20:40:00 +08:00
|
|
|
|
|
|
|
async def upload(self, path: str, *, type: str,
|
|
|
|
file: Tuple[str, BytesIO]) -> Dict[str, Any]:
|
|
|
|
file_type, file_io = file
|
|
|
|
response = await self.client.post(path,
|
|
|
|
data={
|
|
|
|
'sessionKey': self.session_key,
|
|
|
|
'type': type
|
|
|
|
},
|
|
|
|
files={file_type: file_io},
|
|
|
|
timeout=6)
|
|
|
|
response.raise_for_status()
|
2021-01-30 21:51:51 +08:00
|
|
|
return self._raise_code(response.json())
|
2021-01-30 20:40:00 +08:00
|
|
|
|
2021-01-30 05:58:30 +08:00
|
|
|
@classmethod
|
2021-01-30 19:11:17 +08:00
|
|
|
async def new(cls, self_id: int, *, host: IPv4Address, port: int,
|
|
|
|
auth_key: str):
|
|
|
|
if self_id in cls.sessions:
|
|
|
|
manager = cls.get(self_id)
|
|
|
|
if manager is not None:
|
|
|
|
return manager
|
|
|
|
client = httpx.AsyncClient(base_url=f'http://{host}:{port}')
|
|
|
|
response = await client.post('/auth', json={'authKey': auth_key})
|
|
|
|
response.raise_for_status()
|
|
|
|
auth = response.json()
|
|
|
|
assert auth['code'] == 0
|
|
|
|
session_key = auth['session']
|
|
|
|
response = await client.post('/verify',
|
|
|
|
json={
|
|
|
|
'sessionKey': session_key,
|
|
|
|
'qq': self_id
|
|
|
|
})
|
|
|
|
assert response.json()['code'] == 0
|
|
|
|
cls.sessions[self_id] = session_key, datetime.now(), client
|
|
|
|
return cls(session_key, client)
|
2021-01-30 05:58:30 +08:00
|
|
|
|
2021-01-30 19:11:17 +08:00
|
|
|
@classmethod
|
|
|
|
def get(cls, self_id: int):
|
|
|
|
key, time, client = cls.sessions[self_id]
|
|
|
|
if datetime.now() - time > cls.session_expiry:
|
|
|
|
return None
|
|
|
|
return cls(key, client)
|
2021-01-30 05:58:30 +08:00
|
|
|
|
2021-01-29 17:37:44 +08:00
|
|
|
|
|
|
|
class MiraiBot(BaseBot):
|
|
|
|
|
2021-01-30 19:11:17 +08:00
|
|
|
def __init__(self,
|
|
|
|
connection_type: str,
|
|
|
|
self_id: str,
|
|
|
|
*,
|
|
|
|
websocket: Optional[WebSocket] = None):
|
2021-01-29 17:37:44 +08:00
|
|
|
super().__init__(connection_type, self_id, websocket=websocket)
|
2021-01-30 19:11:17 +08:00
|
|
|
self.api = SessionManager.get(int(self_id))
|
2021-01-29 17:37:44 +08:00
|
|
|
|
|
|
|
@property
|
|
|
|
@overrides(BaseBot)
|
|
|
|
def type(self) -> str:
|
|
|
|
return "mirai"
|
|
|
|
|
2021-01-30 13:36:31 +08:00
|
|
|
@property
|
|
|
|
def alive(self) -> bool:
|
|
|
|
return not self.websocket.closed
|
|
|
|
|
2021-01-29 17:37:44 +08:00
|
|
|
@classmethod
|
|
|
|
@overrides(BaseBot)
|
|
|
|
async def check_permission(cls, driver: "Driver", connection_type: str,
|
2021-01-30 19:11:17 +08:00
|
|
|
headers: dict, body: Optional[dict]) -> str:
|
|
|
|
if connection_type == 'ws':
|
|
|
|
raise RequestDenied(
|
|
|
|
status_code=501,
|
|
|
|
reason='Websocket connection is not implemented')
|
|
|
|
self_id: Optional[str] = headers.get('bot')
|
|
|
|
if self_id is None:
|
|
|
|
raise RequestDenied(status_code=400,
|
|
|
|
reason='Header `Bot` is required.')
|
|
|
|
self_id = str(self_id).strip()
|
|
|
|
await SessionManager.new(
|
|
|
|
int(self_id),
|
|
|
|
host=cls.mirai_config.host, # type: ignore
|
|
|
|
port=cls.mirai_config.port, #type: ignore
|
|
|
|
auth_key=cls.mirai_config.auth_key) # type: ignore
|
|
|
|
return self_id
|
2021-01-30 05:58:30 +08:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
@overrides(BaseBot)
|
2021-01-30 19:11:17 +08:00
|
|
|
def register(cls, driver: "Driver", config: "Config"):
|
2021-01-30 13:36:31 +08:00
|
|
|
cls.mirai_config = MiraiConfig(**config.dict())
|
|
|
|
assert cls.mirai_config.auth_key is not None
|
|
|
|
assert cls.mirai_config.host is not None
|
|
|
|
assert cls.mirai_config.port is not None
|
|
|
|
super().register(driver, config)
|
|
|
|
|
2021-01-29 17:37:44 +08:00
|
|
|
@overrides(BaseBot)
|
|
|
|
async def handle_message(self, message: dict):
|
2021-01-30 19:11:17 +08:00
|
|
|
await handle_event(bot=self,
|
|
|
|
event=Event.new({
|
|
|
|
**message,
|
|
|
|
'self_id': self.self_id,
|
|
|
|
}))
|
2021-01-29 17:37:44 +08:00
|
|
|
|
|
|
|
@overrides(BaseBot)
|
2021-01-30 20:40:00 +08:00
|
|
|
async def call_api(self, api: str, **data) -> NoReturn:
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
@overrides(BaseBot)
|
2021-01-30 21:51:51 +08:00
|
|
|
def __getattr__(self, key: str) -> NoReturn:
|
2021-01-30 20:40:00 +08:00
|
|
|
raise NotImplementedError
|
2021-01-29 17:37:44 +08:00
|
|
|
|
|
|
|
@overrides(BaseBot)
|
2021-01-30 20:40:00 +08:00
|
|
|
async def send(self,
|
|
|
|
event: Event,
|
|
|
|
message: MessageChain,
|
|
|
|
at_sender: bool = False,
|
|
|
|
**kwargs):
|
|
|
|
if isinstance(event, FriendMessage):
|
|
|
|
return await self.send_friend_message(target=event.sender.id,
|
|
|
|
message_chain=message)
|
|
|
|
elif isinstance(event, GroupMessage):
|
2021-01-30 21:51:51 +08:00
|
|
|
return await self.send_group_message(
|
|
|
|
group=event.sender.group.id,
|
|
|
|
message_chain=message if not at_sender else
|
|
|
|
(MessageSegment.at(target=event.sender.id) + message))
|
2021-01-30 20:40:00 +08:00
|
|
|
elif isinstance(event, TempMessage):
|
|
|
|
return await self.send_temp_message(qq=event.sender.id,
|
|
|
|
group=event.sender.group.id,
|
|
|
|
message_chain=message)
|
|
|
|
else:
|
|
|
|
raise ValueError(f'Unsupported event type {event!r}.')
|
|
|
|
|
|
|
|
async def send_friend_message(self, target: int,
|
|
|
|
message_chain: MessageChain):
|
|
|
|
return await self.api.post('sendFriendMessage',
|
|
|
|
params={
|
|
|
|
'target': target,
|
|
|
|
'messageChain': message_chain.export()
|
|
|
|
})
|
|
|
|
|
|
|
|
async def send_temp_message(self, qq: int, group: int,
|
|
|
|
message_chain: MessageChain):
|
|
|
|
return await self.api.post('sendTempMessage',
|
|
|
|
params={
|
|
|
|
'qq': qq,
|
|
|
|
'group': group,
|
|
|
|
'messageChain': message_chain.export()
|
|
|
|
})
|
|
|
|
|
2021-01-30 21:51:51 +08:00
|
|
|
async def send_group_message(self,
|
|
|
|
group: int,
|
|
|
|
message_chain: MessageChain,
|
|
|
|
quote: Optional[int] = None):
|
2021-01-30 20:40:00 +08:00
|
|
|
return await self.api.post('sendGroupMessage',
|
|
|
|
params={
|
2021-01-30 21:51:51 +08:00
|
|
|
'group': group,
|
|
|
|
'messageChain': message_chain.export(),
|
|
|
|
'quote': quote
|
2021-01-30 20:40:00 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
async def recall(self, target: int):
|
|
|
|
return await self.api.post('recall', params={'target': target})
|
|
|
|
|
|
|
|
async def send_image_message(self, target: int, qq: int, group: int,
|
|
|
|
urls: List[str]):
|
|
|
|
return await self.api.post('sendImageMessage',
|
|
|
|
params={
|
|
|
|
'target': target,
|
|
|
|
'qq': qq,
|
|
|
|
'group': group,
|
|
|
|
'urls': urls
|
|
|
|
})
|
|
|
|
|
|
|
|
async def upload_image(self, type: str, img: BytesIO):
|
|
|
|
return await self.api.upload('uploadImage',
|
|
|
|
type=type,
|
|
|
|
file=('img', img))
|
|
|
|
|
|
|
|
async def upload_voice(self, type: str, voice: BytesIO):
|
|
|
|
return await self.api.upload('uploadVoice',
|
|
|
|
type=type,
|
|
|
|
file=('voice', voice))
|
|
|
|
|
|
|
|
async def fetch_message(self):
|
|
|
|
return await self.api.request('fetchMessage')
|
|
|
|
|
|
|
|
async def fetch_latest_message(self):
|
|
|
|
return await self.api.request('fetchLatestMessage')
|
|
|
|
|
|
|
|
async def peek_message(self, count: int):
|
|
|
|
return await self.api.request('peekMessage', params={'count': count})
|
|
|
|
|
|
|
|
async def peek_latest_message(self, count: int):
|
|
|
|
return await self.api.request('peekLatestMessage',
|
|
|
|
params={'count': count})
|
|
|
|
|
|
|
|
async def messsage_from_id(self, id: int):
|
|
|
|
return await self.api.request('messageFromId', params={'id': id})
|
|
|
|
|
|
|
|
async def count_message(self):
|
|
|
|
return await self.api.request('countMessage')
|
|
|
|
|
|
|
|
async def friend_list(self) -> List[Dict[str, Any]]:
|
|
|
|
return await self.api.request('friendList') # type: ignore
|
|
|
|
|
|
|
|
async def group_list(self) -> List[Dict[str, Any]]:
|
|
|
|
return await self.api.request('groupList') # type: ignore
|
|
|
|
|
|
|
|
async def member_list(self, target: int) -> List[Dict[str, Any]]:
|
|
|
|
return await self.api.request('memberList',
|
|
|
|
params={'target': target}) # type: ignore
|
|
|
|
|
|
|
|
async def mute(self, target: int, member_id: int, time: int):
|
|
|
|
return await self.api.post('mute',
|
|
|
|
params={
|
|
|
|
'target': target,
|
|
|
|
'memberId': member_id,
|
|
|
|
'time': time
|
|
|
|
})
|
|
|
|
|
|
|
|
async def unmute(self, target: int, member_id: int):
|
|
|
|
return await self.api.post('unmute',
|
|
|
|
params={
|
|
|
|
'target': target,
|
|
|
|
'memberId': member_id
|
|
|
|
})
|
|
|
|
|
|
|
|
async def kick(self, target: int, member_id: int, msg: str):
|
|
|
|
return await self.api.post('kick',
|
|
|
|
params={
|
|
|
|
'target': target,
|
|
|
|
'memberId': member_id,
|
|
|
|
'msg': msg
|
|
|
|
})
|
|
|
|
|
|
|
|
async def quit(self, target: int):
|
|
|
|
return await self.api.post('quit', params={'target': target})
|
|
|
|
|
|
|
|
async def mute_all(self, target: int):
|
|
|
|
return await self.api.post('muteAll', params={'target': target})
|
|
|
|
|
|
|
|
async def unmute_all(self, target: int):
|
|
|
|
return await self.api.post('unmuteAll', params={'target': target})
|
|
|
|
|
|
|
|
async def group_config(self, target: int):
|
|
|
|
return await self.api.request('groupConfig', params={'target': target})
|
|
|
|
|
|
|
|
async def modify_group_config(self, target: int, config: Dict[str, Any]):
|
|
|
|
return await self.api.post('groupConfig',
|
|
|
|
params={
|
|
|
|
'target': target,
|
|
|
|
'config': config
|
|
|
|
})
|
|
|
|
|
|
|
|
async def member_info(self, target: int, member_id: int):
|
|
|
|
return await self.api.request('memberInfo',
|
|
|
|
params={
|
|
|
|
'target': target,
|
|
|
|
'memberId': member_id
|
|
|
|
})
|
|
|
|
|
|
|
|
async def modify_member_info(self, target: int, member_id: int,
|
|
|
|
info: Dict[str, Any]):
|
|
|
|
return await self.api.post('memberInfo',
|
|
|
|
params={
|
|
|
|
'target': target,
|
|
|
|
'memberId': member_id,
|
|
|
|
'info': info
|
|
|
|
})
|