Add files via upload

This commit is contained in:
公明
2026-07-13 19:08:57 +08:00
committed by GitHub
parent 8ea0ee4f1b
commit e5a4703e91
2 changed files with 195 additions and 0 deletions
+192
View File
@@ -0,0 +1,192 @@
package robot
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"cyberstrike-ai/internal/config"
"github.com/bwmarrin/discordgo"
lark "github.com/larksuite/oapi-sdk-go/v3"
larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
"github.com/slack-go/slack"
)
// SendProactive sends a message without an inbound event. Platforms whose
// reply credentials are event-scoped deliberately return an error instead of
// pretending delivery succeeded.
func SendProactive(ctx context.Context, cfg config.RobotsConfig, platform, externalUserID, message string) error {
platform = strings.ToLower(strings.TrimSpace(platform))
userID := robotIdentityUserPart(externalUserID)
if userID == "" {
return fmt.Errorf("invalid robot recipient")
}
switch platform {
case "telegram":
if !cfg.Telegram.Enabled || strings.TrimSpace(cfg.Telegram.BotToken) == "" {
return fmt.Errorf("telegram is not configured")
}
id, err := strconv.ParseInt(userID, 10, 64)
if err != nil {
return fmt.Errorf("invalid telegram user id: %w", err)
}
return telegramSendReply(ctx, nilSafeHTTPClient(), strings.TrimSpace(cfg.Telegram.BotToken), id, message)
case "slack":
if !cfg.Slack.Enabled || strings.TrimSpace(cfg.Slack.BotToken) == "" {
return fmt.Errorf("slack is not configured")
}
api := slack.New(strings.TrimSpace(cfg.Slack.BotToken))
channel, _, _, err := api.OpenConversationContext(ctx, &slack.OpenConversationParameters{Users: []string{userID}})
if err != nil {
return err
}
for _, chunk := range splitTextChunks(message, slackMaxMessageRunes) {
if _, _, err = api.PostMessageContext(ctx, channel.ID, slack.MsgOptionText(chunk, false)); err != nil {
return err
}
}
return nil
case "discord":
if !cfg.Discord.Enabled || strings.TrimSpace(cfg.Discord.BotToken) == "" {
return fmt.Errorf("discord is not configured")
}
token := strings.TrimSpace(cfg.Discord.BotToken)
if !strings.HasPrefix(token, "Bot ") {
token = "Bot " + token
}
session, err := discordgo.New(token)
if err != nil {
return err
}
channel, err := session.UserChannelCreate(userID)
if err != nil {
return err
}
for _, chunk := range splitTextChunks(message, discordMaxMessageRunes) {
if _, err = session.ChannelMessageSend(channel.ID, chunk); err != nil {
return err
}
}
return nil
case "wecom":
return sendWecomProactive(ctx, cfg.Wecom, userID, message)
case "lark":
return sendLarkProactive(ctx, cfg.Lark, externalUserID, message)
default:
return fmt.Errorf("platform %s does not support proactive alerts yet", platform)
}
}
func SupportsProactive(platform string) bool {
switch strings.ToLower(strings.TrimSpace(platform)) {
case "telegram", "slack", "discord", "wecom", "lark":
return true
default:
return false
}
}
func nilSafeHTTPClient() *http.Client { return &http.Client{Timeout: 15 * time.Second} }
func sendWecomProactive(ctx context.Context, cfg config.RobotWecomConfig, userID, message string) error {
if !cfg.Enabled || strings.TrimSpace(cfg.CorpID) == "" || strings.TrimSpace(cfg.Secret) == "" || cfg.AgentID == 0 {
return fmt.Errorf("wecom proactive API is not configured")
}
tokenURL := "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + cfg.CorpID + "&corpsecret=" + cfg.Secret
req, err := http.NewRequestWithContext(ctx, http.MethodGet, tokenURL, nil)
if err != nil {
return err
}
resp, err := nilSafeHTTPClient().Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var tokenResp struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
AccessToken string `json:"access_token"`
}
if err := json.Unmarshal(body, &tokenResp); err != nil {
return err
}
if tokenResp.ErrCode != 0 || tokenResp.AccessToken == "" {
return fmt.Errorf("wecom token: %s", tokenResp.ErrMsg)
}
payload, _ := json.Marshal(map[string]interface{}{
"touser": userID, "msgtype": "text", "agentid": cfg.AgentID,
"text": map[string]string{"content": message}, "safe": 0,
})
sendReq, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="+tokenResp.AccessToken, bytes.NewReader(payload))
if err != nil {
return err
}
sendReq.Header.Set("Content-Type", "application/json")
sendResp, err := nilSafeHTTPClient().Do(sendReq)
if err != nil {
return err
}
defer sendResp.Body.Close()
result, _ := io.ReadAll(sendResp.Body)
var parsed struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
if err := json.Unmarshal(result, &parsed); err != nil {
return err
}
if parsed.ErrCode != 0 {
return fmt.Errorf("wecom send: %s", parsed.ErrMsg)
}
return nil
}
func sendLarkProactive(ctx context.Context, cfg config.RobotLarkConfig, identity, message string) error {
if !cfg.Enabled || strings.TrimSpace(cfg.AppID) == "" || strings.TrimSpace(cfg.AppSecret) == "" {
return fmt.Errorf("lark is not configured")
}
receiveIDType, receiveID := "user_id", robotIdentityUserPart(identity)
if idx := strings.LastIndex(identity, "|o:"); idx >= 0 {
receiveIDType, receiveID = "open_id", strings.TrimSpace(identity[idx+3:])
}
if idx := strings.LastIndex(identity, "|n:"); idx >= 0 {
receiveIDType, receiveID = "union_id", strings.TrimSpace(identity[idx+3:])
}
if receiveID == "" {
return fmt.Errorf("invalid lark recipient")
}
content, _ := json.Marshal(larkTextContent{Text: message})
client := lark.NewClient(cfg.AppID, cfg.AppSecret)
resp, err := client.Im.Message.Create(ctx, larkim.NewCreateMessageReqBuilder().
ReceiveIdType(receiveIDType).
Body(larkim.NewCreateMessageReqBodyBuilder().ReceiveId(receiveID).MsgType(larkim.MsgTypeText).Content(string(content)).Build()).Build())
if err != nil {
return err
}
if resp == nil || !resp.Success() {
return fmt.Errorf("lark send failed")
}
return nil
}
func robotIdentityUserPart(identity string) string {
identity = strings.TrimSpace(identity)
if i := strings.LastIndex(identity, "|u:"); i >= 0 {
return strings.TrimSpace(identity[i+3:])
}
if strings.HasPrefix(identity, "u:") {
return strings.TrimSpace(identity[2:])
}
return identity
}