Skip to content

Commit c0dbf6f

Browse files
committed
feat:add support for slack
1 parent 629ebae commit c0dbf6f

File tree

9 files changed

+429
-2
lines changed

9 files changed

+429
-2
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ botpy.log*
3939
/libs/wecom_api/test.py
4040
/venv
4141
/jp-tyo-churros-05.rockchin.top
42+
test.py

libs/slack_api/__init__.py

Whitespace-only changes.

libs/slack_api/api.py

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import json
2+
from quart import Quart, jsonify,request
3+
from slack_sdk.web.async_client import AsyncWebClient
4+
from .slackevent import SlackEvent
5+
from typing import Callable, Dict, Any
6+
from pkg.platform.types import events as platform_events, message as platform_message
7+
8+
class SlackClient():
9+
10+
def __init__(self,bot_token:str,signing_secret:str):
11+
12+
self.bot_token = bot_token
13+
self.signing_secret = signing_secret
14+
self.app = Quart(__name__)
15+
self.client = AsyncWebClient(self.bot_token)
16+
self.app.add_url_rule('/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST'])
17+
self._message_handlers = {
18+
"example":[],
19+
}
20+
self.bot_user_id = None # avoid block
21+
22+
async def handle_callback_request(self):
23+
try:
24+
body = await request.get_data()
25+
data = json.loads(body)
26+
print("shoudao:")
27+
print(data)
28+
bot_user_id = data.get("event",{}).get("bot_id","")
29+
30+
if self.bot_user_id and bot_user_id == self.bot_user_id:
31+
return jsonify({'status': 'ok'})
32+
33+
if data and data.get("event", {}).get("channel_type") in ["im", "channel"]:
34+
event = SlackEvent.from_payload(data)
35+
await self._handle_message(event)
36+
return jsonify({'status': 'ok'})
37+
38+
except Exception as e:
39+
raise(e)
40+
41+
42+
43+
async def _handle_message(self, event: SlackEvent):
44+
"""
45+
处理消息事件。
46+
"""
47+
msg_type = event.type
48+
if msg_type in self._message_handlers:
49+
for handler in self._message_handlers[msg_type]:
50+
await handler(event)
51+
52+
def on_message(self, msg_type: str):
53+
"""注册消息类型处理器"""
54+
def decorator(func: Callable[[platform_events.Event], None]):
55+
if msg_type not in self._message_handlers:
56+
self._message_handlers[msg_type] = []
57+
self._message_handlers[msg_type].append(func)
58+
return func
59+
return decorator
60+
61+
async def send_message_to_channle(self,text:str,channel_id:str):
62+
try:
63+
response = await self.client.chat_postMessage(
64+
channel=channel_id,
65+
text=text
66+
)
67+
if self.bot_user_id is None and response.get("ok"):
68+
self.bot_user_id = response["message"]["bot_id"]
69+
print("bot_id:")
70+
print(self.bot_user_id)
71+
print("fanhui:")
72+
print(response)
73+
return
74+
except Exception as e:
75+
raise e
76+
77+
async def send_message_to_one(self,text:str,user_id:str):
78+
try:
79+
response = await self.client.chat_postMessage(
80+
channel = '@'+user_id,
81+
text= text
82+
)
83+
if self.bot_user_id is None and response.get("ok"):
84+
self.bot_user_id = response["message"]["bot_id"]
85+
print("bot_id:")
86+
print(self.bot_user_id)
87+
88+
return
89+
except Exception as e:
90+
raise e
91+
92+
async def run_task(self, host: str, port: int, *args, **kwargs):
93+
"""
94+
启动 Quart 应用。
95+
"""
96+
await self.app.run_task(host=host, port=port, *args, **kwargs)
97+
98+
99+
100+
101+
102+
103+
104+

libs/slack_api/slackevent.py

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from typing import Dict, Any, Optional
2+
3+
class SlackEvent(dict):
4+
@staticmethod
5+
def from_payload(payload: Dict[str, Any]) -> Optional["SlackEvent"]:
6+
try:
7+
event = SlackEvent(payload)
8+
return event
9+
except KeyError:
10+
return None
11+
12+
@property
13+
def text(self) -> str:
14+
if self.get("event", {}).get("channel_type") == "im":
15+
elements = self["event"]["blocks"][0]["elements"][0]["elements"]
16+
for el in elements:
17+
if el.get("type") == "text":
18+
return el.get("text", "")
19+
20+
if self.get("event",{}).get("channel_type") == "channel":
21+
elements = self["event"]["blocks"][0]["elements"][0]["elements"]
22+
text_result = next((el["text"] for el in elements if el["type"] == "text"), "")
23+
return text_result
24+
25+
26+
return ""
27+
28+
29+
@property
30+
def user_id(self) -> Optional[str]:
31+
return self.get("event", {}).get("user","")
32+
33+
@property
34+
def channel_id(self) -> Optional[str]:
35+
return self.get("event", {}).get("channel","")
36+
37+
@property
38+
def type(self) -> str:
39+
""" message对应私聊,app_mention对应频道at """
40+
return self.get("event", {}).get("channel_type", "")
41+
42+
@property
43+
def message_id(self) -> str:
44+
return self.get("event_id","")
45+
46+
@property
47+
def pic_url(self) -> str:
48+
"""提取 Slack 事件中的图片 URL"""
49+
files = self.get("event", {}).get("files", [])
50+
if files:
51+
return files[0].get("url_private", "")
52+
return ""
53+
54+
55+
@property
56+
def sender_name(self) -> str:
57+
return self.get("event", {}).get("user","")
58+
59+
def __getattr__(self, key: str) -> Optional[Any]:
60+
return self.get(key)
61+
62+
def __setattr__(self, key: str, value: Any) -> None:
63+
self[key] = value
64+
65+
def __repr__(self) -> str:
66+
return f"<SlackEvent {super().__repr__()}>"

pkg/platform/sources/slack.py

+200
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
from __future__ import annotations
2+
import typing
3+
import asyncio
4+
import traceback
5+
6+
import datetime
7+
8+
from libs.slack_api.api import SlackClient
9+
from pkg.platform.adapter import MessagePlatformAdapter
10+
from pkg.platform.types import events as platform_events, message as platform_message
11+
from libs.slack_api.slackevent import SlackEvent
12+
from pkg.core import app
13+
from .. import adapter
14+
from ...core import app
15+
from ..types import message as platform_message
16+
from ..types import events as platform_events
17+
from ..types import entities as platform_entities
18+
from ...command.errors import ParamNotEnoughError
19+
from ...utils import image
20+
21+
class SlackMessageConverter(adapter.MessageConverter):
22+
23+
@staticmethod
24+
async def yiri2target(message_chain:platform_message.MessageChain):
25+
content_list = []
26+
for msg in message_chain:
27+
if type(msg) is platform_message.Plain:
28+
content_list.append({
29+
"content":msg.text,
30+
})
31+
32+
return content_list
33+
34+
@staticmethod
35+
async def target2yiri(message:str,message_id:str,pic_url:str):
36+
yiri_msg_list = []
37+
yiri_msg_list.append(
38+
platform_message.Source(id=message_id,time=datetime.datetime.now())
39+
)
40+
if pic_url is not None:
41+
base64_url = await image.get_slack_image_to_base64(pic_url=pic_url)
42+
yiri_msg_list.append(
43+
platform_message.Image(base64=base64_url)
44+
)
45+
46+
yiri_msg_list.append(platform_message.Plain(text=message))
47+
chain = platform_message.MessageChain(yiri_msg_list)
48+
return chain
49+
50+
51+
class SlackEventConverter(adapter.EventConverter):
52+
53+
@staticmethod
54+
async def yiri2target(event:platform_events.MessageEvent) -> SlackEvent:
55+
return event.source_platform_object
56+
57+
@staticmethod
58+
async def target2yiri(event:SlackEvent):
59+
yiri_chain = await SlackMessageConverter.target2yiri(
60+
message=event.text,message_id=event.message_id,pic_url=event.pic_url
61+
)
62+
63+
if event.type == 'channel':
64+
yiri_chain.insert(0, platform_message.At(target="SlackBot"))
65+
66+
sender = platform_entities.GroupMember(
67+
id = event.user_id,
68+
member_name= str(event.sender_name),
69+
permission= 'MEMBER',
70+
group = platform_entities.Group(
71+
id = event.channel_id,
72+
name = 'MEMBER',
73+
permission= platform_entities.Permission.Member
74+
),
75+
special_title='',
76+
join_timestamp=0,
77+
last_speak_timestamp=0,
78+
mute_time_remaining=0
79+
)
80+
time = int(datetime.datetime.utcnow().timestamp())
81+
return platform_events.GroupMessage(
82+
sender = sender,
83+
message_chain=yiri_chain,
84+
time = time,
85+
source_platform_object=event
86+
)
87+
88+
if event.type == 'im':
89+
return platform_events.FriendMessage(
90+
sender=platform_entities.Friend(
91+
id=event.user_id,
92+
nickname = event.sender_name,
93+
remark=""
94+
),
95+
message_chain = yiri_chain,
96+
time = float(datetime.datetime.now().timestamp()),
97+
source_platform_object=event,
98+
)
99+
100+
101+
102+
103+
class SlackAdapter(adapter.MessagePlatformAdapter):
104+
bot: SlackClient
105+
ap: app.Application
106+
bot_account_id: str
107+
message_converter: SlackMessageConverter = SlackMessageConverter()
108+
event_converter: SlackEventConverter = SlackEventConverter()
109+
config: dict
110+
111+
def __init__(self,config:dict,ap:app.Application):
112+
self.config = config
113+
self.ap = app.Application
114+
required_keys = [
115+
"bot_token",
116+
"signing_secret",
117+
]
118+
missing_keys = [key for key in required_keys if key not in config]
119+
if missing_keys:
120+
raise ParamNotEnoughError("Slack机器人缺少相关配置项,请查看文档或联系管理员")
121+
122+
self.bot = SlackClient(
123+
bot_token=self.config["bot_token"],
124+
signing_secret=self.config["signing_secret"]
125+
)
126+
127+
async def reply_message(
128+
self,
129+
message_source: platform_events.MessageEvent,
130+
message: platform_message.MessageChain,
131+
quote_origin: bool = False,
132+
):
133+
slack_event = await SlackEventConverter.yiri2target(
134+
message_source
135+
)
136+
137+
content_list = await SlackMessageConverter.yiri2target(message)
138+
139+
for content in content_list:
140+
if slack_event.type == 'channel':
141+
print("fasong1")
142+
await self.bot.send_message_to_channle(
143+
content['content'],slack_event.channel_id
144+
)
145+
if slack_event.type == 'im':
146+
await self.bot.send_message_to_one(
147+
content['content'],slack_event.user_id
148+
)
149+
150+
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
151+
pass
152+
153+
154+
def register_listener(
155+
self,
156+
event_type: typing.Type[platform_events.Event],
157+
callback: typing.Callable[
158+
[platform_events.Event, adapter.MessagePlatformAdapter], None
159+
],
160+
):
161+
async def on_message(event:SlackEvent):
162+
self.bot_account_id = "SlackBot"
163+
try:
164+
return await callback(
165+
await self.event_converter.target2yiri(event),self
166+
)
167+
except:
168+
traceback.print_exc()
169+
170+
if event_type == platform_events.FriendMessage:
171+
self.bot.on_message("im")(on_message)
172+
elif event_type == platform_events.GroupMessage:
173+
self.bot.on_message("channel")(on_message)
174+
175+
176+
async def run_async(self):
177+
async def shutdown_trigger_placeholder():
178+
while True:
179+
await asyncio.sleep(1)
180+
181+
await self.bot.run_task(
182+
host=self.config["host"],
183+
port=self.config["port"],
184+
shutdown_trigger=shutdown_trigger_placeholder,
185+
)
186+
187+
async def kill(self) -> bool:
188+
return False
189+
190+
async def unregister_listener(
191+
self,
192+
event_type: type,
193+
callback: typing.Callable[[platform_events.Event, MessagePlatformAdapter], None],
194+
):
195+
return super().unregister_listener(event_type, callback)
196+
197+
198+
199+
200+

0 commit comments

Comments
 (0)