add slack
This commit is contained in:
parent
346ad2c259
commit
f57e801236
@ -57,7 +57,7 @@ class Settings(BaseSettings):
|
||||
INDEXER: str = "builtin"
|
||||
# 索引站点,站点域名关键字使用,分隔
|
||||
INDEXER_SITES: str = ""
|
||||
# 消息通知渠道 telegram/wechat
|
||||
# 消息通知渠道 telegram/wechat/slack
|
||||
MESSAGER: str = "telegram"
|
||||
# WeChat企业ID
|
||||
WECHAT_CORPID: str = None
|
||||
@ -81,6 +81,12 @@ class Settings(BaseSettings):
|
||||
TELEGRAM_USERS: str = ""
|
||||
# Telegram 管理员ID,使用,分隔
|
||||
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
|
||||
DOWNLOADER: str = "qbittorrent"
|
||||
# Qbittorrent地址
|
||||
|
200
app/modules/slack/__init__.py
Normal file
200
app/modules/slack/__init__.py
Normal 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
325
app/modules/slack/slack.py
Normal 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
|
@ -35,4 +35,6 @@ pillow~=9.5.0
|
||||
pyTelegramBotAPI~=4.12.0
|
||||
playwright~=1.34.0
|
||||
cf_clearance~=0.29.2
|
||||
torrentool~=1.2.0
|
||||
torrentool~=1.2.0
|
||||
slack_bolt~=1.18.0
|
||||
slack_sdk
|
Loading…
x
Reference in New Issue
Block a user