mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 14:27:27 +00:00
feat(messaging): group & broadcast messages + email-all
Members can now message a group or everyone, and the operator can announce to
the whole membership by email.
Inbox channel (any member):
- msg@ recipient spec accepts a comma list (alice,bob,carol) or all/*/everyone
to broadcast: `ssh msg@host alice,bob hi`, `ssh msg@host all hi`.
- Members hub TUI gains multi-select: space toggles, `a` selects all, `m`
messages the selected group (header names the audience); selection clears
after send.
- store.SendMessageMulti delivers one body to many inboxes in a single
transaction (dedupes, skips empties); resolveRecipients validates names,
excludes the sender, and skips banned members on broadcast.
Email channel (operator, explicit):
- new `agentbbs broadcast` subcommand sends an announcement to ALL members via
inbox + email. Preview by default (like notify-creds); --send delivers;
--no-inbox/--no-email pick a channel; --subject/--from/--user refine it.
Email reaches only verified addresses and refuses --send without SMTP.
Tests: SendMessageMulti (dedupe/empty), resolveRecipients (list/unknown/all
tokens, banned + sender exclusion), TUI selection (toggle/select-all/group
compose). Docs: docs/messaging.md. build/vet/gofmt/`go test ./...` green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
54da317f4e
commit
103ddeb7c9
8 changed files with 658 additions and 32 deletions
|
|
@ -118,6 +118,12 @@ type Store interface {
|
|||
|
||||
// SendMessage leaves a note from→to in the recipient's inbox.
|
||||
SendMessage(from, to, body string) error
|
||||
// SendMessageMulti delivers one body to every recipient in to, in a single
|
||||
// transaction (used for group + broadcast messages so a partial failure
|
||||
// doesn't leave some inboxes written and others not). Duplicate and empty
|
||||
// recipients are skipped; an empty list is a no-op. Returns the number of
|
||||
// inboxes written.
|
||||
SendMessageMulti(from string, to []string, body string) (int, error)
|
||||
// Inbox returns up to n messages addressed to username, newest first.
|
||||
Inbox(username string, n int) ([]Message, error)
|
||||
// UnreadCount reports how many unread messages username has waiting.
|
||||
|
|
@ -769,6 +775,38 @@ func (s *sqliteStore) SendMessage(from, to, body string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (s *sqliteStore) SendMessageMulti(from string, to []string, body string) (int, error) {
|
||||
if len(to) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }() // no-op after a successful Commit
|
||||
stmt, err := tx.Prepare(`INSERT INTO messages (from_user, to_user, body) VALUES (?,?,?)`)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
seen := make(map[string]bool, len(to))
|
||||
sent := 0
|
||||
for _, recipient := range to {
|
||||
if recipient == "" || seen[recipient] {
|
||||
continue
|
||||
}
|
||||
seen[recipient] = true
|
||||
if _, err := stmt.Exec(from, recipient, body); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
sent++
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return sent, nil
|
||||
}
|
||||
|
||||
func (s *sqliteStore) Inbox(username string, n int) ([]Message, error) {
|
||||
if n <= 0 {
|
||||
n = 50
|
||||
|
|
|
|||
|
|
@ -58,6 +58,33 @@ func TestMessagingRoundtrip(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSendMessageMulti(t *testing.T) {
|
||||
st := openTest(t)
|
||||
for _, n := range []string{"alice", "bob", "carol"} {
|
||||
_, _ = st.EnsureUser(n, "member", "SHA256:"+n)
|
||||
}
|
||||
|
||||
// Duplicate + empty recipients are skipped; returns the count actually written.
|
||||
sent, err := st.SendMessageMulti("alice", []string{"bob", "carol", "bob", ""}, "group hello")
|
||||
if err != nil {
|
||||
t.Fatalf("multi: %v", err)
|
||||
}
|
||||
if sent != 2 {
|
||||
t.Fatalf("want 2 inboxes written, got %d", sent)
|
||||
}
|
||||
for _, who := range []string{"bob", "carol"} {
|
||||
in, _ := st.Inbox(who, 10)
|
||||
if len(in) != 1 || in[0].Body != "group hello" || in[0].From != "alice" {
|
||||
t.Fatalf("%s inbox = %+v", who, in)
|
||||
}
|
||||
}
|
||||
|
||||
// Empty recipient list is a no-op.
|
||||
if sent, err := st.SendMessageMulti("alice", nil, "x"); err != nil || sent != 0 {
|
||||
t.Fatalf("empty list: sent=%d err=%v", sent, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnlineUsers(t *testing.T) {
|
||||
st := openTest(t)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue