This commit is contained in:
+11
-7
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/bwmarrin/discordgo"
|
||||
|
||||
"plumber/internal/events"
|
||||
"plumber/internal/mail"
|
||||
"plumber/internal/store"
|
||||
)
|
||||
|
||||
@@ -25,7 +24,7 @@ type Bot struct {
|
||||
links store.DiscordLinkStore
|
||||
api API
|
||||
store store.Store
|
||||
mail mail.Notifier
|
||||
bus events.Publisher
|
||||
admins map[string]string
|
||||
botUserID string
|
||||
}
|
||||
@@ -36,7 +35,7 @@ func New(channelID string, links store.DiscordLinkStore, api API) *Bot {
|
||||
}
|
||||
|
||||
// FromEnv builds a bot when Discord env is set. Missing config is a no-op.
|
||||
func FromEnv(links store.DiscordLinkStore, bus *events.Bus, st store.Store, mailer mail.Notifier) (*Bot, error) {
|
||||
func FromEnv(links store.DiscordLinkStore, bus *events.Bus, st store.Store) (*Bot, error) {
|
||||
token := strings.TrimSpace(os.Getenv("DISCORD_BOT_TOKEN"))
|
||||
channelID := strings.TrimSpace(os.Getenv("DISCORD_CHANNEL_ID"))
|
||||
if token == "" && channelID == "" {
|
||||
@@ -56,12 +55,9 @@ func FromEnv(links store.DiscordLinkStore, bus *events.Bus, st store.Store, mail
|
||||
return nil, err
|
||||
}
|
||||
session.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildMessages | discordgo.IntentsMessageContent
|
||||
if mailer == nil {
|
||||
mailer = mail.Nop{}
|
||||
}
|
||||
bot := New(channelID, links, &sessionAPI{session: session})
|
||||
bot.store = st
|
||||
bot.mail = mailer
|
||||
bot.bus = bus
|
||||
bot.admins = parseAdminMap(os.Getenv("DISCORD_ADMIN_MAP"))
|
||||
session.AddHandler(bot.onMessageCreate)
|
||||
if bus != nil {
|
||||
@@ -102,6 +98,14 @@ func (b *Bot) Handle(_ context.Context, ev any) {
|
||||
}
|
||||
|
||||
func (b *Bot) onCreated(ctx context.Context, ev events.PostEvent) {
|
||||
_, err := b.links.GetByPostID(ctx, ev.PostID)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
log.Printf("discord: load link %s: %v", ev.PostID, err)
|
||||
return
|
||||
}
|
||||
if isRoot(ev) {
|
||||
b.createRoot(ctx, ev)
|
||||
return
|
||||
|
||||
@@ -161,6 +161,36 @@ func TestOutboundSkipsReplyWithoutRootLink(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutboundSkipsAlreadyLinkedPost(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
links := newMemoryLinks()
|
||||
api := &fakeAPI{}
|
||||
bot := New("channel-1", links, api)
|
||||
if err := links.Upsert(context.Background(), store.DiscordLink{
|
||||
PostID: "reply-1",
|
||||
MessageID: "d-reply-1",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := links.Upsert(context.Background(), store.DiscordLink{
|
||||
PostID: "root-1",
|
||||
MessageID: "d-root",
|
||||
ThreadID: "thread-1",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bot.Handle(context.Background(), events.PostCreated{PostEvent: events.PostEvent{
|
||||
PostID: "reply-1",
|
||||
RootID: "root-1",
|
||||
ParentID: "root-1",
|
||||
Body: "Already on Discord.",
|
||||
}})
|
||||
if len(api.sends) != 0 {
|
||||
t.Fatalf("echoed already-linked reply: %+v", api.sends)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutboundUpdateWithoutLinkCreates(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -218,7 +248,7 @@ func TestFormatMessage(t *testing.T) {
|
||||
func TestFromEnvDisabled(t *testing.T) {
|
||||
t.Setenv("DISCORD_BOT_TOKEN", "")
|
||||
t.Setenv("DISCORD_CHANNEL_ID", "")
|
||||
bot, err := FromEnv(newMemoryLinks(), nil, nil, nil)
|
||||
bot, err := FromEnv(newMemoryLinks(), nil, nil)
|
||||
if err != nil || bot != nil {
|
||||
t.Fatalf("disabled FromEnv = (%v, %v)", bot, err)
|
||||
}
|
||||
@@ -227,12 +257,12 @@ func TestFromEnvDisabled(t *testing.T) {
|
||||
func TestFromEnvRequiresBoth(t *testing.T) {
|
||||
t.Setenv("DISCORD_BOT_TOKEN", "token")
|
||||
t.Setenv("DISCORD_CHANNEL_ID", "")
|
||||
if _, err := FromEnv(newMemoryLinks(), nil, nil, nil); err == nil {
|
||||
if _, err := FromEnv(newMemoryLinks(), nil, nil); err == nil {
|
||||
t.Fatal("expected error when channel is missing")
|
||||
}
|
||||
t.Setenv("DISCORD_BOT_TOKEN", "")
|
||||
t.Setenv("DISCORD_CHANNEL_ID", "channel")
|
||||
if _, err := FromEnv(newMemoryLinks(), nil, nil, nil); err == nil {
|
||||
if _, err := FromEnv(newMemoryLinks(), nil, nil); err == nil {
|
||||
t.Fatal("expected error when token is missing")
|
||||
}
|
||||
}
|
||||
|
||||
+15
-39
@@ -7,11 +7,10 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
|
||||
"plumber/internal/mail"
|
||||
"plumber/internal/events"
|
||||
"plumber/internal/store"
|
||||
)
|
||||
|
||||
@@ -127,8 +126,9 @@ func (b *Bot) handleInbound(in inboundMessage) {
|
||||
MessageID: in.ID,
|
||||
}); err != nil {
|
||||
log.Printf("discord: save inbound link %s: %v", reply.ID, err)
|
||||
return
|
||||
}
|
||||
b.notifyInboundReply(parent, root, reply, author)
|
||||
b.publishInbound(reply, root, author)
|
||||
log.Printf("discord: inbound reply %s -> post %s", in.ID, reply.ID)
|
||||
}
|
||||
|
||||
@@ -184,44 +184,20 @@ func (b *Bot) postAndRoot(ctx context.Context, postID string) (*store.Post, *sto
|
||||
return post, current, nil
|
||||
}
|
||||
|
||||
func (b *Bot) notifyInboundReply(parent, root, reply *store.Post, author *store.User) {
|
||||
if parent == nil || root == nil || reply == nil || author == nil || b.mail == nil {
|
||||
func (b *Bot) publishInbound(reply, root *store.Post, author *store.User) {
|
||||
if b == nil || b.bus == nil || reply == nil || root == nil || author == nil {
|
||||
return
|
||||
}
|
||||
if _, disabled := b.mail.(mail.Nop); disabled {
|
||||
return
|
||||
ev := events.PostEvent{
|
||||
PostID: reply.ID,
|
||||
RootID: root.ID,
|
||||
Body: reply.Body,
|
||||
AuthorID: author.ID,
|
||||
AuthorName: author.Name,
|
||||
AuthorRole: string(author.Role),
|
||||
}
|
||||
recipientID := parent.AuthorID
|
||||
if author.Admin() {
|
||||
recipientID = root.AuthorID
|
||||
if reply.ParentID != nil {
|
||||
ev.ParentID = *reply.ParentID
|
||||
}
|
||||
if recipientID == author.ID {
|
||||
return
|
||||
}
|
||||
msg := mail.PostReply{
|
||||
RootID: root.ID,
|
||||
RootTitle: root.Title,
|
||||
ReplyID: reply.ID,
|
||||
ReplyBody: reply.Body,
|
||||
ReplyAuthorName: author.Name,
|
||||
}
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
recipient, err := b.store.UserByID(ctx, recipientID)
|
||||
if err != nil {
|
||||
log.Printf("notify reply %s: load recipient: %v", msg.ReplyID, err)
|
||||
return
|
||||
}
|
||||
if recipient == nil || strings.TrimSpace(recipient.Email) == "" {
|
||||
return
|
||||
}
|
||||
msg.ToEmail = recipient.Email
|
||||
msg.ToName = recipient.Name
|
||||
if err := b.mail.NotifyPostReply(ctx, msg); err != nil {
|
||||
log.Printf("notify reply %s: %v", msg.ReplyID, err)
|
||||
return
|
||||
}
|
||||
log.Printf("notify reply %s: accepted", msg.ReplyID)
|
||||
}()
|
||||
b.bus.Publish(context.Background(), events.PostCreated{PostEvent: ev})
|
||||
}
|
||||
|
||||
@@ -27,9 +27,11 @@ func TestInboundCreatesSiteReply(t *testing.T) {
|
||||
links := newMemoryLinks()
|
||||
api := &fakeAPI{}
|
||||
mailer := &mail.Recording{}
|
||||
bot := inboundTestBot(mem, links, api, mailer, admin.Username)
|
||||
bot := inboundTestBot(mem, links, api, admin.Username)
|
||||
bus := events.New()
|
||||
defer bus.Close()
|
||||
bot.bus = bus
|
||||
mail.Subscribe(bus, mem, mailer)
|
||||
bus.Subscribe(bot.Handle)
|
||||
root := seedLinkedRoot(t, mem, links, homeowner.ID, "thread-1")
|
||||
|
||||
@@ -68,7 +70,7 @@ func TestInboundParentsFromReference(t *testing.T) {
|
||||
|
||||
mem, homeowner, admin := seedInboundUsers(t)
|
||||
links := newMemoryLinks()
|
||||
bot := inboundTestBot(mem, links, &fakeAPI{}, &mail.Recording{}, admin.Username)
|
||||
bot := inboundTestBot(mem, links, &fakeAPI{}, admin.Username)
|
||||
root := seedLinkedRoot(t, mem, links, homeowner.ID, "thread-1")
|
||||
plumberReply := &store.Post{ParentID: &root.ID, AuthorID: admin.ID, Body: "First look."}
|
||||
if err := mem.CreatePost(context.Background(), plumberReply); err != nil {
|
||||
@@ -106,7 +108,7 @@ func TestInboundIgnoresAllowlistHiddenAndEchoSources(t *testing.T) {
|
||||
mem, homeowner, admin := seedInboundUsers(t)
|
||||
links := newMemoryLinks()
|
||||
api := &fakeAPI{}
|
||||
bot := inboundTestBot(mem, links, api, mail.Nop{}, admin.Username)
|
||||
bot := inboundTestBot(mem, links, api, admin.Username)
|
||||
root := seedLinkedRoot(t, mem, links, homeowner.ID, "thread-1")
|
||||
hidden := &store.Post{
|
||||
AuthorID: homeowner.ID,
|
||||
@@ -148,10 +150,9 @@ func TestInboundIgnoresAllowlistHiddenAndEchoSources(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func inboundTestBot(mem *store.Memory, links *memoryLinks, api *fakeAPI, mailer mail.Notifier, adminUsername string) *Bot {
|
||||
func inboundTestBot(mem *store.Memory, links *memoryLinks, api *fakeAPI, adminUsername string) *Bot {
|
||||
bot := New("channel-1", links, api)
|
||||
bot.store = mem
|
||||
bot.mail = mailer
|
||||
bot.admins = map[string]string{"snow-admin": adminUsername}
|
||||
bot.botUserID = "bot-1"
|
||||
return bot
|
||||
|
||||
Reference in New Issue
Block a user