landed E-mail subscription delivery
This commit is contained in:
+14
-1
@@ -20,6 +20,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"git.erbosoft.com/amy/amsterdam/database"
|
||||
"git.erbosoft.com/amy/amsterdam/email"
|
||||
"git.erbosoft.com/amy/amsterdam/htmlcheck"
|
||||
"git.erbosoft.com/amy/amsterdam/ui"
|
||||
"github.com/CloudyKit/jet/v6"
|
||||
@@ -770,7 +771,19 @@ func PostInTopic(ctxt ui.AmContext) (string, any, error) {
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
|
||||
// TODO: whoever's subscribed needs to get a copy of this post in their E-mail
|
||||
// Check who's subscribed to this topic.
|
||||
subs, err := topic.GetSubscribers(ctxt.Ctx())
|
||||
if err != nil {
|
||||
log.Errorf("unable to deliver message to subscribers: %v", err)
|
||||
} else if len(subs) > 0 {
|
||||
// kick off a task to compose E-mails and deliver them to everyone
|
||||
alias := ctxt.GetScratch("currentAlias").(string)
|
||||
poster := ctxt.CurrentUser()
|
||||
ipaddr := ctxt.RemoteIP()
|
||||
ampool.Submit(func(ctx context.Context) {
|
||||
email.AmDeliverSubscription(ctx, comm, conf, alias, topic, poster, hdr, postText, subs, ipaddr)
|
||||
})
|
||||
}
|
||||
|
||||
if !ctxt.FormFieldIsSet("attach") {
|
||||
return "redirect", returnURL, nil // no attachment - just bounce directly to the destination
|
||||
|
||||
+23
-1
@@ -12,6 +12,7 @@ package database
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -258,7 +259,7 @@ func internalContactInfo(ctx context.Context, id int32) (*ContactInfo, error) {
|
||||
/* AmGetContactInfo retrieves the contact info for a given identifier.
|
||||
* Parameters:
|
||||
* ctx - Standard Go context value.
|
||||
* id - The contact info ID top retrieve.
|
||||
* id - The contact info ID to retrieve.
|
||||
* Returns:
|
||||
* ContactInfo retrieved, or nil.
|
||||
* Standard Go error status.
|
||||
@@ -280,6 +281,27 @@ func AmGetContactInfo(ctx context.Context, id int32) (*ContactInfo, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
/* AmGetContactInfoForUser retrieves the contact info for a given user ID.
|
||||
* Parameters:
|
||||
* ctx - Standard Go context value.
|
||||
* uid - The UID to get the contact info for.
|
||||
* Returns:
|
||||
* ContactInfo retrieved, or nil.
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func AmGetContactInfoForUser(ctx context.Context, uid int32) (*ContactInfo, error) {
|
||||
row := amdb.QueryRowContext(ctx, "SELECT contactid FROM contacts WHERE owner_uid = ? AND owner_commid = -1", uid)
|
||||
var cid int32
|
||||
err := row.Scan(&cid)
|
||||
switch err {
|
||||
case nil:
|
||||
return AmGetContactInfo(ctx, cid)
|
||||
case sql.ErrNoRows:
|
||||
return nil, errors.New("contact not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
/* AmNewUserContactInfo creates a new contact info record for the user.
|
||||
* Parameters:
|
||||
* uid - The UID of the owner of this contact info.
|
||||
|
||||
@@ -240,6 +240,25 @@ func (t *Topic) GetBozos(ctx context.Context, u *User) ([]TopicBozo, error) {
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
// GetSubscribers returns an array of UIDs of every user that subscribed to the topic.
|
||||
func (t *Topic) GetSubscribers(ctx context.Context) ([]int32, error) {
|
||||
rs, err := amdb.QueryContext(ctx, "SELECT uid FROM topicsettings WHERE topicid = ? AND subscribe <> 0", t.TopicId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rc := make([]int32, 0)
|
||||
for rs.Next() {
|
||||
var tmp int32
|
||||
err = rs.Scan(&tmp)
|
||||
if err == nil {
|
||||
rc = append(rc, tmp)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return rc, err
|
||||
}
|
||||
|
||||
// TopicSettings contains per-user settings for topics, including the "last read" message pointer.
|
||||
type TopicSettings struct {
|
||||
TopicId int32 `db:"topicid"` // unique ID of the topic
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
_(italicized items can be deferred)_
|
||||
|
||||
- ~~Topics list: Set up Conference permalink~~
|
||||
- Send out E-mails to topic subscribers when a post is made
|
||||
- ~~Send out E-mails to topic subscribers when a post is made~~
|
||||
- _Error handling: shift titles and templates for different error codes_
|
||||
- Find Posts
|
||||
- Services mechanism: Conference vtable
|
||||
|
||||
@@ -24,6 +24,7 @@ type Message interface {
|
||||
AddTo(string, string)
|
||||
AddCC(string, string)
|
||||
AddBCC(string, string)
|
||||
GetSubject() string
|
||||
SetSubject(string)
|
||||
SetText(string)
|
||||
AddHeader(string, string)
|
||||
@@ -85,6 +86,11 @@ func (m *amMessage) AddBCC(addr string, name string) {
|
||||
m.toAddrs = append(m.toAddrs, addr)
|
||||
}
|
||||
|
||||
// GetSubject gets the message's subject.
|
||||
func (m *amMessage) GetSubject() string {
|
||||
return m.subject
|
||||
}
|
||||
|
||||
// SetSubject sets the message's subject.
|
||||
func (m *amMessage) SetSubject(s string) {
|
||||
m.subject = s
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ var auth smtp.Auth
|
||||
|
||||
// formatMessage takes a message and turns it into serialized bytes for sending.
|
||||
func formatMessage(ctx context.Context, m *amMessage) ([]byte, error) {
|
||||
if m.template != "" {
|
||||
if m.template != "" && m.text == "" {
|
||||
// Render the template for the message, which may reset Subject.
|
||||
templ, err := emailRenderer.GetTemplate(m.template)
|
||||
if err == nil {
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Amsterdam Web Communities System
|
||||
* Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
// Package email contains support for E-mail messages sent by Amsterdam.
|
||||
package email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"git.erbosoft.com/amy/amsterdam/database"
|
||||
"git.erbosoft.com/amy/amsterdam/htmlcheck"
|
||||
"github.com/CloudyKit/jet/v6"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
/* AmDeliverSubscription takes a message that's just been posted to a topic and sends it to the list of subscribers
|
||||
* to that topic. It's intended to be executed in a worker pool task.
|
||||
* Parameters:
|
||||
* ctx - Standard Go context parameter.
|
||||
* comm - Community the message was posted to.
|
||||
* conf - Conference the message was posted to.
|
||||
* confAlias - Current alias for that conference.
|
||||
* topic - Current topic the message was posted to.
|
||||
* poster - User that posted the message.
|
||||
* post - The message that was posted.
|
||||
* text - The text of that post.
|
||||
* recipUids - Array of user IDs representing recipients of E-mail messages.
|
||||
* ipaddr - IP address of the poster, used for mail tracing.
|
||||
*/
|
||||
func AmDeliverSubscription(ctx context.Context, comm *database.Community, conf *database.Conference, confAlias string,
|
||||
topic *database.Topic, poster *database.User, post *database.PostHeader, text string, recipUids []int32, ipaddr string) {
|
||||
log.Debugf("AmDeliverSubscription kicked off by %s with %d mail(s) to deliver", poster.Username, len(recipUids))
|
||||
|
||||
// Preprocess the text and the pseud.
|
||||
checker, err := htmlcheck.AmNewHTMLChecker(ctx, "mail-post")
|
||||
if err != nil {
|
||||
log.Errorf("AmDeliverSubscription: failed to get HTML checker (%v)", err)
|
||||
return
|
||||
}
|
||||
var realText string
|
||||
err = checker.Append(text)
|
||||
if err == nil {
|
||||
err = checker.Finish()
|
||||
if err == nil {
|
||||
realText, err = checker.Value()
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("AmDeliverSubscription: failed to process post text (%v)", err)
|
||||
return
|
||||
}
|
||||
checker.Reset()
|
||||
var realPseud string
|
||||
if post.Pseud != nil {
|
||||
err = checker.Append(*post.Pseud)
|
||||
if err == nil {
|
||||
err = checker.Finish()
|
||||
if err == nil {
|
||||
realPseud, err = checker.Value()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
realPseud = ""
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("AmDeliverSubscription: failed to process post pseud (%v)", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Use Jet to format the message directly. We bypass the regular formatter in formatMessage in sender.go because
|
||||
// we don't want to have to format the message once per recipient.
|
||||
templ, err := emailRenderer.GetTemplate("mailpost.jet")
|
||||
if err != nil {
|
||||
log.Errorf("AmDeliverSubscription: failed to load template (%v)", err)
|
||||
return
|
||||
}
|
||||
vars := make(jet.VarMap)
|
||||
vars.Set("userName", poster.Username)
|
||||
vars.Set("communityName", comm.Name)
|
||||
vars.Set("conferenceName", conf.Name)
|
||||
vars.Set("topicName", topic.Name)
|
||||
pl := database.AmCreatePostLinkContext(comm.Alias, confAlias, topic.Number)
|
||||
vars.Set("topicLink", pl.AsString())
|
||||
vars.Set("pseud", realPseud)
|
||||
vars.Set("text", realText)
|
||||
subjectSink := AmNewEmailMessage(0, "")
|
||||
var textBuf bytes.Buffer
|
||||
err = templ.Execute(&textBuf, vars, subjectSink)
|
||||
if err != nil {
|
||||
log.Errorf("AmDeliverSubscription: failed to format template (%v)", err)
|
||||
return
|
||||
}
|
||||
sendText := textBuf.String()
|
||||
|
||||
// The delivery loop; build each message and send it. Note that sending a message puts the Message structure on
|
||||
// the sender goroutine channel, so we have to create a new Message each time, unlike what we did in Venice.
|
||||
for i := range recipUids {
|
||||
err = ctx.Err()
|
||||
if err != nil {
|
||||
log.Errorf("AmDeliverSubscription: aborted on send loop iter %d with %v", i+1, err)
|
||||
}
|
||||
if ci, err := database.AmGetContactInfoForUser(ctx, recipUids[i]); err == nil {
|
||||
msg := AmNewEmailMessage(poster.Uid, ipaddr)
|
||||
msg.SetSubject(subjectSink.GetSubject())
|
||||
msg.SetText(sendText)
|
||||
msg.AddTo(*ci.Email, ci.FullName(false))
|
||||
msg.Send()
|
||||
} else {
|
||||
log.Warnf("AmDeliverSubscription skipped uid %d because no contact info retrieved (%v)", recipUids[i], err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,11 @@
|
||||
{*
|
||||
* Amsterdam Web Communities System
|
||||
* Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*}
|
||||
{{ .SetSubject("Amsterdam Email Confirmation") }}
|
||||
Welcome to the Amsterdam conferencing system! In order to fully activate your
|
||||
account after you register or change your E-mail address, you must provide a
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{*
|
||||
* Amsterdam Web Communities System
|
||||
* Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*}
|
||||
{{ .SetSubject("New Post in " + topicName)}}
|
||||
The following message was just posted by "{{ userName }}" to the
|
||||
"{{ topicName | raw }}" topic in the "{{ conferenceName }}" conference
|
||||
of the "{{ communityName }}" community on the Amsterdam community system.
|
||||
--------------------------------------------------------------------------
|
||||
{{ pseud | raw }}
|
||||
|
||||
{{ text | raw }}
|
||||
--------------------------------------------------------------------------
|
||||
Join the ongoing discussion at:
|
||||
{{ GlobalConfig.Site.BaseURL }}/go/{{ topicLink }}
|
||||
|
||||
To stop receiving new posts in this topic by E-mail, visit the above URL,
|
||||
click the "Manage" button, and click the "Stop Subscribing To This Topic" link.
|
||||
@@ -1,3 +1,11 @@
|
||||
{*
|
||||
* Amsterdam Web Communities System
|
||||
* Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*}
|
||||
{{ .SetSubject("Amsterdam Password Changed") }}
|
||||
The password for your account "{{ username }}" has been changed. The new
|
||||
password is "{{ password }}".
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
{*
|
||||
* Amsterdam Web Communities System
|
||||
* Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*}
|
||||
{{ .SetSubject("Amsterdam Password Reminder Message") }}
|
||||
Here is the password reminder for your account "{{ username }}" as you requested:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user