Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b3aac413d | ||
|
|
a1b0351048 | ||
|
|
de8560948a | ||
|
|
8eddbfe438 | ||
|
|
911355ae35 |
+1
-1
@@ -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), notifier)
|
bot, err := discord.FromEnv(store.NewDiscordLinks(db), bus, store.NewPostgres(db))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("discord: %v", err)
|
log.Fatalf("discord: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-7
@@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
|
|
||||||
"plumber/internal/events"
|
"plumber/internal/events"
|
||||||
"plumber/internal/mail"
|
|
||||||
"plumber/internal/store"
|
"plumber/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ type Bot struct {
|
|||||||
links store.DiscordLinkStore
|
links store.DiscordLinkStore
|
||||||
api API
|
api API
|
||||||
store store.Store
|
store store.Store
|
||||||
mail mail.Notifier
|
bus events.Publisher
|
||||||
admins map[string]string
|
admins map[string]string
|
||||||
botUserID 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.
|
// 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"))
|
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 == "" {
|
||||||
@@ -56,12 +55,9 @@ func FromEnv(links store.DiscordLinkStore, bus *events.Bus, st store.Store, mail
|
|||||||
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.mail = mailer
|
bot.bus = bus
|
||||||
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 {
|
||||||
@@ -102,6 +98,14 @@ 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
|
||||||
|
|||||||
@@ -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) {
|
func TestOutboundUpdateWithoutLinkCreates(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -218,7 +248,7 @@ func TestFormatMessage(t *testing.T) {
|
|||||||
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, nil)
|
bot, err := FromEnv(newMemoryLinks(), 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)
|
||||||
}
|
}
|
||||||
@@ -227,12 +257,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, nil); err == nil {
|
if _, err := FromEnv(newMemoryLinks(), 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, nil); err == nil {
|
if _, err := FromEnv(newMemoryLinks(), nil, nil); err == nil {
|
||||||
t.Fatal("expected error when token is missing")
|
t.Fatal("expected error when token is missing")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-38
@@ -7,11 +7,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
|
|
||||||
"plumber/internal/mail"
|
"plumber/internal/events"
|
||||||
"plumber/internal/store"
|
"plumber/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -127,8 +126,9 @@ 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.notifyInboundReply(parent, root, reply, author)
|
b.publishInbound(reply, root, 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,44 +184,20 @@ func (b *Bot) postAndRoot(ctx context.Context, postID string) (*store.Post, *sto
|
|||||||
return post, current, nil
|
return post, current, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) notifyInboundReply(parent, root, reply *store.Post, author *store.User) {
|
func (b *Bot) publishInbound(reply, root *store.Post, author *store.User) {
|
||||||
if parent == nil || root == nil || reply == nil || author == nil || b.mail == nil {
|
if b == nil || b.bus == nil || reply == nil || root == nil || author == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, disabled := b.mail.(mail.Nop); disabled {
|
ev := events.PostEvent{
|
||||||
return
|
PostID: reply.ID,
|
||||||
}
|
|
||||||
recipientID := parent.AuthorID
|
|
||||||
if author.Admin() {
|
|
||||||
recipientID = root.AuthorID
|
|
||||||
}
|
|
||||||
if recipientID == author.ID {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msg := mail.PostReply{
|
|
||||||
RootID: root.ID,
|
RootID: root.ID,
|
||||||
RootTitle: root.Title,
|
Body: reply.Body,
|
||||||
ReplyID: reply.ID,
|
AuthorID: author.ID,
|
||||||
ReplyBody: reply.Body,
|
AuthorName: author.Name,
|
||||||
ReplyAuthorName: author.Name,
|
AuthorRole: string(author.Role),
|
||||||
}
|
}
|
||||||
go func() {
|
if reply.ParentID != nil {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ev.ParentID = *reply.ParentID
|
||||||
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) == "" {
|
b.bus.Publish(context.Background(), events.PostCreated{PostEvent: ev})
|
||||||
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)
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,11 @@ func TestInboundCreatesSiteReply(t *testing.T) {
|
|||||||
links := newMemoryLinks()
|
links := newMemoryLinks()
|
||||||
api := &fakeAPI{}
|
api := &fakeAPI{}
|
||||||
mailer := &mail.Recording{}
|
mailer := &mail.Recording{}
|
||||||
bot := inboundTestBot(mem, links, api, mailer, admin.Username)
|
bot := inboundTestBot(mem, links, api, 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")
|
||||||
|
|
||||||
@@ -68,7 +70,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{}, &mail.Recording{}, admin.Username)
|
bot := inboundTestBot(mem, links, &fakeAPI{}, 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 {
|
||||||
@@ -106,7 +108,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, mail.Nop{}, admin.Username)
|
bot := inboundTestBot(mem, links, api, 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,
|
||||||
@@ -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 := 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
|
||||||
|
|||||||
Reference in New Issue
Block a user