Compare commits

..
Author SHA1 Message Date
codegirl007 a6232ea784 Send reply mail from the post event bus (#16)
Stacks on #15. Handlers only persist and publish; a mail subscriber sends reply email. Unset Resend stays a no-op.

Reviewed-on: #16
Co-authored-by: codegirl-007 <s.raide@gmail.com>
2026-08-29 18:25:30 +00:00
7 changed files with 84 additions and 149 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ func main() {
} }
bus := events.New() bus := events.New()
mail.Subscribe(bus, store.NewPostgres(db), notifier) mail.Subscribe(bus, store.NewPostgres(db), notifier)
bot, err := discord.FromEnv(store.NewDiscordLinks(db), bus, store.NewPostgres(db)) bot, err := discord.FromEnv(store.NewDiscordLinks(db), bus, store.NewPostgres(db), notifier)
if err != nil { if err != nil {
log.Fatalf("discord: %v", err) log.Fatalf("discord: %v", err)
} }
+4 -8
View File
@@ -9,7 +9,7 @@ import (
// API is the Discord REST surface used by the outbound subscriber. // API is the Discord REST surface used by the outbound subscriber.
type API interface { type API interface {
SendToChannel(ctx context.Context, channelID string, msg Message) (messageID string, err error) SendToChannel(ctx context.Context, channelID string, msg Message) (messageID string, err error)
StartThread(ctx context.Context, channelID, name string) (threadID string, err error) StartThread(ctx context.Context, channelID, messageID, name string) (threadID string, err error)
SendToThread(ctx context.Context, threadID string, msg Message) (messageID string, err error) SendToThread(ctx context.Context, threadID string, msg Message) (messageID string, err error)
Edit(ctx context.Context, channelID, messageID string, msg Message) error Edit(ctx context.Context, channelID, messageID string, msg Message) error
Close() error Close() error
@@ -27,10 +27,9 @@ func (s *sessionAPI) SendToChannel(_ context.Context, channelID string, msg Mess
return sent.ID, nil return sent.ID, nil
} }
func (s *sessionAPI) StartThread(_ context.Context, channelID, name string) (string, error) { func (s *sessionAPI) StartThread(_ context.Context, channelID, messageID, name string) (string, error) {
thread, err := s.session.ThreadStartComplex(channelID, &discordgo.ThreadStart{ thread, err := s.session.MessageThreadStartComplex(channelID, messageID, &discordgo.ThreadStart{
Name: name, Name: name,
Type: discordgo.ChannelTypeGuildPublicThread,
AutoArchiveDuration: 10080, AutoArchiveDuration: 10080,
}) })
if err != nil { if err != nil {
@@ -44,12 +43,10 @@ func (s *sessionAPI) SendToThread(ctx context.Context, threadID string, msg Mess
} }
func (s *sessionAPI) Edit(_ context.Context, channelID, messageID string, msg Message) error { func (s *sessionAPI) Edit(_ context.Context, channelID, messageID string, msg Message) error {
content := messageContent(msg)
embeds := toEmbeds(msg) embeds := toEmbeds(msg)
_, err := s.session.ChannelMessageEditComplex(&discordgo.MessageEdit{ _, err := s.session.ChannelMessageEditComplex(&discordgo.MessageEdit{
ID: messageID, ID: messageID,
Channel: channelID, Channel: channelID,
Content: &content,
Embeds: &embeds, Embeds: &embeds,
}) })
return err return err
@@ -64,7 +61,6 @@ func (s *sessionAPI) Close() error {
func toMessageSend(msg Message) *discordgo.MessageSend { func toMessageSend(msg Message) *discordgo.MessageSend {
return &discordgo.MessageSend{ return &discordgo.MessageSend{
Content: messageContent(msg),
Embeds: toEmbeds(msg), Embeds: toEmbeds(msg),
AllowedMentions: &discordgo.MessageAllowedMentions{}, AllowedMentions: &discordgo.MessageAllowedMentions{},
} }
@@ -73,7 +69,7 @@ func toMessageSend(msg Message) *discordgo.MessageSend {
func toEmbeds(msg Message) []*discordgo.MessageEmbed { func toEmbeds(msg Message) []*discordgo.MessageEmbed {
main := &discordgo.MessageEmbed{ main := &discordgo.MessageEmbed{
Title: msg.Title, Title: msg.Title,
URL: publicURL(msg.URL), URL: msg.URL,
Description: msg.Description, Description: msg.Description,
Color: embedColor, Color: embedColor,
} }
+14 -18
View File
@@ -13,6 +13,7 @@ import (
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"plumber/internal/events" "plumber/internal/events"
"plumber/internal/mail"
"plumber/internal/store" "plumber/internal/store"
) )
@@ -24,7 +25,7 @@ type Bot struct {
links store.DiscordLinkStore links store.DiscordLinkStore
api API api API
store store.Store store store.Store
bus events.Publisher mail mail.Notifier
admins map[string]string admins map[string]string
botUserID string botUserID string
} }
@@ -35,7 +36,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. // 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) (*Bot, error) { func FromEnv(links store.DiscordLinkStore, bus *events.Bus, st store.Store, mailer mail.Notifier) (*Bot, error) {
token := strings.TrimSpace(os.Getenv("DISCORD_BOT_TOKEN")) token := strings.TrimSpace(os.Getenv("DISCORD_BOT_TOKEN"))
channelID := strings.TrimSpace(os.Getenv("DISCORD_CHANNEL_ID")) channelID := strings.TrimSpace(os.Getenv("DISCORD_CHANNEL_ID"))
if token == "" && channelID == "" { if token == "" && channelID == "" {
@@ -55,9 +56,12 @@ func FromEnv(links store.DiscordLinkStore, bus *events.Bus, st store.Store) (*Bo
return nil, err return nil, err
} }
session.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildMessages | discordgo.IntentsMessageContent session.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildMessages | discordgo.IntentsMessageContent
if mailer == nil {
mailer = mail.Nop{}
}
bot := New(channelID, links, &sessionAPI{session: session}) bot := New(channelID, links, &sessionAPI{session: session})
bot.store = st bot.store = st
bot.bus = bus bot.mail = mailer
bot.admins = parseAdminMap(os.Getenv("DISCORD_ADMIN_MAP")) bot.admins = parseAdminMap(os.Getenv("DISCORD_ADMIN_MAP"))
session.AddHandler(bot.onMessageCreate) session.AddHandler(bot.onMessageCreate)
if bus != nil { if bus != nil {
@@ -98,14 +102,6 @@ func (b *Bot) Handle(_ context.Context, ev any) {
} }
func (b *Bot) onCreated(ctx context.Context, ev events.PostEvent) { 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) { if isRoot(ev) {
b.createRoot(ctx, ev) b.createRoot(ctx, ev)
return return
@@ -137,16 +133,16 @@ func (b *Bot) onUpdated(ctx context.Context, ev events.PostEvent) {
func (b *Bot) createRoot(ctx context.Context, ev events.PostEvent) { func (b *Bot) createRoot(ctx context.Context, ev events.PostEvent) {
msg := formatMessage(ev) msg := formatMessage(ev)
threadID, err := b.api.StartThread(ctx, b.channelID, msg.ThreadName) messageID, err := b.api.SendToChannel(ctx, b.channelID, msg)
if err != nil {
log.Printf("discord: start thread %s: %v", ev.PostID, err)
return
}
messageID, err := b.api.SendToThread(ctx, threadID, msg)
if err != nil { if err != nil {
log.Printf("discord: send root %s: %v", ev.PostID, err) log.Printf("discord: send root %s: %v", ev.PostID, err)
return return
} }
threadID, err := b.api.StartThread(ctx, b.channelID, messageID, msg.ThreadName)
if err != nil {
log.Printf("discord: start thread %s: %v", ev.PostID, err)
return
}
if err := b.links.Upsert(ctx, store.DiscordLink{ if err := b.links.Upsert(ctx, store.DiscordLink{
PostID: ev.PostID, PostID: ev.PostID,
MessageID: messageID, MessageID: messageID,
@@ -189,7 +185,7 @@ func (b *Bot) createReply(ctx context.Context, ev events.PostEvent) {
func (b *Bot) editChannel(ctx context.Context, ev events.PostEvent, link *store.DiscordLink) (string, error) { func (b *Bot) editChannel(ctx context.Context, ev events.PostEvent, link *store.DiscordLink) (string, error) {
if strings.TrimSpace(link.ThreadID) != "" { if strings.TrimSpace(link.ThreadID) != "" {
return link.ThreadID, nil return b.channelID, nil
} }
root, err := b.links.GetByPostID(ctx, ev.RootID) root, err := b.links.GetByPostID(ctx, ev.RootID)
if err != nil { if err != nil {
+17 -61
View File
@@ -2,11 +2,11 @@ package discord
import ( import (
"context" "context"
"strconv"
"strings"
"sync" "sync"
"testing" "testing"
"strconv"
"plumber/internal/events" "plumber/internal/events"
"plumber/internal/store" "plumber/internal/store"
) )
@@ -30,18 +30,17 @@ func (f *fakeAPI) SendToChannel(_ context.Context, channelID string, msg Message
return f.record("channel", channelID, "", msg) return f.record("channel", channelID, "", msg)
} }
func (f *fakeAPI) StartThread(_ context.Context, channelID, name string) (string, error) { func (f *fakeAPI) StartThread(_ context.Context, channelID, messageID, name string) (string, error) {
f.mu.Lock() f.mu.Lock()
defer f.mu.Unlock() defer f.mu.Unlock()
f.next++ f.next++
id := "thread-" + strconv.Itoa(f.next)
f.sends = append(f.sends, recordedSend{ f.sends = append(f.sends, recordedSend{
Kind: "thread", Kind: "thread",
ChannelID: channelID, ChannelID: channelID,
Name: name, Name: name,
Msg: Message{ThreadName: name}, Msg: Message{ThreadName: name, URL: messageID},
}) })
return id, nil return "thread-" + messageID, nil
} }
func (f *fakeAPI) SendToThread(_ context.Context, threadID string, msg Message) (string, error) { func (f *fakeAPI) SendToThread(_ context.Context, threadID string, msg Message) (string, error) {
@@ -94,21 +93,17 @@ func TestOutboundRootReplyAndEdit(t *testing.T) {
} }
bot.Handle(ctx, events.PostCreated{PostEvent: root}) bot.Handle(ctx, events.PostCreated{PostEvent: root})
if len(api.sends) != 2 || if len(api.sends) != 2 || api.sends[0].Kind != "channel" || api.sends[1].Kind != "thread" {
api.sends[0].Kind != "thread" ||
api.sends[1].Kind != "thread-msg" {
t.Fatalf("root sends = %+v", api.sends) t.Fatalf("root sends = %+v", api.sends)
} }
if api.sends[0].ChannelID != "channel-1" || if api.sends[0].ChannelID != "channel-1" || api.sends[1].Name != "Leaky sink" {
api.sends[0].Name != "sam asks: Leaky sink" ||
api.sends[1].ChannelID != "thread-1" {
t.Fatalf("root routing = %+v", api.sends) t.Fatalf("root routing = %+v", api.sends)
} }
if got := api.sends[1].Msg.ImageURLs; len(got) != 2 || got[0] != "https://cdn.example/a.jpg" { if got := api.sends[0].Msg.ImageURLs; len(got) != 2 || got[0] != "https://cdn.example/a.jpg" {
t.Fatalf("root images = %v", got) t.Fatalf("root images = %v", got)
} }
link, err := links.GetByPostID(ctx, "root-1") link, err := links.GetByPostID(ctx, "root-1")
if err != nil || link.MessageID != "msg-2" || link.ThreadID != "thread-1" { if err != nil || link.MessageID != "msg-1" || link.ThreadID != "thread-msg-1" {
t.Fatalf("root link = %+v, %v", link, err) t.Fatalf("root link = %+v, %v", link, err)
} }
@@ -121,7 +116,7 @@ func TestOutboundRootReplyAndEdit(t *testing.T) {
Permalink: "https://www.askaplumberfirst.com/questions/root-1#post-reply-1", Permalink: "https://www.askaplumberfirst.com/questions/root-1#post-reply-1",
} }
bot.Handle(ctx, events.PostCreated{PostEvent: reply}) bot.Handle(ctx, events.PostCreated{PostEvent: reply})
if len(api.sends) != 3 || api.sends[2].Kind != "thread-msg" || api.sends[2].ChannelID != "thread-1" { if len(api.sends) != 3 || api.sends[2].Kind != "thread-msg" || api.sends[2].ChannelID != "thread-msg-1" {
t.Fatalf("reply sends = %+v", api.sends) t.Fatalf("reply sends = %+v", api.sends)
} }
replyLink, err := links.GetByPostID(ctx, "reply-1") replyLink, err := links.GetByPostID(ctx, "reply-1")
@@ -131,7 +126,7 @@ func TestOutboundRootReplyAndEdit(t *testing.T) {
root.Body = "Updated leak." root.Body = "Updated leak."
bot.Handle(ctx, events.PostUpdated{PostEvent: root}) bot.Handle(ctx, events.PostUpdated{PostEvent: root})
if len(api.edits) != 1 || api.edits[0].ChannelID != "thread-1" || api.edits[0].Name != "msg-2" { if len(api.edits) != 1 || api.edits[0].ChannelID != "channel-1" || api.edits[0].Name != "msg-1" {
t.Fatalf("root edit = %+v", api.edits) t.Fatalf("root edit = %+v", api.edits)
} }
if api.edits[0].Msg.Description != "Updated leak." { if api.edits[0].Msg.Description != "Updated leak." {
@@ -140,7 +135,7 @@ func TestOutboundRootReplyAndEdit(t *testing.T) {
reply.Body = "Use a ceramic cartridge." reply.Body = "Use a ceramic cartridge."
bot.Handle(ctx, events.PostUpdated{PostEvent: reply}) bot.Handle(ctx, events.PostUpdated{PostEvent: reply})
if len(api.edits) != 2 || api.edits[1].ChannelID != "thread-1" || api.edits[1].Name != "msg-3" { if len(api.edits) != 2 || api.edits[1].ChannelID != "thread-msg-1" || api.edits[1].Name != "msg-3" {
t.Fatalf("reply edit = %+v", api.edits) t.Fatalf("reply edit = %+v", api.edits)
} }
} }
@@ -161,36 +156,6 @@ 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) { func TestOutboundUpdateWithoutLinkCreates(t *testing.T) {
t.Parallel() t.Parallel()
@@ -225,30 +190,21 @@ func TestFormatMessage(t *testing.T) {
got.City != "Oakland" || got.City != "Oakland" ||
got.Author != "sam" || got.Author != "sam" ||
got.URL != "https://example.com/q" || got.URL != "https://example.com/q" ||
got.ThreadName != "sam asks: Leaky sink" || got.ThreadName != "Leaky sink" ||
len(got.ImageURLs) != 1 { len(got.ImageURLs) != 1 {
t.Fatalf("format = %+v", got) t.Fatalf("format = %+v", got)
} }
reply := formatMessage(events.PostEvent{Body: "Thanks", AuthorName: ""}) reply := formatMessage(events.PostEvent{Body: "Thanks", AuthorName: ""})
if reply.Title != "Reply" || reply.Author != "Someone" || reply.ThreadName != "Someone asks: Question" { if reply.Title != "Reply" || reply.Author != "Someone" || reply.ThreadName != "Question" {
t.Fatalf("reply format = %+v", reply) t.Fatalf("reply format = %+v", reply)
} }
content := messageContent(got)
if strings.Contains(content, "Leaky sink") ||
!strings.Contains(content, "It drips.") ||
!strings.Contains(content, "Oakland") {
t.Fatalf("content = %q", content)
}
if publicURL("/questions/x") != "" || publicURL("http://localhost:8080/q") != "" {
t.Fatal("localhost or relative permalink should not be an embed URL")
}
} }
func TestFromEnvDisabled(t *testing.T) { func TestFromEnvDisabled(t *testing.T) {
t.Setenv("DISCORD_BOT_TOKEN", "") t.Setenv("DISCORD_BOT_TOKEN", "")
t.Setenv("DISCORD_CHANNEL_ID", "") t.Setenv("DISCORD_CHANNEL_ID", "")
bot, err := FromEnv(newMemoryLinks(), nil, nil) bot, err := FromEnv(newMemoryLinks(), nil, nil, nil)
if err != nil || bot != nil { if err != nil || bot != nil {
t.Fatalf("disabled FromEnv = (%v, %v)", bot, err) t.Fatalf("disabled FromEnv = (%v, %v)", bot, err)
} }
@@ -257,12 +213,12 @@ func TestFromEnvDisabled(t *testing.T) {
func TestFromEnvRequiresBoth(t *testing.T) { func TestFromEnvRequiresBoth(t *testing.T) {
t.Setenv("DISCORD_BOT_TOKEN", "token") t.Setenv("DISCORD_BOT_TOKEN", "token")
t.Setenv("DISCORD_CHANNEL_ID", "") t.Setenv("DISCORD_CHANNEL_ID", "")
if _, err := FromEnv(newMemoryLinks(), nil, nil); err == nil { if _, err := FromEnv(newMemoryLinks(), nil, nil, nil); err == nil {
t.Fatal("expected error when channel is missing") t.Fatal("expected error when channel is missing")
} }
t.Setenv("DISCORD_BOT_TOKEN", "") t.Setenv("DISCORD_BOT_TOKEN", "")
t.Setenv("DISCORD_CHANNEL_ID", "channel") t.Setenv("DISCORD_CHANNEL_ID", "channel")
if _, err := FromEnv(newMemoryLinks(), nil, nil); err == nil { if _, err := FromEnv(newMemoryLinks(), nil, nil, nil); err == nil {
t.Fatal("expected error when token is missing") t.Fatal("expected error when token is missing")
} }
} }
+4 -40
View File
@@ -39,7 +39,7 @@ func formatMessage(ev events.PostEvent) Message {
Description: truncateRunes(strings.TrimSpace(ev.Body), embedDescriptionLimit), Description: truncateRunes(strings.TrimSpace(ev.Body), embedDescriptionLimit),
City: strings.TrimSpace(ev.City), City: strings.TrimSpace(ev.City),
Author: author, Author: author,
ThreadName: threadName(author, ev.Title), ThreadName: threadName(ev.Title),
} }
for _, img := range ev.Images { for _, img := range ev.Images {
url := strings.TrimSpace(img.URL) url := strings.TrimSpace(img.URL)
@@ -51,16 +51,12 @@ func formatMessage(ev events.PostEvent) Message {
return msg return msg
} }
func threadName(author, title string) string { func threadName(title string) string {
author = strings.TrimSpace(author)
if author == "" {
author = "Someone"
}
title = strings.TrimSpace(title) title = strings.TrimSpace(title)
if title == "" { if title == "" {
title = "Question" return "Question"
} }
return truncateRunes(author+" asks: "+title, threadNameLimit) return truncateRunes(title, threadNameLimit)
} }
func truncateRunes(s string, max int) string { func truncateRunes(s string, max int) string {
@@ -77,35 +73,3 @@ func truncateRunes(s string, max int) string {
func isRoot(ev events.PostEvent) bool { func isRoot(ev events.PostEvent) bool {
return strings.TrimSpace(ev.ParentID) == "" return strings.TrimSpace(ev.ParentID) == ""
} }
func messageContent(msg Message) string {
var parts []string
if body := strings.TrimSpace(msg.Description); body != "" {
parts = append(parts, body)
}
var meta []string
if msg.City != "" {
meta = append(meta, msg.City)
}
if msg.Author != "" {
meta = append(meta, msg.Author)
}
if len(meta) > 0 {
parts = append(parts, strings.Join(meta, " · "))
}
if u := publicURL(msg.URL); u != "" {
parts = append(parts, u)
}
return truncateRunes(strings.Join(parts, "\n"), 2000)
}
func publicURL(raw string) string {
raw = strings.TrimSpace(raw)
if !strings.HasPrefix(raw, "https://") {
return ""
}
if strings.Contains(raw, "localhost") || strings.Contains(raw, "127.0.0.1") {
return ""
}
return raw
}
+38 -14
View File
@@ -7,10 +7,11 @@ import (
"fmt" "fmt"
"log" "log"
"strings" "strings"
"time"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"plumber/internal/events" "plumber/internal/mail"
"plumber/internal/store" "plumber/internal/store"
) )
@@ -126,9 +127,8 @@ func (b *Bot) handleInbound(in inboundMessage) {
MessageID: in.ID, MessageID: in.ID,
}); err != nil { }); err != nil {
log.Printf("discord: save inbound link %s: %v", reply.ID, err) log.Printf("discord: save inbound link %s: %v", reply.ID, err)
return
} }
b.publishInbound(reply, root, author) b.notifyInboundReply(parent, root, reply, author)
log.Printf("discord: inbound reply %s -> post %s", in.ID, reply.ID) log.Printf("discord: inbound reply %s -> post %s", in.ID, reply.ID)
} }
@@ -184,20 +184,44 @@ func (b *Bot) postAndRoot(ctx context.Context, postID string) (*store.Post, *sto
return post, current, nil return post, current, nil
} }
func (b *Bot) publishInbound(reply, root *store.Post, author *store.User) { func (b *Bot) notifyInboundReply(parent, root, reply *store.Post, author *store.User) {
if b == nil || b.bus == nil || reply == nil || root == nil || author == nil { if parent == nil || root == nil || reply == nil || author == nil || b.mail == nil {
return return
} }
ev := events.PostEvent{ if _, disabled := b.mail.(mail.Nop); disabled {
PostID: reply.ID, return
}
recipientID := parent.AuthorID
if author.Admin() {
recipientID = root.AuthorID
}
if recipientID == author.ID {
return
}
msg := mail.PostReply{
RootID: root.ID, RootID: root.ID,
Body: reply.Body, RootTitle: root.Title,
AuthorID: author.ID, ReplyID: reply.ID,
AuthorName: author.Name, ReplyBody: reply.Body,
AuthorRole: string(author.Role), ReplyAuthorName: author.Name,
} }
if reply.ParentID != nil { go func() {
ev.ParentID = *reply.ParentID 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
} }
b.bus.Publish(context.Background(), events.PostCreated{PostEvent: ev}) 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)
}()
} }
+5 -6
View File
@@ -27,11 +27,9 @@ func TestInboundCreatesSiteReply(t *testing.T) {
links := newMemoryLinks() links := newMemoryLinks()
api := &fakeAPI{} api := &fakeAPI{}
mailer := &mail.Recording{} mailer := &mail.Recording{}
bot := inboundTestBot(mem, links, api, admin.Username) bot := inboundTestBot(mem, links, api, mailer, admin.Username)
bus := events.New() bus := events.New()
defer bus.Close() defer bus.Close()
bot.bus = bus
mail.Subscribe(bus, mem, mailer)
bus.Subscribe(bot.Handle) bus.Subscribe(bot.Handle)
root := seedLinkedRoot(t, mem, links, homeowner.ID, "thread-1") root := seedLinkedRoot(t, mem, links, homeowner.ID, "thread-1")
@@ -70,7 +68,7 @@ func TestInboundParentsFromReference(t *testing.T) {
mem, homeowner, admin := seedInboundUsers(t) mem, homeowner, admin := seedInboundUsers(t)
links := newMemoryLinks() links := newMemoryLinks()
bot := inboundTestBot(mem, links, &fakeAPI{}, admin.Username) bot := inboundTestBot(mem, links, &fakeAPI{}, &mail.Recording{}, admin.Username)
root := seedLinkedRoot(t, mem, links, homeowner.ID, "thread-1") root := seedLinkedRoot(t, mem, links, homeowner.ID, "thread-1")
plumberReply := &store.Post{ParentID: &root.ID, AuthorID: admin.ID, Body: "First look."} plumberReply := &store.Post{ParentID: &root.ID, AuthorID: admin.ID, Body: "First look."}
if err := mem.CreatePost(context.Background(), plumberReply); err != nil { if err := mem.CreatePost(context.Background(), plumberReply); err != nil {
@@ -108,7 +106,7 @@ func TestInboundIgnoresAllowlistHiddenAndEchoSources(t *testing.T) {
mem, homeowner, admin := seedInboundUsers(t) mem, homeowner, admin := seedInboundUsers(t)
links := newMemoryLinks() links := newMemoryLinks()
api := &fakeAPI{} api := &fakeAPI{}
bot := inboundTestBot(mem, links, api, admin.Username) bot := inboundTestBot(mem, links, api, mail.Nop{}, admin.Username)
root := seedLinkedRoot(t, mem, links, homeowner.ID, "thread-1") root := seedLinkedRoot(t, mem, links, homeowner.ID, "thread-1")
hidden := &store.Post{ hidden := &store.Post{
AuthorID: homeowner.ID, AuthorID: homeowner.ID,
@@ -150,9 +148,10 @@ func TestInboundIgnoresAllowlistHiddenAndEchoSources(t *testing.T) {
} }
} }
func inboundTestBot(mem *store.Memory, links *memoryLinks, api *fakeAPI, adminUsername string) *Bot { func inboundTestBot(mem *store.Memory, links *memoryLinks, api *fakeAPI, mailer mail.Notifier, adminUsername string) *Bot {
bot := New("channel-1", links, api) bot := New("channel-1", links, api)
bot.store = mem bot.store = mem
bot.mail = mailer
bot.admins = map[string]string{"snow-admin": adminUsername} bot.admins = map[string]string{"snow-admin": adminUsername}
bot.botUserID = "bot-1" bot.botUserID = "bot-1"
return bot return bot