add slack

This commit is contained in:
jxxghp 2023-06-11 13:15:24 +08:00
parent 346ad2c259
commit f57e801236
4 changed files with 535 additions and 2 deletions

View File

@ -57,7 +57,7 @@ class Settings(BaseSettings):
INDEXER: str = "builtin" INDEXER: str = "builtin"
# 索引站点,站点域名关键字使用,分隔 # 索引站点,站点域名关键字使用,分隔
INDEXER_SITES: str = "" INDEXER_SITES: str = ""
# 消息通知渠道 telegram/wechat # 消息通知渠道 telegram/wechat/slack
MESSAGER: str = "telegram" MESSAGER: str = "telegram"
# WeChat企业ID # WeChat企业ID
WECHAT_CORPID: str = None WECHAT_CORPID: str = None
@ -81,6 +81,12 @@ class Settings(BaseSettings):
TELEGRAM_USERS: str = "" TELEGRAM_USERS: str = ""
# Telegram 管理员ID使用,分隔 # Telegram 管理员ID使用,分隔
TELEGRAM_ADMINS: str = "" TELEGRAM_ADMINS: str = ""
# Slack Bot User OAuth Token
SLACK_OAUTH_TOKEN: str = ""
# Slack App-Level Token
SLACK_APP_TOKEN: str = ""
# Slack 频道名称
SLACK_CHANNEL: str = ""
# 下载器 qbittorrent/transmission # 下载器 qbittorrent/transmission
DOWNLOADER: str = "qbittorrent" DOWNLOADER: str = "qbittorrent"
# Qbittorrent地址 # Qbittorrent地址

View File

@ -0,0 +1,200 @@
import json
import re
from typing import Optional, Union, List, Tuple, Any
from app.core.context import MediaInfo, Context
from app.core.config import settings
from app.log import logger
from app.modules import _ModuleBase
from app.modules.slack.slack import Slack
class SlackModule(_ModuleBase):
slack: Slack = None
def init_module(self) -> None:
self.slack = Slack()
def stop(self):
self.slack.stop()
def init_setting(self) -> Tuple[str, Union[str, bool]]:
return "MESSAGER", "slack"
def message_parser(self, body: Any, form: Any, args: Any) -> Optional[dict]:
"""
解析消息内容返回字典注意以下约定值
userid: 用户ID
username: 用户名
text: 内容
:param body: 请求体
:param form: 表单
:param args: 参数
:return: 消息内容用户ID
"""
"""
# 消息
{
'client_msg_id': '',
'type': 'message',
'text': 'hello',
'user': '',
'ts': '1670143568.444289',
'blocks': [{
'type': 'rich_text',
'block_id': 'i2j+',
'elements': [{
'type': 'rich_text_section',
'elements': [{
'type': 'text',
'text': 'hello'
}]
}]
}],
'team': '',
'client': '',
'event_ts': '1670143568.444289',
'channel_type': 'im'
}
# 快捷方式
{
"type": "shortcut",
"token": "XXXXXXXXXXXXX",
"action_ts": "1581106241.371594",
"team": {
"id": "TXXXXXXXX",
"domain": "shortcuts-test"
},
"user": {
"id": "UXXXXXXXXX",
"username": "aman",
"team_id": "TXXXXXXXX"
},
"callback_id": "shortcut_create_task",
"trigger_id": "944799105734.773906753841.38b5894552bdd4a780554ee59d1f3638"
}
# 按钮点击
{
"type": "block_actions",
"team": {
"id": "T9TK3CUKW",
"domain": "example"
},
"user": {
"id": "UA8RXUSPL",
"username": "jtorrance",
"team_id": "T9TK3CUKW"
},
"api_app_id": "AABA1ABCD",
"token": "9s8d9as89d8as9d8as989",
"container": {
"type": "message_attachment",
"message_ts": "1548261231.000200",
"attachment_id": 1,
"channel_id": "CBR2V3XEX",
"is_ephemeral": false,
"is_app_unfurl": false
},
"trigger_id": "12321423423.333649436676.d8c1bb837935619ccad0f624c448ffb3",
"client": {
"id": "CBR2V3XEX",
"name": "review-updates"
},
"message": {
"bot_id": "BAH5CA16Z",
"type": "message",
"text": "This content can't be displayed.",
"user": "UAJ2RU415",
"ts": "1548261231.000200",
...
},
"response_url": "https://hooks.slack.com/actions/AABA1ABCD/1232321423432/D09sSasdasdAS9091209",
"actions": [
{
"action_id": "WaXA",
"block_id": "=qXel",
"text": {
"type": "plain_text",
"text": "View",
"emoji": true
},
"value": "click_me_123",
"type": "button",
"action_ts": "1548426417.840180"
}
]
}
"""
# 校验token
token = args.get("token")
if not token or token != settings.API_TOKEN:
return None
try:
msg_json: dict = json.loads(body)
except Exception as err:
logger.error(f"解析Slack消息失败{err}")
return None
if msg_json:
if msg_json.get("type") == "message":
userid = msg_json.get("user")
text = msg_json.get("text")
username = msg_json.get("user")
elif msg_json.get("type") == "block_actions":
userid = msg_json.get("user", {}).get("id")
text = msg_json.get("actions")[0].get("value")
username = msg_json.get("user", {}).get("name")
elif msg_json.get("type") == "event_callback":
userid = msg_json.get('event', {}).get('user')
text = re.sub(r"<@[0-9A-Z]+>", "", msg_json.get("event", {}).get("text"), flags=re.IGNORECASE).strip()
username = ""
elif msg_json.get("type") == "shortcut":
userid = msg_json.get("user", {}).get("id")
text = msg_json.get("callback_id")
username = msg_json.get("user", {}).get("username")
else:
return None
logger.info(f"收到Slack消息userid={userid}, username={username}, text={text}")
return {
"userid": userid,
"username": username,
"text": text
}
return None
def post_message(self, title: str,
text: str = None, image: str = None,
userid: Union[str, int] = None) -> Optional[bool]:
"""
发送消息
:param title: 标题
:param text: 内容
:param image: 图片
:param userid: 用户ID
:return: 成功或失败
"""
return self.slack.send_msg(title=title, text=text, image=image, userid=userid)
def post_medias_message(self, title: str, items: List[MediaInfo],
userid: Union[str, int] = None) -> Optional[bool]:
"""
发送媒体信息选择列表
:param title: 标题
:param items: 消息列表
:param userid: 用户ID
:return: 成功或失败
"""
return self.slack.send_meidas_msg(title=title, medias=items, userid=userid)
def post_torrents_message(self, title: str, items: List[Context],
mediainfo: MediaInfo = None,
userid: Union[str, int] = None) -> Optional[bool]:
"""
发送种子信息选择列表
:param title: 标题
:param items: 消息列表
:param mediainfo: 媒体信息
:param userid: 用户ID
:return: 成功或失败
"""
return self.slack.send_torrents_msg(title=title, torrents=items,
userid=userid)

325
app/modules/slack/slack.py Normal file
View File

@ -0,0 +1,325 @@
import re
from threading import Lock
from typing import List, Optional
import requests
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
from slack_sdk import WebClient
from app.core.config import settings
from app.core.context import MediaInfo, Context
from app.core.metainfo import MetaInfo
from app.log import logger
from app.utils.string import StringUtils
lock = Lock()
class Slack:
_client: WebClient = None
_service: SocketModeHandler = None
_ds_url = f"http://127.0.0.1:{settings.PORT}/api/v1/messages?token={settings.API_TOKEN}"
def __init__(self):
if not settings.SLACK_OAUTH_TOKEN or not settings.SLACK_APP_TOKEN:
return
try:
slack_app = App(token=settings.SLACK_OAUTH_TOKEN,
ssl_check_enabled=False,
url_verification_enabled=False)
except Exception as err:
logger.error(f"Slack初始化失败: {err}")
return
self._client = slack_app.client
# 注册消息响应
@slack_app.event("message")
def slack_message(message):
local_res = requests.post(self._ds_url, json=message, timeout=10)
logger.debug("message: %s processed, response is: %s" % (message, local_res.text))
@slack_app.action(re.compile(r"actionId-\d+"))
def slack_action(ack, body):
ack()
local_res = requests.post(self._ds_url, json=body, timeout=60)
logger.debug("message: %s processed, response is: %s" % (body, local_res.text))
@slack_app.event("app_mention")
def slack_mention(say, body):
say(f"收到,请稍等... <@{body.get('event', {}).get('user')}>")
local_res = requests.post(self._ds_url, json=body, timeout=10)
logger.debug("message: %s processed, response is: %s" % (body, local_res.text))
@slack_app.shortcut(re.compile(r"/*"))
def slack_shortcut(ack, body):
ack()
local_res = requests.post(self._ds_url, json=body, timeout=10)
logger.debug("message: %s processed, response is: %s" % (body, local_res.text))
# 启动服务
try:
self._service = SocketModeHandler(
slack_app,
settings.SLACK_APP_TOKEN
)
self._service.connect()
logger.info("Slack消息接收服务启动")
except Exception as err:
logger.error("Slack消息接收服务启动失败: %s" % str(err))
def stop(self):
if self._service:
try:
self._service.close()
logger.info("Slack消息接收服务已停止")
except Exception as err:
logger.error("Slack消息接收服务停止失败: %s" % str(err))
def send_msg(self, title: str, text: str = "", image: str = "", url: str = "", userid: str = ""):
"""
发送Telegram消息
:param title: 消息标题
:param text: 消息内容
:param image: 消息图片地址
:param url: 点击消息转转的URL
:param userid: 用户ID如有则只发消息给该用户
:user_id: 发送消息的目标用户ID为空则发给管理员
"""
if not self._client:
return False, "消息客户端未就绪"
if not title and not text:
return False, "标题和内容不能同时为空"
try:
if userid:
channel = userid
else:
# 消息广播
channel = self.__find_public_channel()
# 拼装消息内容
block = {
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*{title}*\n{text}"
}
}
# 消息图片
if image:
block['accessory'] = {
"type": "image",
"image_url": f"{image}",
"alt_text": f"{title}"
}
blocks = [block]
# 链接
if image and url:
blocks.append({
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "查看详情",
"emoji": True
},
"value": "click_me_url",
"url": f"{url}",
"action_id": "actionId-url"
}
]
})
# 发送
result = self._client.chat_postMessage(
channel=channel,
blocks=blocks
)
return True, result
except Exception as msg_e:
logger.error(f"Slack消息发送失败: {msg_e}")
return False, str(msg_e)
def send_meidas_msg(self, medias: List[MediaInfo], userid: str = "", title: str = "") -> Optional[bool]:
"""
发送列表类消息
"""
if not self._client:
return False
if not medias:
return False
try:
if userid:
channel = userid
else:
# 消息广播
channel = self.__find_public_channel()
# 消息主体
title_section = {
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*{title}*"
}
}
blocks = [title_section]
# 列表
if medias:
blocks.append({
"type": "divider"
})
index = 1
for media in medias:
if media.get_poster_image():
if media.get_star_string():
text = f"{index}. *<{media.get_detail_url()}|{media.get_title_string()}>*" \
f"\n类型:{media.type.value}" \
f"\n{media.get_star_string()}" \
f"\n{media.get_overview_string(50)}"
else:
text = f"{index}. *<{media.get_detail_url()}|{media.get_title_string()}>*" \
f"\n类型:{media.type.value}" \
f"\n{media.get_overview_string(50)}"
blocks.append(
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": text
},
"accessory": {
"type": "image",
"image_url": f"{media.get_poster_image()}",
"alt_text": f"{media.get_title_string()}"
}
}
)
blocks.append(
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "选择",
"emoji": True
},
"value": f"{index}",
"action_id": f"actionId-{index}"
}
]
}
)
index += 1
# 发送
result = self._client.chat_postMessage(
channel=channel,
blocks=blocks
)
return True if result else False
except Exception as msg_e:
logger.error(f"Slack消息发送失败: {msg_e}")
return False
def send_torrents_msg(self, torrents: List[Context],
userid: str = "", title: str = "") -> Optional[bool]:
"""
发送列表消息
"""
if not self._client:
return None
try:
if userid:
channel = userid
else:
# 消息广播
channel = self.__find_public_channel()
# 消息主体
title_section = {
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*{title}*"
}
}
blocks = [title_section, {
"type": "divider"
}]
# 列表
index = 1
for context in torrents:
torrent = context.torrent_info
site_name = torrent.site_name
meta = MetaInfo(torrent.title, torrent.description)
link = torrent.page_url
title = f"{meta.get_season_episode_string()} " \
f"{meta.get_resource_type_string()} " \
f"{meta.get_resource_team_string()} " \
f"{StringUtils.str_filesize(torrent.size)}"
title = re.sub(r"\s+", " ", title).strip()
free = torrent.get_volume_factor_string()
seeder = f"{torrent.seeders}"
description = torrent.description
text = f"{index}. 【{site_name}】[{title}]({link}) {free} {seeder}\n- {description}"
blocks.append(
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": text
}
}
)
blocks.append(
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "选择",
"emoji": True
},
"value": f"{index}",
"action_id": f"actionId-{index}"
}
]
}
)
index += 1
# 发送
result = self._client.chat_postMessage(
channel=channel,
blocks=blocks
)
return True if result else False
except Exception as msg_e:
logger.error(f"Slack消息发送失败: {msg_e}")
return False
def __find_public_channel(self):
"""
查找公共频道
"""
if not self._client:
return ""
conversation_id = ""
try:
for result in self._client.conversations_list():
if conversation_id:
break
for channel in result["channels"]:
if channel.get("name") == settings.SLACK_CHANNEL or "全体":
conversation_id = channel.get("id")
break
except Exception as e:
logger.error(f"查找Slack公共频道失败: {e}")
return conversation_id

View File

@ -36,3 +36,5 @@ pyTelegramBotAPI~=4.12.0
playwright~=1.34.0 playwright~=1.34.0
cf_clearance~=0.29.2 cf_clearance~=0.29.2
torrentool~=1.2.0 torrentool~=1.2.0
slack_bolt~=1.18.0
slack_sdk