mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
fix: double reply bug
This commit is contained in:
@@ -13,401 +13,460 @@ import (
|
||||
"github.com/memohai/memoh/internal/channel/adapters/common"
|
||||
)
|
||||
|
||||
const inboundDedupTTL = time.Minute
|
||||
|
||||
type DiscordAdapter struct {
|
||||
logger *slog.Logger
|
||||
mu sync.RWMutex
|
||||
sessions map[string]*discordgo.Session // keyed by bot token
|
||||
logger *slog.Logger
|
||||
mu sync.RWMutex
|
||||
sessions map[string]*discordgo.Session // keyed by bot token
|
||||
handlerRemovers map[string]func() // keyed by bot token
|
||||
seenMessages map[string]time.Time // keyed by token:messageID
|
||||
}
|
||||
|
||||
func NewDiscordAdapter(log *slog.Logger) *DiscordAdapter {
|
||||
if log == nil {
|
||||
log = slog.Default()
|
||||
}
|
||||
return &DiscordAdapter{
|
||||
logger: log.With(slog.String("adapter", "discord")),
|
||||
sessions: make(map[string]*discordgo.Session),
|
||||
}
|
||||
if log == nil {
|
||||
log = slog.Default()
|
||||
}
|
||||
return &DiscordAdapter{
|
||||
logger: log.With(slog.String("adapter", "discord")),
|
||||
sessions: make(map[string]*discordgo.Session),
|
||||
handlerRemovers: make(map[string]func()),
|
||||
seenMessages: make(map[string]time.Time),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) Type() channel.ChannelType {
|
||||
return Type
|
||||
return Type
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) Descriptor() channel.Descriptor {
|
||||
return channel.Descriptor{
|
||||
Type: Type,
|
||||
DisplayName: "Discord",
|
||||
Capabilities: channel.ChannelCapabilities{
|
||||
Text: true,
|
||||
Markdown: true,
|
||||
Reply: true,
|
||||
Attachments: true,
|
||||
Media: true,
|
||||
Streaming: true,
|
||||
BlockStreaming: true,
|
||||
Reactions: true,
|
||||
},
|
||||
ConfigSchema: channel.ConfigSchema{
|
||||
Version: 1,
|
||||
Fields: map[string]channel.FieldSchema{
|
||||
"botToken": {
|
||||
Type: channel.FieldSecret,
|
||||
Required: true,
|
||||
Title: "Bot Token",
|
||||
},
|
||||
},
|
||||
},
|
||||
UserConfigSchema: channel.ConfigSchema{
|
||||
Version: 1,
|
||||
Fields: map[string]channel.FieldSchema{
|
||||
"user_id": {Type: channel.FieldString},
|
||||
"channel_id": {Type: channel.FieldString},
|
||||
"guild_id": {Type: channel.FieldString},
|
||||
"username": {Type: channel.FieldString},
|
||||
},
|
||||
},
|
||||
TargetSpec: channel.TargetSpec{
|
||||
Format: "channel_id | user_id",
|
||||
Hints: []channel.TargetHint{
|
||||
{Label: "Channel ID", Example: "1234567890123456789"},
|
||||
{Label: "User ID", Example: "1234567890123456789"},
|
||||
},
|
||||
},
|
||||
}
|
||||
return channel.Descriptor{
|
||||
Type: Type,
|
||||
DisplayName: "Discord",
|
||||
Capabilities: channel.ChannelCapabilities{
|
||||
Text: true,
|
||||
Markdown: true,
|
||||
Reply: true,
|
||||
Attachments: true,
|
||||
Media: true,
|
||||
Streaming: true,
|
||||
BlockStreaming: true,
|
||||
Reactions: true,
|
||||
},
|
||||
ConfigSchema: channel.ConfigSchema{
|
||||
Version: 1,
|
||||
Fields: map[string]channel.FieldSchema{
|
||||
"botToken": {
|
||||
Type: channel.FieldSecret,
|
||||
Required: true,
|
||||
Title: "Bot Token",
|
||||
},
|
||||
},
|
||||
},
|
||||
UserConfigSchema: channel.ConfigSchema{
|
||||
Version: 1,
|
||||
Fields: map[string]channel.FieldSchema{
|
||||
"user_id": {Type: channel.FieldString},
|
||||
"channel_id": {Type: channel.FieldString},
|
||||
"guild_id": {Type: channel.FieldString},
|
||||
"username": {Type: channel.FieldString},
|
||||
},
|
||||
},
|
||||
TargetSpec: channel.TargetSpec{
|
||||
Format: "channel_id | user_id",
|
||||
Hints: []channel.TargetHint{
|
||||
{Label: "Channel ID", Example: "1234567890123456789"},
|
||||
{Label: "User ID", Example: "1234567890123456789"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) getOrCreateSession(token, configID string) (*discordgo.Session, error) {
|
||||
a.mu.RLock()
|
||||
session, ok := a.sessions[token]
|
||||
a.mu.RUnlock()
|
||||
if ok {
|
||||
return session, nil
|
||||
}
|
||||
a.mu.RLock()
|
||||
session, ok := a.sessions[token]
|
||||
a.mu.RUnlock()
|
||||
if ok {
|
||||
return session, nil
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if s, ok := a.sessions[token]; ok {
|
||||
return s, nil
|
||||
}
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if s, ok := a.sessions[token]; ok {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
session, err := discordgo.New("Bot " + token)
|
||||
if err != nil {
|
||||
a.logger.Error("create session failed", slog.String("config_id", configID), slog.Any("error", err))
|
||||
return nil, err
|
||||
}
|
||||
session, err := discordgo.New("Bot " + token)
|
||||
if err != nil {
|
||||
a.logger.Error("create session failed", slog.String("config_id", configID), slog.Any("error", err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session.Identify.Intents = discordgo.IntentsAll
|
||||
|
||||
a.sessions[token] = session
|
||||
return session, nil
|
||||
session.Identify.Intents = discordgo.IntentsAll
|
||||
|
||||
a.sessions[token] = session
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) Connect(ctx context.Context, cfg channel.ChannelConfig, handler channel.InboundHandler) (channel.Connection, error) {
|
||||
if a.logger != nil {
|
||||
a.logger.Info("start", slog.String("config_id", cfg.ID))
|
||||
}
|
||||
if a.logger != nil {
|
||||
a.logger.Info("start", slog.String("config_id", cfg.ID))
|
||||
}
|
||||
|
||||
discordCfg, err := parseConfig(cfg.Credentials)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
discordCfg, err := parseConfig(cfg.Credentials)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session, err := a.getOrCreateSession(discordCfg.BotToken, cfg.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
session, err := a.getOrCreateSession(discordCfg.BotToken, cfg.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||
if m.Author != nil && m.Author.Bot {
|
||||
return
|
||||
}
|
||||
remove := session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||
if m.Author != nil && m.Author.Bot {
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
text := strings.TrimSpace(m.Content)
|
||||
botId := s.State.User.ID
|
||||
if text == "" && len(m.Attachments) == 0 {
|
||||
return
|
||||
}
|
||||
if a.isDuplicateInbound(discordCfg.BotToken, m.ID) {
|
||||
return
|
||||
}
|
||||
|
||||
attachments := a.collectAttachments(m.Message)
|
||||
chatType := "direct"
|
||||
if m.GuildID != "" {
|
||||
chatType = "guild"
|
||||
}
|
||||
text := strings.TrimSpace(m.Content)
|
||||
botId := s.State.User.ID
|
||||
if text == "" && len(m.Attachments) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
isMentioned := a.isBotMentioned(m.Message, botId)
|
||||
isReplyToBot := m.ReferencedMessage != nil &&
|
||||
m.ReferencedMessage.Author != nil &&
|
||||
m.ReferencedMessage.Author.ID == botId
|
||||
attachments := a.collectAttachments(m.Message)
|
||||
chatType := "direct"
|
||||
if m.GuildID != "" {
|
||||
chatType = "guild"
|
||||
}
|
||||
|
||||
msg := channel.InboundMessage{
|
||||
Channel: Type,
|
||||
Message: channel.Message{
|
||||
ID: m.ID,
|
||||
Format: channel.MessageFormatPlain,
|
||||
Text: text,
|
||||
Attachments: attachments,
|
||||
},
|
||||
BotID: cfg.BotID,
|
||||
ReplyTarget: m.ChannelID,
|
||||
Sender: channel.Identity{
|
||||
SubjectID: m.Author.ID,
|
||||
DisplayName: m.Author.Username,
|
||||
Attributes: map[string]string{
|
||||
"user_id": m.Author.ID,
|
||||
"username": m.Author.Username,
|
||||
},
|
||||
},
|
||||
Conversation: channel.Conversation{
|
||||
ID: m.ChannelID,
|
||||
Type: chatType,
|
||||
},
|
||||
ReceivedAt: time.Now().UTC(),
|
||||
Source: "discord",
|
||||
Metadata: map[string]any{
|
||||
"guild_id": m.GuildID,
|
||||
"is_mentioned": isMentioned,
|
||||
"is_reply_to_bot": isReplyToBot,
|
||||
},
|
||||
}
|
||||
isMentioned := a.isBotMentioned(m.Message, botId)
|
||||
isReplyToBot := m.ReferencedMessage != nil &&
|
||||
m.ReferencedMessage.Author != nil &&
|
||||
m.ReferencedMessage.Author.ID == botId
|
||||
|
||||
if a.logger != nil {
|
||||
a.logger.Info("inbound received",
|
||||
slog.String("config_id", cfg.ID),
|
||||
slog.String("chat_type", chatType),
|
||||
slog.String("user_id", m.Author.ID),
|
||||
slog.String("username", m.Author.Username),
|
||||
slog.String("text", common.SummarizeText(text)),
|
||||
)
|
||||
}
|
||||
msg := channel.InboundMessage{
|
||||
Channel: Type,
|
||||
Message: channel.Message{
|
||||
ID: m.ID,
|
||||
Format: channel.MessageFormatPlain,
|
||||
Text: text,
|
||||
Attachments: attachments,
|
||||
},
|
||||
BotID: cfg.BotID,
|
||||
ReplyTarget: m.ChannelID,
|
||||
Sender: channel.Identity{
|
||||
SubjectID: m.Author.ID,
|
||||
DisplayName: m.Author.Username,
|
||||
Attributes: map[string]string{
|
||||
"user_id": m.Author.ID,
|
||||
"username": m.Author.Username,
|
||||
},
|
||||
},
|
||||
Conversation: channel.Conversation{
|
||||
ID: m.ChannelID,
|
||||
Type: chatType,
|
||||
},
|
||||
ReceivedAt: time.Now().UTC(),
|
||||
Source: "discord",
|
||||
Metadata: map[string]any{
|
||||
"guild_id": m.GuildID,
|
||||
"is_mentioned": isMentioned,
|
||||
"is_reply_to_bot": isReplyToBot,
|
||||
},
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := handler(ctx, cfg, msg); err != nil && a.logger != nil {
|
||||
a.logger.Error("handle inbound failed", slog.String("config_id", cfg.ID), slog.Any("error", err))
|
||||
}
|
||||
}()
|
||||
})
|
||||
if a.logger != nil {
|
||||
a.logger.Info("inbound received",
|
||||
slog.String("config_id", cfg.ID),
|
||||
slog.String("chat_type", chatType),
|
||||
slog.String("user_id", m.Author.ID),
|
||||
slog.String("username", m.Author.Username),
|
||||
slog.String("text", common.SummarizeText(text)),
|
||||
)
|
||||
}
|
||||
|
||||
if err := session.Open(); err != nil {
|
||||
return nil, fmt.Errorf("discord open connection: %w", err)
|
||||
}
|
||||
go func() {
|
||||
if err := handler(ctx, cfg, msg); err != nil && a.logger != nil {
|
||||
a.logger.Error("handle inbound failed", slog.String("config_id", cfg.ID), slog.Any("error", err))
|
||||
}
|
||||
}()
|
||||
})
|
||||
|
||||
stop := func(stopCtx context.Context) error {
|
||||
if a.logger != nil {
|
||||
a.logger.Info("stop", slog.String("config_id", cfg.ID))
|
||||
}
|
||||
return session.Close()
|
||||
}
|
||||
a.swapHandlerRemover(discordCfg.BotToken, remove)
|
||||
|
||||
return channel.NewConnection(cfg, stop), nil
|
||||
if err := session.Open(); err != nil {
|
||||
return nil, fmt.Errorf("discord open connection: %w", err)
|
||||
}
|
||||
|
||||
stop := func(stopCtx context.Context) error {
|
||||
if a.logger != nil {
|
||||
a.logger.Info("stop", slog.String("config_id", cfg.ID))
|
||||
}
|
||||
remove := a.clearSessionState(discordCfg.BotToken)
|
||||
if remove != nil {
|
||||
remove()
|
||||
}
|
||||
return session.Close()
|
||||
}
|
||||
|
||||
return channel.NewConnection(cfg, stop), nil
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) Send(ctx context.Context, cfg channel.ChannelConfig, msg channel.OutboundMessage) error {
|
||||
discordCfg, err := parseConfig(cfg.Credentials)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
discordCfg, err := parseConfig(cfg.Credentials)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session, err := a.getOrCreateSession(discordCfg.BotToken, cfg.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
session, err := a.getOrCreateSession(discordCfg.BotToken, cfg.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
channelID := strings.TrimSpace(msg.Target)
|
||||
if channelID == "" {
|
||||
return fmt.Errorf("discord target is required")
|
||||
}
|
||||
channelID := strings.TrimSpace(msg.Target)
|
||||
if channelID == "" {
|
||||
return fmt.Errorf("discord target is required")
|
||||
}
|
||||
|
||||
err = sendDiscordText(session, channelID, msg)
|
||||
return err
|
||||
err = sendDiscordText(session, channelID, msg)
|
||||
return err
|
||||
}
|
||||
|
||||
func sendDiscordText(session *discordgo.Session, channelID string, message channel.OutboundMessage) error {
|
||||
text_truncated := truncateDiscordText(message.Message.Text)
|
||||
var err error
|
||||
if message.Message.Reply != nil && message.Message.Reply.MessageID != "" {
|
||||
_, err = session.ChannelMessageSendReply(channelID, text_truncated, &discordgo.MessageReference{
|
||||
ChannelID: channelID,
|
||||
MessageID: message.Message.Reply.MessageID,
|
||||
})
|
||||
} else {
|
||||
_, err = session.ChannelMessageSend(channelID, text_truncated)
|
||||
}
|
||||
textTruncated := truncateDiscordText(message.Message.Text)
|
||||
var err error
|
||||
if message.Message.Reply != nil && message.Message.Reply.MessageID != "" {
|
||||
_, err = session.ChannelMessageSendReply(channelID, textTruncated, &discordgo.MessageReference{
|
||||
ChannelID: channelID,
|
||||
MessageID: message.Message.Reply.MessageID,
|
||||
})
|
||||
} else {
|
||||
_, err = session.ChannelMessageSend(channelID, textTruncated)
|
||||
}
|
||||
|
||||
return err
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
func truncateDiscordText(text string) string {
|
||||
const discordMaxLength = 2000
|
||||
if len(text) > discordMaxLength {
|
||||
text = text[:discordMaxLength-3] + "..."
|
||||
}
|
||||
return text
|
||||
const discordMaxLength = 2000
|
||||
if len(text) > discordMaxLength {
|
||||
text = text[:discordMaxLength-3] + "..."
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) OpenStream(ctx context.Context, cfg channel.ChannelConfig, target string, opts channel.StreamOptions) (channel.OutboundStream, error) {
|
||||
target = strings.TrimSpace(target)
|
||||
if target == "" {
|
||||
return nil, fmt.Errorf("discord target is required")
|
||||
}
|
||||
target = strings.TrimSpace(target)
|
||||
if target == "" {
|
||||
return nil, fmt.Errorf("discord target is required")
|
||||
}
|
||||
|
||||
discordCfg, err := parseConfig(cfg.Credentials)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
discordCfg, err := parseConfig(cfg.Credentials)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session, err := a.getOrCreateSession(discordCfg.BotToken, cfg.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
session, err := a.getOrCreateSession(discordCfg.BotToken, cfg.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &discordOutboundStream{
|
||||
adapter: a,
|
||||
cfg: cfg,
|
||||
target: target,
|
||||
reply: opts.Reply,
|
||||
session: session,
|
||||
}, nil
|
||||
return &discordOutboundStream{
|
||||
adapter: a,
|
||||
cfg: cfg,
|
||||
target: target,
|
||||
reply: opts.Reply,
|
||||
session: session,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) ProcessingStarted(ctx context.Context, cfg channel.ChannelConfig, msg channel.InboundMessage, info channel.ProcessingStatusInfo) (channel.ProcessingStatusHandle, error) {
|
||||
chatID := strings.TrimSpace(info.ReplyTarget)
|
||||
if chatID == "" {
|
||||
return channel.ProcessingStatusHandle{}, nil
|
||||
}
|
||||
chatID := strings.TrimSpace(info.ReplyTarget)
|
||||
if chatID == "" {
|
||||
return channel.ProcessingStatusHandle{}, nil
|
||||
}
|
||||
|
||||
discordCfg, err := parseConfig(cfg.Credentials)
|
||||
if err != nil {
|
||||
return channel.ProcessingStatusHandle{}, err
|
||||
}
|
||||
discordCfg, err := parseConfig(cfg.Credentials)
|
||||
if err != nil {
|
||||
return channel.ProcessingStatusHandle{}, err
|
||||
}
|
||||
|
||||
session, err := a.getOrCreateSession(discordCfg.BotToken, cfg.ID)
|
||||
if err != nil {
|
||||
return channel.ProcessingStatusHandle{}, err
|
||||
}
|
||||
session, err := a.getOrCreateSession(discordCfg.BotToken, cfg.ID)
|
||||
if err != nil {
|
||||
return channel.ProcessingStatusHandle{}, err
|
||||
}
|
||||
|
||||
// Discord typing indicator
|
||||
err = session.ChannelTyping(chatID)
|
||||
return channel.ProcessingStatusHandle{}, err
|
||||
// Discord typing indicator
|
||||
err = session.ChannelTyping(chatID)
|
||||
return channel.ProcessingStatusHandle{}, err
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) ProcessingCompleted(ctx context.Context, cfg channel.ChannelConfig, msg channel.InboundMessage, info channel.ProcessingStatusInfo, handle channel.ProcessingStatusHandle) error {
|
||||
return nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) ProcessingFailed(ctx context.Context, cfg channel.ChannelConfig, msg channel.InboundMessage, info channel.ProcessingStatusInfo, handle channel.ProcessingStatusHandle, cause error) error {
|
||||
return nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) React(ctx context.Context, cfg channel.ChannelConfig, target string, messageID string, emoji string) error {
|
||||
discordCfg, err := parseConfig(cfg.Credentials)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
discordCfg, err := parseConfig(cfg.Credentials)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session, err := a.getOrCreateSession(discordCfg.BotToken, cfg.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
session, err := a.getOrCreateSession(discordCfg.BotToken, cfg.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return session.MessageReactionAdd(target, messageID, emoji)
|
||||
return session.MessageReactionAdd(target, messageID, emoji)
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) Unreact(ctx context.Context, cfg channel.ChannelConfig, target string, messageID string, emoji string) error {
|
||||
discordCfg, err := parseConfig(cfg.Credentials)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
discordCfg, err := parseConfig(cfg.Credentials)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session, err := a.getOrCreateSession(discordCfg.BotToken, cfg.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
session, err := a.getOrCreateSession(discordCfg.BotToken, cfg.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return session.MessageReactionRemove(target, messageID, emoji, "@me")
|
||||
return session.MessageReactionRemove(target, messageID, emoji, "@me")
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) NormalizeConfig(raw map[string]any) (map[string]any, error) {
|
||||
return normalizeConfig(raw)
|
||||
return normalizeConfig(raw)
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) NormalizeUserConfig(raw map[string]any) (map[string]any, error) {
|
||||
return normalizeUserConfig(raw)
|
||||
return normalizeUserConfig(raw)
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) NormalizeTarget(raw string) string {
|
||||
return normalizeTarget(raw)
|
||||
return normalizeTarget(raw)
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) ResolveTarget(userConfig map[string]any) (string, error) {
|
||||
return resolveTarget(userConfig)
|
||||
return resolveTarget(userConfig)
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) MatchBinding(config map[string]any, criteria channel.BindingCriteria) bool {
|
||||
return matchBinding(config, criteria)
|
||||
return matchBinding(config, criteria)
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) BuildUserConfig(identity channel.Identity) map[string]any {
|
||||
return buildUserConfig(identity)
|
||||
return buildUserConfig(identity)
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) collectAttachments(msg *discordgo.Message) []channel.Attachment {
|
||||
if msg == nil || len(msg.Attachments) == 0 {
|
||||
return nil
|
||||
}
|
||||
if msg == nil || len(msg.Attachments) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
attachments := make([]channel.Attachment, 0, len(msg.Attachments))
|
||||
for _, att := range msg.Attachments {
|
||||
attachment := channel.Attachment{
|
||||
Type: channel.AttachmentFile,
|
||||
URL: att.URL,
|
||||
PlatformKey: att.ID,
|
||||
SourcePlatform: Type.String(),
|
||||
Name: att.Filename,
|
||||
Size: int64(att.Size),
|
||||
}
|
||||
attachments := make([]channel.Attachment, 0, len(msg.Attachments))
|
||||
for _, att := range msg.Attachments {
|
||||
attachment := channel.Attachment{
|
||||
Type: channel.AttachmentFile,
|
||||
URL: att.URL,
|
||||
PlatformKey: att.ID,
|
||||
SourcePlatform: Type.String(),
|
||||
Name: att.Filename,
|
||||
Size: int64(att.Size),
|
||||
}
|
||||
|
||||
if att.ContentType != "" {
|
||||
switch {
|
||||
case strings.HasPrefix(att.ContentType, "image/"):
|
||||
attachment.Type = channel.AttachmentImage
|
||||
attachment.Width = att.Width
|
||||
attachment.Height = att.Height
|
||||
case strings.HasPrefix(att.ContentType, "video/"):
|
||||
attachment.Type = channel.AttachmentVideo
|
||||
case strings.HasPrefix(att.ContentType, "audio/"):
|
||||
attachment.Type = channel.AttachmentAudio
|
||||
}
|
||||
}
|
||||
if att.ContentType != "" {
|
||||
switch {
|
||||
case strings.HasPrefix(att.ContentType, "image/"):
|
||||
attachment.Type = channel.AttachmentImage
|
||||
attachment.Width = att.Width
|
||||
attachment.Height = att.Height
|
||||
case strings.HasPrefix(att.ContentType, "video/"):
|
||||
attachment.Type = channel.AttachmentVideo
|
||||
case strings.HasPrefix(att.ContentType, "audio/"):
|
||||
attachment.Type = channel.AttachmentAudio
|
||||
}
|
||||
}
|
||||
|
||||
attachments = append(attachments, attachment)
|
||||
}
|
||||
attachments = append(attachments, attachment)
|
||||
}
|
||||
|
||||
return attachments
|
||||
return attachments
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) isBotMentioned(msg *discordgo.Message, botID string) bool {
|
||||
if msg == nil {
|
||||
return false
|
||||
}
|
||||
if msg == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, mention := range msg.Mentions {
|
||||
if mention != nil && mention.ID == botID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, mention := range msg.Mentions {
|
||||
if mention != nil && mention.ID == botID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if msg.MentionEveryone {
|
||||
return true
|
||||
}
|
||||
if msg.MentionEveryone {
|
||||
return true
|
||||
}
|
||||
|
||||
botMention := "<@" + botID + ">"
|
||||
botNickMention := "<@!" + botID + ">"
|
||||
content := strings.ToLower(msg.Content)
|
||||
return strings.Contains(content, strings.ToLower(botMention)) ||
|
||||
strings.Contains(content, strings.ToLower(botNickMention))
|
||||
}
|
||||
botMention := "<@" + botID + ">"
|
||||
botNickMention := "<@!" + botID + ">"
|
||||
content := strings.ToLower(msg.Content)
|
||||
return strings.Contains(content, strings.ToLower(botMention)) ||
|
||||
strings.Contains(content, strings.ToLower(botNickMention))
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) isDuplicateInbound(token, messageID string) bool {
|
||||
if strings.TrimSpace(token) == "" || strings.TrimSpace(messageID) == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
expireBefore := now.Add(-inboundDedupTTL)
|
||||
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
for key, seenAt := range a.seenMessages {
|
||||
if seenAt.Before(expireBefore) {
|
||||
delete(a.seenMessages, key)
|
||||
}
|
||||
}
|
||||
|
||||
seenKey := token + ":" + messageID
|
||||
if _, ok := a.seenMessages[seenKey]; ok {
|
||||
return true
|
||||
}
|
||||
a.seenMessages[seenKey] = now
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) swapHandlerRemover(token string, remove func()) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if oldRemove := a.handlerRemovers[token]; oldRemove != nil {
|
||||
oldRemove()
|
||||
}
|
||||
a.handlerRemovers[token] = remove
|
||||
}
|
||||
|
||||
func (a *DiscordAdapter) clearSessionState(token string) func() {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
remove := a.handlerRemovers[token]
|
||||
delete(a.handlerRemovers, token)
|
||||
delete(a.sessions, token)
|
||||
return remove
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func (s *discordOutboundStream) Push(ctx context.Context, event channel.StreamEv
|
||||
switch event.Type {
|
||||
case channel.StreamEventStatus:
|
||||
if event.Status == channel.StreamStatusStarted {
|
||||
return s.ensureMessage(ctx, "Thinking...")
|
||||
return s.ensureMessage("Thinking...")
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -56,7 +56,7 @@ func (s *discordOutboundStream) Push(ctx context.Context, event channel.StreamEv
|
||||
|
||||
// Discord has strict rate limits, only update periodically
|
||||
if time.Since(s.lastUpdate) > 2*time.Second {
|
||||
return s.updateMessage(ctx)
|
||||
return s.updateMessage()
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -64,14 +64,14 @@ func (s *discordOutboundStream) Push(ctx context.Context, event channel.StreamEv
|
||||
if event.Final != nil && !event.Final.Message.IsEmpty() {
|
||||
finalText := strings.TrimSpace(event.Final.Message.PlainText())
|
||||
if finalText != "" {
|
||||
return s.finalizeMessage(ctx, finalText)
|
||||
return s.finalizeMessage(finalText)
|
||||
}
|
||||
}
|
||||
s.mu.Lock()
|
||||
finalText := strings.TrimSpace(s.buffer.String())
|
||||
s.mu.Unlock()
|
||||
if finalText != "" {
|
||||
return s.finalizeMessage(ctx, finalText)
|
||||
return s.finalizeMessage(finalText)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -80,7 +80,7 @@ func (s *discordOutboundStream) Push(ctx context.Context, event channel.StreamEv
|
||||
if errText == "" {
|
||||
return nil
|
||||
}
|
||||
return s.finalizeMessage(ctx, "Error: "+errText)
|
||||
return s.finalizeMessage("Error: " + errText)
|
||||
|
||||
case channel.StreamEventAgentStart, channel.StreamEventAgentEnd, channel.StreamEventPhaseStart, channel.StreamEventPhaseEnd, channel.StreamEventProcessingStarted, channel.StreamEventProcessingCompleted, channel.StreamEventProcessingFailed, channel.StreamEventToolCallStart, channel.StreamEventToolCallEnd:
|
||||
// Status events - no action needed for Discord
|
||||
@@ -104,7 +104,7 @@ func (s *discordOutboundStream) Close(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *discordOutboundStream) ensureMessage(ctx context.Context, text string) error {
|
||||
func (s *discordOutboundStream) ensureMessage(text string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
@@ -112,11 +112,7 @@ func (s *discordOutboundStream) ensureMessage(ctx context.Context, text string)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Discord limit: 2000 characters
|
||||
content := text
|
||||
if len(content) > 2000 {
|
||||
content = content[:1997] + "..."
|
||||
}
|
||||
content := truncateDiscordText(text)
|
||||
|
||||
msg, err := s.session.ChannelMessageSend(s.target, content)
|
||||
if err != nil {
|
||||
@@ -128,7 +124,7 @@ func (s *discordOutboundStream) ensureMessage(ctx context.Context, text string)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *discordOutboundStream) updateMessage(ctx context.Context) error {
|
||||
func (s *discordOutboundStream) updateMessage() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
@@ -141,10 +137,7 @@ func (s *discordOutboundStream) updateMessage(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Discord limit
|
||||
if len(content) > 2000 {
|
||||
content = content[:1997] + "..."
|
||||
}
|
||||
content = truncateDiscordText(content)
|
||||
|
||||
_, err := s.session.ChannelMessageEdit(s.target, s.msgID, content)
|
||||
if err != nil {
|
||||
@@ -155,18 +148,20 @@ func (s *discordOutboundStream) updateMessage(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *discordOutboundStream) finalizeMessage(ctx context.Context, text string) error {
|
||||
func (s *discordOutboundStream) finalizeMessage(text string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Discord limit
|
||||
if len(text) > 2000 {
|
||||
text = text[:1997] + "..."
|
||||
}
|
||||
text = truncateDiscordText(text)
|
||||
|
||||
if s.msgID == "" {
|
||||
_, err := s.session.ChannelMessageSend(s.target, text)
|
||||
return err
|
||||
msg, err := s.session.ChannelMessageSend(s.target, text)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.msgID = msg.ID
|
||||
s.lastUpdate = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := s.session.ChannelMessageEdit(s.target, s.msgID, text)
|
||||
|
||||
Reference in New Issue
Block a user