nonebot2/packages/nonebot-adapter-feishu/nonebot/adapters/feishu/bot.py

312 lines
9.6 KiB
Python
Raw Normal View History

import json
import httpx
from typing import Any, Dict, Tuple, Union, Optional, TYPE_CHECKING
2021-07-01 07:59:50 +08:00
from nonebot.log import logger
from nonebot.typing import overrides
2021-07-01 07:59:50 +08:00
from nonebot.message import handle_event
from nonebot.adapters import Bot as BaseBot
from nonebot.drivers import Driver, HTTPRequest, HTTPResponse
2021-07-01 07:59:50 +08:00
from .config import Config as FeishuConfig
2021-07-06 10:45:00 +08:00
from .event import Event, GroupMessageEvent, PrivateMessageEvent, get_event_model
from .exception import ActionFailed, ApiNotAvailable, NetworkError
from .message import Message, MessageSegment, MessageSerializer
from .utils import log, AESCipher
2021-07-01 07:59:50 +08:00
if TYPE_CHECKING:
from nonebot.config import Config
2021-07-01 07:59:50 +08:00
2021-07-04 14:19:10 +08:00
async def _check_reply(bot: "Bot", event: "Event"):
"""
:说明:
检查消息中存在的回复去除并赋值 ``event.reply``, ``event.to_me``
:参数:
* ``bot: Bot``: Bot 对象
* ``event: Event``: Event 对象
"""
2021-07-08 16:13:07 +08:00
#TODO:实现该函数
2021-07-04 14:19:10 +08:00
...
def _check_at_me(bot: "Bot", event: "Event"):
"""
:说明:
检查消息开头或结尾是否存在 @机器人去除并赋值 ``event.to_me``
:参数:
* ``bot: Bot``: Bot 对象
* ``event: Event``: Event 对象
"""
2021-07-08 16:13:07 +08:00
#TODO:实现该函数
2021-07-04 14:19:10 +08:00
...
def _check_nickname(bot: "Bot", event: "Event"):
"""
:说明:
检查消息开头是否存在去除并赋值 ``event.to_me``
:参数:
* ``bot: Bot``: Bot 对象
* ``event: Event``: Event 对象
"""
2021-07-08 16:13:07 +08:00
#TODO:实现该函数
2021-07-04 14:19:10 +08:00
...
def _handle_api_result(result: Optional[Dict[str, Any]]) -> Any:
"""
:说明:
处理 API 请求返回值
:参数:
* ``result: Optional[Dict[str, Any]]``: API 返回数据
:返回:
- ``Any``: API 调用返回数据
:异常:
- ``ActionFailed``: API 调用失败
"""
if isinstance(result, dict):
if result.get("code") != 0:
raise ActionFailed(**result)
return result.get("data")
2021-07-01 07:59:50 +08:00
class Bot(BaseBot):
"""
飞书 协议 Bot 适配继承属性参考 `BaseBot <./#class-basebot>`_ 。
"""
@property
def type(self) -> str:
return "feishu"
@property
def api_root(self) -> str:
return "https://open.feishu.cn/open-apis/"
2021-07-01 07:59:50 +08:00
@classmethod
def register(cls, driver: Driver, config: "Config"):
super().register(driver, config)
cls.feishu_config = FeishuConfig(**config.dict())
@classmethod
@overrides(BaseBot)
async def check_permission(
cls, driver: Driver, request: HTTPRequest
) -> Tuple[Optional[str], Optional[HTTPResponse]]:
if not isinstance(request, HTTPRequest):
log("WARNING",
"Unsupported connection type, available type: `http`")
return None, HTTPResponse(
405, b"Unsupported connection type, available type: `http`")
encrypt_key = cls.feishu_config.encrypt_key
if encrypt_key:
encrypted = json.loads(request.body)["encrypt"]
decrypted = AESCipher(encrypt_key).decrypt_string(encrypted)
data = json.loads(decrypted)
else:
data = json.loads(request.body)
challenge = data.get("challenge")
if challenge:
2021-07-04 14:19:10 +08:00
return data.get("token"), HTTPResponse(
200,
json.dumps({
"challenge": challenge
}).encode())
schema = data.get("schema")
if not schema:
return None, HTTPResponse(
400,
b"Missing `schema` in POST body, only accept event of version 2.0"
)
headers = data.get("header")
if headers:
token = headers.get("token")
app_id = headers.get("app_id")
else:
log("WARNING", "Missing `header` in POST body")
return None, HTTPResponse(400, b"Missing `header` in POST body")
if not token:
log("WARNING", "Missing `verification token` in POST body")
return None, HTTPResponse(
400, b"Missing `verification token` in POST body")
else:
if token != cls.feishu_config.verification_token:
log("WARNING", "Verification token check failed")
return None, HTTPResponse(403,
b"Verification token check failed")
return app_id, HTTPResponse(200, b'')
async def handle_message(self, message: bytes):
"""
:说明:
2021-07-01 07:59:50 +08:00
处理事件并转换为 `Event <#class-event>`_
"""
data = json.loads(message)
2021-07-04 14:19:10 +08:00
if data.get("type") == "url_verification":
return
2021-07-01 07:59:50 +08:00
try:
2021-07-04 14:19:10 +08:00
header = data["header"]
event_type = header["event_type"]
if data.get("event"):
if data["event"].get("message"):
event_type += f".{data['event']['message']['chat_type']}"
2021-07-04 14:19:10 +08:00
models = get_event_model(event_type)
for model in models:
try:
event = model.parse_obj(data)
break
except Exception as e:
log("DEBUG", "Event Parser Error", e)
else:
event = Event.parse_obj(data)
2021-07-01 07:59:50 +08:00
await handle_event(self, event)
except Exception as e:
logger.opt(colors=True, exception=e).error(
f"<r><bg #f8bbd0>Failed to handle event. Raw: {message}</bg #f8bbd0></r>"
)
def _construct_url(self, path: str) -> str:
return self.api_root + path
2021-07-06 10:45:00 +08:00
#TODO:实现token缓存与ttl
async def _fetch_tenant_access_token(self) -> str:
try:
async with httpx.AsyncClient() as client:
response = await client.post(
self._construct_url(
"auth/v3/tenant_access_token/internal/"),
json={
"app_id": self.feishu_config.app_id,
"app_secret": self.feishu_config.app_secret
},
timeout=self.config.api_timeout)
if 200 <= response.status_code < 300:
result = response.json()
return result["tenant_access_token"]
else:
raise NetworkError(f"HTTP request received unexpected "
f"status code: {response.status_code}")
except httpx.InvalidURL:
raise NetworkError("API root url invalid")
except httpx.HTTPError:
raise NetworkError("HTTP request failed")
@overrides(BaseBot)
2021-07-01 07:59:50 +08:00
async def _call_api(self, api: str, **data) -> Any:
log("DEBUG", f"Calling API <y>{api}</y>")
if isinstance(self.request, HTTPRequest):
if not self.api_root:
raise ApiNotAvailable
headers = {}
if self.feishu_config.tenant_access_token is None:
self.feishu_config.tenant_access_token = await self._fetch_tenant_access_token(
)
headers[
"Authorization"] = "Bearer " + self.feishu_config.tenant_access_token
try:
async with httpx.AsyncClient(headers=headers) as client:
response = await client.post(
self.api_root + api,
json=data["body"],
params=data["query"],
timeout=self.config.api_timeout)
if 200 <= response.status_code < 300:
result = response.json()
return _handle_api_result(result)
raise NetworkError(f"HTTP request received unexpected "
2021-07-06 22:11:34 +08:00
f"status code: {response.status_code} "
2021-07-06 22:08:38 +08:00
f"response body: {response.text}")
except httpx.InvalidURL:
raise NetworkError("API root url invalid")
except httpx.HTTPError:
raise NetworkError("HTTP request failed")
@overrides(BaseBot)
async def call_api(self, api: str, **data) -> Any:
"""
:说明:
调用 飞书 协议 API
2021-07-01 07:59:50 +08:00
:参数:
* ``api: str``: API 名称
* ``**data: Any``: API 参数
:返回:
- ``Any``: API 调用返回数据
:异常:
- ``NetworkError``: 网络错误
- ``ActionFailed``: API 调用失败
"""
return await super().call_api(api, **data)
@overrides(BaseBot)
2021-07-08 16:13:07 +08:00
async def send(self,
event: Event,
message: Union[str, Message, MessageSegment],
at_sender: bool = False,
2021-07-01 07:59:50 +08:00
**kwargs) -> Any:
msg = message if isinstance(message, Message) else Message(message)
2021-07-06 10:45:00 +08:00
if isinstance(event, GroupMessageEvent):
receive_id, receive_id_type = event.event.message.chat_id, 'chat_id'
elif isinstance(event, PrivateMessageEvent):
receive_id, receive_id_type = event.get_user_id(), 'union_id'
else:
raise ValueError(
"Cannot guess `receive_id` and `receive_id_type` to reply!")
2021-07-08 16:13:07 +08:00
at_sender = at_sender and bool(event.get_user_id())
if at_sender and receive_id_type != "union_id":
2021-07-08 22:30:39 +08:00
msg = MessageSegment.at(event.get_user_id(), "StarHeart") + " " + msg
2021-07-08 16:13:07 +08:00
2021-07-06 21:12:49 +08:00
msg_type, content = MessageSerializer(msg).serialize()
params = {
2021-07-06 10:45:00 +08:00
"query": {
"receive_id_type": receive_id_type
},
"body": {
"receive_id": receive_id,
2021-07-06 21:12:49 +08:00
"content": content,
"msg_type": msg_type
2021-07-06 10:45:00 +08:00
}
}
2021-07-06 10:45:00 +08:00
return await self.call_api(f"im/v1/messages", **params)