-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(docs): update api docs with sphinx-apidoc
- Loading branch information
1 parent
8f8fe77
commit e8d49a8
Showing
11 changed files
with
502 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
iamai.adapter.dingtalk.config module | ||
==================================== | ||
|
||
.. automodule:: iamai.adapter.dingtalk.config | ||
:members: | ||
:undoc-members: | ||
:show-inheritance: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
iamai.adapter.dingtalk.event module | ||
=================================== | ||
|
||
.. automodule:: iamai.adapter.dingtalk.event | ||
:members: | ||
:undoc-members: | ||
:show-inheritance: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
iamai.adapter.dingtalk.exceptions module | ||
======================================== | ||
|
||
.. automodule:: iamai.adapter.dingtalk.exceptions | ||
:members: | ||
:undoc-members: | ||
:show-inheritance: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
iamai.adapter.dingtalk.message module | ||
===================================== | ||
|
||
.. automodule:: iamai.adapter.dingtalk.message | ||
:members: | ||
:undoc-members: | ||
:show-inheritance: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
iamai.adapter.dingtalk package | ||
============================== | ||
|
||
Submodules | ||
---------- | ||
|
||
.. toctree:: | ||
:maxdepth: 4 | ||
|
||
iamai.adapter.dingtalk.config | ||
iamai.adapter.dingtalk.event | ||
iamai.adapter.dingtalk.exceptions | ||
iamai.adapter.dingtalk.message | ||
|
||
Module contents | ||
--------------- | ||
|
||
.. automodule:: iamai.adapter.dingtalk | ||
:members: | ||
:undoc-members: | ||
:show-inheritance: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
"""DingTalk 协议适配器。 | ||
本适配器适配了钉钉企业自建机器人协议。 | ||
协议详情请参考:[钉钉开放平台](https://open.dingtalk.com/document/robots/robot-overview)。 | ||
""" | ||
|
||
import base64 | ||
import hashlib | ||
import hmac | ||
import time | ||
from typing import Any, Dict, Literal, Union | ||
|
||
import aiohttp | ||
from aiohttp import web | ||
|
||
from iamai.adapter import Adapter | ||
from iamai.log import logger | ||
|
||
from .config import Config | ||
from .event import DingTalkEvent | ||
from .exceptions import NetworkError | ||
from .message import DingTalkMessage | ||
|
||
__all__ = ["DingTalkAdapter"] | ||
|
||
|
||
class DingTalkAdapter(Adapter[DingTalkEvent, Config]): | ||
"""钉钉协议适配器。""" | ||
|
||
name: str = "dingtalk" | ||
Config = Config | ||
|
||
app: web.Application | ||
runner: web.AppRunner | ||
site: web.TCPSite | ||
|
||
session: aiohttp.ClientSession | ||
|
||
async def startup(self) -> None: | ||
"""创建 aiohttp Application。""" | ||
self.app = web.Application() | ||
self.app.add_routes([web.post(self.config.url, self.handler)]) | ||
|
||
self.session = aiohttp.ClientSession() | ||
|
||
async def run(self) -> None: | ||
"""运行 aiohttp 服务器。""" | ||
self.runner = web.AppRunner(self.app) | ||
await self.runner.setup() | ||
self.site = web.TCPSite(self.runner, self.config.host, self.config.port) | ||
await self.site.start() | ||
|
||
async def shutdown(self) -> None: | ||
"""清理 aiohttp AppRunner。""" | ||
await self.session.close() | ||
await self.site.stop() | ||
await self.runner.cleanup() | ||
|
||
async def handler(self, request: web.Request) -> web.Response: | ||
"""处理 aiohttp 服务器的接收。 | ||
Args: | ||
request: aiohttp 服务器的 `Request` 对象。 | ||
""" | ||
if "timestamp" not in request.headers or "sign" not in request.headers: | ||
logger.error("Illegal http header, incomplete http header") | ||
elif ( | ||
abs(int(request.headers["timestamp"]) - time.time() * 1000) > 60 * 60 * 1000 | ||
): | ||
logger.error( | ||
f'Illegal http header, timestamp: {request.headers["timestamp"]}' | ||
) | ||
elif request.headers["sign"] != self.get_sign(request.headers["timestamp"]): | ||
logger.error(f'Illegal http header, sign: {request.headers["sign"]}') | ||
else: | ||
try: | ||
dingtalk_event = DingTalkEvent(adapter=self, **(await request.json())) | ||
except Exception as e: | ||
self.bot.error_or_exception("Request parsing error:", e) | ||
return web.Response() | ||
await self.handle_event(dingtalk_event) | ||
return web.Response() | ||
|
||
def get_sign(self, timestamp: str) -> str: | ||
"""计算签名。 | ||
Args: | ||
timestamp: 时间戳。 | ||
Returns: | ||
签名。 | ||
""" | ||
hmac_code = hmac.new( | ||
self.config.app_secret.encode("utf-8"), | ||
f"{timestamp}\n{self.config.app_secret}".encode(), | ||
digestmod=hashlib.sha256, | ||
).digest() | ||
return base64.b64encode(hmac_code).decode("utf-8") | ||
|
||
async def send( | ||
self, | ||
webhook: str, | ||
conversation_type: Literal["1", "2"], | ||
msg: Union[str, Dict[str, Any], DingTalkMessage], | ||
at: Union[None, Dict[str, Any], DingTalkMessage] = None, | ||
) -> Dict[str, Any]: | ||
"""发送消息。 | ||
Args: | ||
webhook: Webhook 网址。 | ||
conversation_type: 聊天类型,"1" 表示单聊,"2" 表示群聊。 | ||
msg: 消息。 | ||
at: At 对象,仅在群聊时生效,默认为空。 | ||
Returns: | ||
钉钉服务器的响应。 | ||
Raises: | ||
TypeError: 传入参数类型错误。 | ||
ValueError: 传入参数值错误。 | ||
NetworkError: 调用 Webhook 地址时网络错误。 | ||
""" | ||
if isinstance(msg, DingTalkMessage): | ||
pass | ||
elif isinstance(msg, dict): | ||
msg = DingTalkMessage.raw(msg) | ||
elif isinstance(msg, str): | ||
msg = DingTalkMessage.text(msg) | ||
else: | ||
raise TypeError( | ||
f"msg must be str, Dict or DingTalkMessage, not {type(msg)!r}" | ||
) | ||
|
||
if at is not None: | ||
if isinstance(at, DingTalkMessage): | ||
if at.type == "at": | ||
pass | ||
else: | ||
raise ValueError(f'at.type must be "at", not {at.type}') | ||
elif isinstance(at, dict): | ||
at = DingTalkMessage.raw(at) | ||
else: | ||
raise TypeError(f"at must be Dict or DingTalkMessage, not {type(at)!r}") | ||
|
||
data: Union[Dict[str, Any], DingTalkMessage] | ||
if conversation_type == "1": | ||
data = msg | ||
elif conversation_type == "2": | ||
if at is None: | ||
data = {"msgtype": msg.type, **msg.model_dump()} | ||
else: | ||
data = {"msgtype": msg.type, **msg.model_dump(), **at.model_dump()} | ||
else: | ||
raise ValueError( | ||
f'conversation_type must be "1" or "2" not {conversation_type}' | ||
) | ||
|
||
try: | ||
async with self.session.post(webhook, json=data) as resp: | ||
return await resp.json() | ||
except aiohttp.ClientError as e: | ||
raise NetworkError from e |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
"""DingTalk 适配器配置。""" | ||
|
||
from iamai.config import ConfigModel | ||
|
||
__all__ = ["Config"] | ||
|
||
|
||
class Config(ConfigModel): | ||
"""DingTalk 配置类,将在适配器被加载时被混入到机器人主配置中。 | ||
Attributes: | ||
host: 本机域名。 | ||
port: 监听的端口。 | ||
url: 路径。 | ||
api_timeout: 进行 API 调用时等待返回响应的超时时间。 | ||
app_secret: 机器人的 `appSecret`。 | ||
""" | ||
|
||
__config_name__ = "dingtalk" | ||
host: str = "127.0.0.1" | ||
port: int = 8080 | ||
url: str = "/dingtalk" | ||
api_timeout: int = 1000 | ||
app_secret: str = "" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
"""DingTalk 适配器事件。""" | ||
|
||
import time | ||
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union | ||
from typing_extensions import Self | ||
|
||
from pydantic import BaseModel, Field | ||
|
||
from iamai.event import MessageEvent | ||
|
||
from .exceptions import WebhookExpiredError | ||
from .message import DingTalkMessage | ||
|
||
if TYPE_CHECKING: | ||
pass | ||
|
||
__all__ = ["UserInfo", "Text", "DingTalkEvent"] | ||
|
||
|
||
class UserInfo(BaseModel): | ||
"""用户信息""" | ||
|
||
dingtalkId: str | ||
staffId: Optional[str] = None | ||
|
||
|
||
class Text(BaseModel): | ||
"""文本消息""" | ||
|
||
content: str | ||
|
||
|
||
class DingTalkEvent(MessageEvent["DingTalkAdapter"]): | ||
"""DingTalk 事件基类""" | ||
|
||
type: Optional[str] = Field(alias="msgtype") | ||
|
||
msgtype: str | ||
msgId: str | ||
createAt: str | ||
conversationType: Literal["1", "2"] | ||
conversationId: str | ||
conversationTitle: Optional[str] = None | ||
senderId: str | ||
senderNick: str | ||
senderCorpId: Optional[str] = None | ||
sessionWebhook: str | ||
sessionWebhookExpiredTime: int | ||
isAdmin: Optional[bool] = None | ||
chatbotCorpId: Optional[str] = None | ||
isInAtList: Optional[bool] = None | ||
senderStaffId: Optional[str] = None | ||
chatbotUserId: str | ||
atUsers: List[UserInfo] | ||
text: Text | ||
|
||
response_msg: Union[None, str, Dict[str, Any], DingTalkMessage] = None | ||
response_at: Union[None, Dict[str, Any], DingTalkMessage] = None | ||
|
||
@property | ||
def message(self) -> DingTalkMessage: | ||
"""返回 message 字段。""" | ||
return DingTalkMessage.text(self.text.content) | ||
|
||
def get_plain_text(self) -> str: | ||
"""获取消息的纯文本内容。 | ||
Returns: | ||
消息的纯文本内容。 | ||
""" | ||
return self.message.get_plain_text() | ||
|
||
async def reply( | ||
self, | ||
message: Union[str, Dict[str, Any], DingTalkMessage], | ||
at: Union[None, Dict[str, Any], DingTalkMessage] = None, | ||
) -> Dict[str, Any]: | ||
"""回复消息。 | ||
Args: | ||
message: 回复消息的内容,可以是 `str`, `Dict` 或 `DingTalkMessage`。 | ||
at: 回复消息时 At 的对象,必须时 at 类型的 `DingTalkMessage`,或者符合标准的 `Dict`。 | ||
Returns: | ||
调用 Webhook 地址后钉钉服务器的响应。 | ||
Raises: | ||
WebhookExpiredError: 当前事件的 Webhook 地址已经过期。 | ||
...: 同 `DingTalkAdapter.send()` 方法。 | ||
""" | ||
if self.sessionWebhookExpiredTime > time.time() * 1000: | ||
return await self.adapter.send( | ||
webhook=self.sessionWebhook, | ||
conversation_type=self.conversationType, | ||
msg=message, | ||
at=at, | ||
) | ||
raise WebhookExpiredError | ||
|
||
async def is_same_sender(self, other: Self) -> bool: | ||
"""判断自身和另一个事件是否是同一个发送者。 | ||
Args: | ||
other: 另一个事件。 | ||
Returns: | ||
是否是同一个发送者。 | ||
""" | ||
return self.senderId == other.senderId |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
"""DingTalk 适配器异常。""" | ||
|
||
from iamai.exceptions import AdapterException | ||
|
||
__all__ = ["DingTalkException", "NetworkError", "WebhookExpiredError"] | ||
|
||
|
||
class DingTalkException(AdapterException): | ||
"""DingTalk 异常基类。""" | ||
|
||
|
||
class NetworkError(DingTalkException): | ||
"""网络异常。""" | ||
|
||
|
||
class WebhookExpiredError(DingTalkException): | ||
"""Webhook 地址已到期。""" |
Oops, something went wrong.