Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
149 lines
4.2 KiB
Go
149 lines
4.2 KiB
Go
package slack
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
|
|
"github.com/slack-go/slack"
|
|
"github.com/slack-go/slack/slackevents"
|
|
"github.com/slack-go/slack/socketmode"
|
|
|
|
"github.com/Tencent/WeKnora/internal/im"
|
|
"github.com/Tencent/WeKnora/internal/logger"
|
|
)
|
|
|
|
// MessageHandler is called when an IM message is received via long connection.
|
|
type MessageHandler func(ctx context.Context, msg *im.IncomingMessage) error
|
|
|
|
// LongConnClient manages a Slack Socket Mode long connection.
|
|
type LongConnClient struct {
|
|
appToken string
|
|
botToken string
|
|
handler MessageHandler
|
|
|
|
api *slack.Client
|
|
client *socketmode.Client
|
|
}
|
|
|
|
// NewLongConnClient creates a Slack long connection client.
|
|
func NewLongConnClient(appToken, botToken string, handler MessageHandler) *LongConnClient {
|
|
api := slack.New(
|
|
botToken,
|
|
slack.OptionAppLevelToken(appToken),
|
|
)
|
|
|
|
client := socketmode.New(
|
|
api,
|
|
socketmode.OptionDebug(false), // true for debugging
|
|
)
|
|
|
|
return &LongConnClient{
|
|
appToken: appToken,
|
|
botToken: botToken,
|
|
handler: handler,
|
|
api: api,
|
|
client: client,
|
|
}
|
|
}
|
|
|
|
// GetAPI returns the underlying slack API client.
|
|
func (c *LongConnClient) GetAPI() *slack.Client {
|
|
return c.api
|
|
}
|
|
|
|
// Start begins the WebSocket long connection. It blocks until ctx is cancelled.
|
|
func (c *LongConnClient) Start(ctx context.Context) error {
|
|
logger.Infof(ctx, "[IM] Slack WebSocket connecting...")
|
|
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case evt := <-c.client.Events:
|
|
switch evt.Type {
|
|
case socketmode.EventTypeConnecting:
|
|
logger.Infof(ctx, "[Slack] Connecting to Slack with Socket Mode...")
|
|
case socketmode.EventTypeConnectionError:
|
|
logger.Errorf(ctx, "[Slack] Connection failed. Retrying later...")
|
|
case socketmode.EventTypeConnected:
|
|
logger.Infof(ctx, "[IM] Slack WebSocket connected successfully")
|
|
case socketmode.EventTypeEventsAPI:
|
|
eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent)
|
|
if !ok {
|
|
logger.Warnf(ctx, "[Slack] Ignored %+v", evt)
|
|
continue
|
|
}
|
|
|
|
// Acknowledge the event
|
|
c.client.Ack(*evt.Request)
|
|
|
|
// Handle the event
|
|
c.handleEvent(ctx, eventsAPIEvent, evt.Request.Payload)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
return c.client.RunContext(ctx)
|
|
}
|
|
|
|
func (c *LongConnClient) handleEvent(ctx context.Context, eventsAPIEvent slackevents.EventsAPIEvent, rawPayload json.RawMessage) {
|
|
logger.Infof(ctx, "[Slack] Received event type: %s", eventsAPIEvent.Type)
|
|
switch eventsAPIEvent.Type {
|
|
case slackevents.CallbackEvent:
|
|
var rawEvent struct {
|
|
Event struct {
|
|
Files []slack.File `json:"files"`
|
|
} `json:"event"`
|
|
}
|
|
_ = json.Unmarshal(rawPayload, &rawEvent)
|
|
files := rawEvent.Event.Files
|
|
|
|
innerEvent := eventsAPIEvent.InnerEvent
|
|
logger.Infof(ctx, "[Slack] Received inner event type: %s", innerEvent.Type)
|
|
switch ev := innerEvent.Data.(type) {
|
|
case *slackevents.AppMentionEvent:
|
|
logger.Infof(ctx, "[Slack] AppMentionEvent: user=%s channel=%s text=%s", ev.User, ev.Channel, ev.Text)
|
|
// Handle @bot mention in a channel
|
|
threadTs := ev.ThreadTimeStamp
|
|
if threadTs == "" {
|
|
threadTs = ev.TimeStamp
|
|
}
|
|
c.processMessage(ctx, ev.User, ev.Channel, ev.Text, threadTs, im.ChatTypeGroup, files)
|
|
case *slackevents.MessageEvent:
|
|
logger.Infof(ctx, "[Slack] MessageEvent: user=%s channel=%s text=%s subtype=%s bot_id=%s", ev.User, ev.Channel, ev.Text, ev.SubType, ev.BotID)
|
|
if ev.BotID != "" {
|
|
return
|
|
}
|
|
if ev.SubType != "" && ev.SubType != "file_share" {
|
|
return
|
|
}
|
|
|
|
chatType := im.ChatTypeDirect
|
|
if ev.ChannelType == "channel" || ev.ChannelType == "group" {
|
|
chatType = im.ChatTypeGroup
|
|
}
|
|
|
|
threadTs := ev.ThreadTimeStamp
|
|
if threadTs == "" {
|
|
threadTs = ev.TimeStamp
|
|
}
|
|
|
|
c.processMessage(ctx, ev.User, ev.Channel, ev.Text, threadTs, chatType, files)
|
|
default:
|
|
logger.Warnf(ctx, "[Slack] Unhandled inner event type: %T", innerEvent.Data)
|
|
}
|
|
default:
|
|
logger.Warnf(ctx, "[Slack] Unhandled event type: %s", eventsAPIEvent.Type)
|
|
}
|
|
}
|
|
|
|
func (c *LongConnClient) processMessage(ctx context.Context, user, channel, text, ts string, chatType im.ChatType, files []slack.File) {
|
|
incoming := parseIncomingMessage(user, channel, text, ts, chatType, files)
|
|
|
|
if err := c.handler(ctx, incoming); err != nil {
|
|
logger.Errorf(ctx, "[Slack] Handle message error: %v", err)
|
|
}
|
|
}
|