diff --git a/.env.example b/.env.example index 9c90d95..451b5f6 100644 --- a/.env.example +++ b/.env.example @@ -56,7 +56,11 @@ channel_9_TYPE=telegram channel_9_TELEGRAM_TOKEN= channel_9_TELEGRAM_CHAT_ID= -# channel_10(Ntfy) +# channel_10(ntfy) channel_10_TYPE=ntfy channel_10_NTFY_HOST= -channel_10_NTFY_TOPIC= \ No newline at end of file +channel_10_NTFY_TOPIC= + +# channel_11(lark) +channel_11_TYPE=lark_webhook +channel_11_LARK_TOKEN= \ No newline at end of file diff --git a/.github/workflows/sonarqube-scan.yml b/.github/workflows/sonarqube-scan.yml deleted file mode 100644 index 238105e..0000000 --- a/.github/workflows/sonarqube-scan.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Build -on: - push: - branches: - - master - pull_request: - types: [opened, synchronize, reopened] -jobs: - sonarcloud: - name: SonarCloud - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: SonarCloud Scan - uses: SonarSource/sonarcloud-github-action@master - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} # Needed to get PR information, if any - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} \ No newline at end of file diff --git a/README.md b/README.md index 951875e..fde9a5c 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,11 @@ Heimdallr 是一个非常轻量的通知网关,可以聚合各种推送渠道 - [Discord(webhook)](https://discord.com/developers/docs/resources/webhook#execute-webhook) - [Telegram Bot](https://core.telegram.org/bots/api#sendmessage) - [ntfy](https://docs.ntfy.sh/) +- [飞书/Lark](https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot) + ### 可能会支持的推送方式 - [ ] 钉钉 -- [ ] 飞书/Lark - [ ] ... > 如果有需要的通知方式,请提交 [issue](https://github.com/LeslieLeung/heimdallr/issues/new?assignees=LeslieLeung&labels=enhancement&template=feature_request.md&title=) diff --git a/docs/Changelog.md b/docs/Changelog.md index 9a0bda5..9c4a069 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -1,4 +1,6 @@ # 更新日志 +## v2.0.2 +- 通知渠道新增 飞书/Lark Webhook ## v2.0.1 - 通知渠道新增 ntfy - 新增 `/push/form` 接口,支持 form-data diff --git a/docs/Config.md b/docs/Config.md index 6859a17..e03256e 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -62,47 +62,50 @@ channel_3_WECOM_WEBHOOK_KEY= #### 通知渠道类型 -| 渠道名称 | 渠道类型(上面的`_TYPE` | -| ---------------- | ------------------------------------- | -| Bark | bark | -| 企业微信 Webhook | wecom_webhook | -| 企业微信应用 | wecom_app | -| Pushover | pushover | -| PushDeer | pushdeer | -| Chanify | chanify | -| SMTP(邮件) | email | -| Discord | discord_webhook | -| Telegram | telegram | -| ntfy | ntfy | +| 渠道名称 | 渠道类型(上面的`_TYPE` | +| ----------------- | ------------------------------------- | +| Bark | bark | +| 企业微信 Webhook | wecom_webhook | +| 企业微信应用 | wecom_app | +| Pushover | pushover | +| PushDeer | pushdeer | +| Chanify | chanify | +| SMTP(邮件) | email | +| Discord | discord_webhook | +| Telegram | telegram | +| ntfy | ntfy | +| 飞书/Lark Webhook | lark_webhook | #### 渠道配置后缀 -| 后缀名 | 通知渠道 | 后缀说明 | -| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `BARK_URL` | Bark | Bark服务器地址,如`https://api.day.app` | -| `BARK_KEY` | Bark | Bark的推送 key,如 `qy7s8qnhjhphuNDHJNFxQE` | -| `WECOM_KEY` | 企业微信 | 企业微信机器人的 key,见 [企业微信机器人webhook](https://developer.work.weixin.qq.com/document/path/91770) | -| `WECOM_CORP_ID` | 企业微信 | 企业微信应用的 corp_id,见 [企业微信应用消息](https://developer.work.weixin.qq.com/document/path/90236) | -| `WECOM_AGENT_ID` | 企业微信 | 企业微信应用的 agent_id | -| `WECOM_SECRET` | 企业微信 | 企业微信应用的 secret | -| `PUSHOVER_TOKEN` | Pushover | Pushover 的 token,见 [Pushover API](https://pushover.net/api) | -| `PUSHOVER_USER` | Pushover | Pushover 的 user | -| `PUSHDEER_TOKEN` | PushDeer | PushDeer 的 token,见 [Pushdeer API](http://pushdeer.com) | -| `CHANIFY_ENDPOINT` | Chanify | Chanify 的 endpoint,见 [Chanify](https://github.com/chanify/chanify#as-sender-client),可不填,默认为 `https://api.chanify.net` | -| `CHANIFY_TOKEN` | Chanify | Chanify 的 token | -| `EMAIL_HOST` | Email | Email 服务器地址,如 `smtp.gmail.com` | -| `EMAIL_PORT` | Email | Email 服务器端口,如 `465` | -| `EMAIL_USER` | Email | Email 用户名 | -| `EMAIL_PASSWORD` | Email | Email 密码 | -| `EMAIL_SENDER` | Email | Email 发件人名称 | -| `EMAIL_TO` | Email | Email 收件人 | -| `EMAIL_STARTTLS` | Email | Email 是否使用 TLS | -| `DISCORD_WEBHOOK_ID` | Discord | Discord 的 Webhook ID,见 [Discord 文档](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) | -| `DISCORD_WEBHOOK_TOKEN` | Discord | Discord 的 Webhook Token | -| `TELEGRAM_TOKEN` | Telegram | Telegram 的 Token,见 [这里](https://github.com/pppscn/SmsForwarder/wiki/2.%E5%8F%91%E9%80%81%E9%80%9A%E9%81%93#tele%E6%9C%BA%E5%99%A8%E4%BA%BA%E7%A7%91%E5%AD%A6%E4%B8%8A%E7%BD%91) | -| `TELEGRAM_CHAT_ID` | Telegram | Telegram 的 Chat ID,见 [这里](https://github.com/pppscn/SmsForwarder/issues/319) | -| `NTFY_HOST` | ntfy | ntfy 的服务端地址 | -| `NTFY_TOPIC` | ntfy | ntfy 的 topic,见 [这里](https://docs.ntfy.sh/) | +| 后缀名 | 通知渠道 | 后缀说明 | +| ----------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `BARK_URL` | Bark | Bark服务器地址,如`https://api.day.app` | +| `BARK_KEY` | Bark | Bark的推送 key,如 `qy7s8qnhjhphuNDHJNFxQE` | +| `WECOM_KEY` | 企业微信 | 企业微信机器人的 key,见 [企业微信机器人webhook](https://developer.work.weixin.qq.com/document/path/91770) | +| `WECOM_CORP_ID` | 企业微信 | 企业微信应用的 corp_id,见 [企业微信应用消息](https://developer.work.weixin.qq.com/document/path/90236) | +| `WECOM_AGENT_ID` | 企业微信 | 企业微信应用的 agent_id | +| `WECOM_SECRET` | 企业微信 | 企业微信应用的 secret | +| `PUSHOVER_TOKEN` | Pushover | Pushover 的 token,见 [Pushover API](https://pushover.net/api) | +| `PUSHOVER_USER` | Pushover | Pushover 的 user | +| `PUSHDEER_TOKEN` | PushDeer | PushDeer 的 token,见 [Pushdeer API](http://pushdeer.com) | +| `CHANIFY_ENDPOINT` | Chanify | Chanify 的 endpoint,见 [Chanify](https://github.com/chanify/chanify#as-sender-client),可不填,默认为 `https://api.chanify.net` | +| `CHANIFY_TOKEN` | Chanify | Chanify 的 token | +| `EMAIL_HOST` | Email | Email 服务器地址,如 `smtp.gmail.com` | +| `EMAIL_PORT` | Email | Email 服务器端口,如 `465` | +| `EMAIL_USER` | Email | Email 用户名 | +| `EMAIL_PASSWORD` | Email | Email 密码 | +| `EMAIL_SENDER` | Email | Email 发件人名称 | +| `EMAIL_TO` | Email | Email 收件人 | +| `EMAIL_STARTTLS` | Email | Email 是否使用 TLS | +| `DISCORD_WEBHOOK_ID` | Discord | Discord 的 Webhook ID,见 [Discord 文档](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) | +| `DISCORD_WEBHOOK_TOKEN` | Discord | Discord 的 Webhook Token | +| `TELEGRAM_TOKEN` | Telegram | Telegram 的 Token,见 [这里](https://github.com/pppscn/SmsForwarder/wiki/2.%E5%8F%91%E9%80%81%E9%80%9A%E9%81%93#tele%E6%9C%BA%E5%99%A8%E4%BA%BA%E7%A7%91%E5%AD%A6%E4%B8%8A%E7%BD%91) | +| `TELEGRAM_CHAT_ID` | Telegram | Telegram 的 Chat ID,见 [这里](https://github.com/pppscn/SmsForwarder/issues/319) | +| `NTFY_HOST` | ntfy | ntfy 的服务端地址 | +| `NTFY_TOPIC` | ntfy | ntfy 的 topic,见 [这里](https://docs.ntfy.sh/) | +| `LARK_HOST` | 飞书/Lark | 飞书/Lark 的接口地址,默认可以留空。如果使用 Lark, 则为 https://open.larksuite.com/open-apis/bot/v2/hook/ | +| `LARK_TOKEN` | 飞书/Lark | 飞书/Lark 的 Token | ## 腾讯云 Serverless 环境变量设置 diff --git a/heimdallr/api/base.py b/heimdallr/api/base.py index 85f7c47..0ea2333 100644 --- a/heimdallr/api/base.py +++ b/heimdallr/api/base.py @@ -10,28 +10,6 @@ logger = logging.getLogger(__name__) -def serve(key: str, title: str = "", body: str = "", **kwargs): - try: - group = config.get_group(key) - except AuthException as e: - return Response(code=-1, message=str(e)).render() - logger.info(f"group: {group.name}, token: {group.token}") - errors = {} - for chan in group.channels: - logger.info(f"channel: {chan.get_name()}, channel_type: {chan.get_type()}") - message = build_message(chan.get_name(), title, body, **kwargs) - rs, msg = chan.send(message) - if not rs: - errors[chan.get_name()] = msg - - if len(errors) == 0: - return success() - err_msg = "" - for err in errors.items(): - err_msg += f"{err[0]} return: {err[1]}." - return Response(code=1, message=err_msg).render() - - async def serve_channels_async(key: str, title: str = "", body: str = "", **kwargs): try: group = config.get_group(key) diff --git a/heimdallr/channel/factory.py b/heimdallr/channel/factory.py index 76498f9..8ea611c 100644 --- a/heimdallr/channel/factory.py +++ b/heimdallr/channel/factory.py @@ -5,6 +5,7 @@ from heimdallr.channel.chanify import Chanify, ChanifyMessage from heimdallr.channel.discord import DiscordWebhook, DiscordWebhookMessage from heimdallr.channel.email import Email, EmailMessage +from heimdallr.channel.lark import LarkWebhook, LarkWebhookMessage from heimdallr.channel.ntfy import Ntfy, NtfyMessage from heimdallr.channel.pushdeer import PushDeer, PushDeerMessage from heimdallr.channel.pushover import Pushover, PushoverMessage @@ -32,6 +33,7 @@ CHANNEL_DISCORD_WEBHOOK = "discord_webhook" CHANNEL_TELEGRAM = "telegram" CHANNEL_NTFY = "ntfy" +CHANNEL_LARK_WEBHOOK = "lark_webhook" def _get_channel_type_by_name(name: str) -> str: @@ -69,6 +71,8 @@ def build_channel(name: str) -> Channel: return Telegram(name, channel_type) elif channel_type == CHANNEL_NTFY: return Ntfy(name, channel_type) + elif channel_type == CHANNEL_LARK_WEBHOOK: + return LarkWebhook(name, channel_type) else: raise ParamException(f"Channel {name} type {channel_type} not supported.") @@ -99,5 +103,7 @@ def build_message(name: str, title: str, body: str, **kwargs) -> Message: return TelegramMessage(title, body, **kwargs) elif channel_type == CHANNEL_NTFY: return NtfyMessage(title, body, **kwargs) + elif channel_type == CHANNEL_LARK_WEBHOOK: + return LarkWebhookMessage(title, body, **kwargs) else: raise ParamException(f"Channel type {channel_type} not supported.") diff --git a/heimdallr/channel/lark.py b/heimdallr/channel/lark.py new file mode 100644 index 0000000..25c316a --- /dev/null +++ b/heimdallr/channel/lark.py @@ -0,0 +1,49 @@ +import logging +from typing import Any, Tuple + +import requests + +from heimdallr.channel.base import Channel, Message +from heimdallr.config.config import get_config_str +from heimdallr.config.definition import SUFFIX_LARK_HOST, SUFFIX_LARK_TOKEN +from heimdallr.exception import ParamException + +logger = logging.getLogger(__name__) + + +class LarkWebhookMessage(Message): + def __init__(self, title: str, body: str, **kwargs) -> None: + super().__init__(title, body) + + def render_message(self) -> Any: + return {"msg_type": "text", "content": {"text": f"{self.title}\n{self.body}"}} + + +class LarkWebhook(Channel): + def __init__(self, name: str, type: str) -> None: + super().__init__(name, type) + self.base_url: str = "https://open.feishu.cn/open-apis/bot/v2/hook/" + self.token: str = "" + self._build_channel() + + def _build_channel(self) -> None: + self.base_url = get_config_str(self.get_name(), SUFFIX_LARK_HOST, self.base_url) + self.token = get_config_str(self.get_name(), SUFFIX_LARK_TOKEN, "") + if self.token == "": + raise ParamException("LarkWebhook key not set") + + def send(self, message: Message) -> Tuple[bool, str]: + if not isinstance(message, LarkWebhookMessage): + raise ParamException("Invalid message type") + + url = f"{self.base_url}{self.token}" + rs = requests.post( + url, + json=message.render_message(), + headers={"Content-Type": "application/json"}, + ).json() + logger.debug(f"LarkWebhook response: {rs}") + if rs["code"] != 0: + logger.error(f"LarkWebhook error: {rs['msg']}") + return False, rs["msg"] + return True, rs["msg"] diff --git a/heimdallr/channel/wecom.py b/heimdallr/channel/wecom.py index 694e75d..974bbbe 100644 --- a/heimdallr/channel/wecom.py +++ b/heimdallr/channel/wecom.py @@ -67,7 +67,7 @@ def render_message(self) -> Any: class WecomWebhook(Channel): def __init__(self, name: str, type: str): super().__init__(name, type) - self.base_url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=" + self.base_url: str = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=" self.key: str = "" self._build_channel() diff --git a/heimdallr/config/config.py b/heimdallr/config/config.py index 1611e4d..be9638c 100644 --- a/heimdallr/config/config.py +++ b/heimdallr/config/config.py @@ -28,10 +28,10 @@ def get_config_int(name: str, suffix: str, default: int = 0) -> int: return env.int(name + "_" + suffix, default) -def log_env_vars(): +def log_env_vars() -> None: for key, value in os.environ.items(): logger.debug(f"{key}: {value}") -def is_debug(): +def is_debug() -> bool: return env.bool("DEBUG", False) diff --git a/heimdallr/config/definition.py b/heimdallr/config/definition.py index de80f24..8d787d0 100644 --- a/heimdallr/config/definition.py +++ b/heimdallr/config/definition.py @@ -40,3 +40,6 @@ # ntfy.sh SUFFIX_NTFY_HOST = "NTFY_HOST" SUFFIX_NTFY_TOPIC = "NTFY_TOPIC" +# lark/feishu +SUFFIX_LARK_HOST = "LARK_HOST" +SUFFIX_LARK_TOKEN = "LARK_TOKEN"