landed conference mass E-mail
This commit is contained in:
@@ -11,12 +11,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.erbosoft.com/amy/amsterdam/database"
|
||||
"git.erbosoft.com/amy/amsterdam/email"
|
||||
"git.erbosoft.com/amy/amsterdam/ui"
|
||||
"git.erbosoft.com/amy/amsterdam/util"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -514,6 +517,13 @@ func ConfReports(ctxt ui.AmContext) (string, any) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ConferenceEmailForm displays the dialog for E-mailing participants in a conference.
|
||||
* Parameters:
|
||||
* ctxt - The AmContext for the request.
|
||||
* Returns:
|
||||
* Command string dictating what to be rendered.
|
||||
* Data as a parameter for the command string.
|
||||
*/
|
||||
func ConferenceEmailForm(ctxt ui.AmContext) (string, any) {
|
||||
comm := ctxt.CurrentCommunity()
|
||||
conf := ctxt.GetScratch("currentConference").(*database.Conference)
|
||||
@@ -535,6 +545,115 @@ func ConferenceEmailForm(ctxt ui.AmContext) (string, any) {
|
||||
return "framed", "conf_email.jet"
|
||||
}
|
||||
|
||||
/* ConferenceEmail sends E-mail to participants in a conference.
|
||||
* Parameters:
|
||||
* ctxt - The AmContext for the request.
|
||||
* Returns:
|
||||
* Command string dictating what to be rendered.
|
||||
* Data as a parameter for the command string.
|
||||
*/
|
||||
func ConferenceEmail(ctxt ui.AmContext) (string, any) {
|
||||
comm := ctxt.CurrentCommunity()
|
||||
conf := ctxt.GetScratch("currentConference").(*database.Conference)
|
||||
myLevel := ctxt.GetScratch("levelInConference").(uint16)
|
||||
if !conf.TestPermission("Conference.EMailParticipants", myLevel) {
|
||||
return "error", ENOPERM
|
||||
}
|
||||
|
||||
// Handle button presses.
|
||||
if ctxt.FormFieldIsSet("cancel") {
|
||||
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/manage", comm.Alias, ctxt.GetScratch("currentAlias"))
|
||||
} else if !ctxt.FormFieldIsSet("send") {
|
||||
return "error", EBUTTON
|
||||
}
|
||||
|
||||
// extract user selector
|
||||
porl := ctxt.FormField("porl")
|
||||
var userSelect int
|
||||
switch porl {
|
||||
case "0":
|
||||
userSelect = database.ActiveUserPosters
|
||||
case "1":
|
||||
userSelect = database.ActiveUserReaders
|
||||
default:
|
||||
return "error", EINVAL
|
||||
}
|
||||
|
||||
// extract number of days
|
||||
days := -1
|
||||
if ctxt.FormFieldIsSet("xday") {
|
||||
var err error
|
||||
days, err = ctxt.FormFieldInt("day")
|
||||
if err != nil {
|
||||
return "error", err
|
||||
} else if days <= 0 {
|
||||
return "error", "Invalid number of days specified"
|
||||
}
|
||||
}
|
||||
|
||||
// extract list of recipients and other needed data
|
||||
var recipients []string
|
||||
templateName := ""
|
||||
topicName := ""
|
||||
top, err := ctxt.FormFieldInt("top")
|
||||
if err != nil {
|
||||
return "error", err
|
||||
}
|
||||
if top == 0 {
|
||||
recipients, err = conf.GetActiveUserEMailAddrs(ctxt.Ctx(), userSelect, days)
|
||||
if userSelect == database.ActiveUserPosters {
|
||||
templateName = "conf_mass_poster.jet"
|
||||
} else {
|
||||
templateName = "conf_mass_reader.jet"
|
||||
}
|
||||
} else {
|
||||
var topic *database.Topic
|
||||
if topic, err = database.AmGetTopicByNumber(ctxt.Ctx(), conf, int16(top)); err == nil {
|
||||
recipients, err = topic.GetActiveUserEMailAddrs(ctxt.Ctx(), userSelect, days)
|
||||
topicName = topic.Name
|
||||
}
|
||||
if userSelect == database.ActiveUserPosters {
|
||||
templateName = "topic_mass_poster.jet"
|
||||
} else {
|
||||
templateName = "topic_mass_reader.jet"
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return "error", err
|
||||
}
|
||||
|
||||
// Kick off a background task to send all the E-mail messages.
|
||||
subj := ctxt.FormField("subj")
|
||||
pb := ctxt.FormField("pb")
|
||||
confName := conf.Name
|
||||
commName := comm.Name
|
||||
myUID := ctxt.CurrentUserId()
|
||||
myIP := ctxt.RemoteIP()
|
||||
log.Infof("ConferenceEmail: About to send mass E-mail to %d recipients", len(recipients))
|
||||
ampool.Submit(func(ctx context.Context) {
|
||||
start := time.Now()
|
||||
for _, addr := range recipients {
|
||||
err := ctx.Err()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
msg := email.AmNewEmailMessage(myUID, myIP)
|
||||
msg.AddTo(addr, "")
|
||||
msg.SetSubject(subj)
|
||||
msg.SetTemplate(templateName)
|
||||
msg.AddVariable("text", pb)
|
||||
msg.AddVariable("topicName", topicName)
|
||||
msg.AddVariable("confName", confName)
|
||||
msg.AddVariable("commName", commName)
|
||||
msg.Send()
|
||||
}
|
||||
elapsed := time.Since(start)
|
||||
log.Infof("ConferenceEmail delivery completed in %s", elapsed)
|
||||
})
|
||||
|
||||
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/manage", comm.Alias, ctxt.GetScratch("currentAlias"))
|
||||
}
|
||||
|
||||
/* CreateConferenceForm displays the dialog for creating a new conference.
|
||||
* Parameters:
|
||||
* ctxt - The AmContext for the request.
|
||||
|
||||
@@ -726,6 +726,66 @@ func (c *Conference) GetActivity(ctx context.Context, reportType int) ([]Activit
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
// Active user selection types.
|
||||
const (
|
||||
ActiveUserReaders = 0 // select active readers
|
||||
ActiveUserPosters = 1 // select active posters
|
||||
)
|
||||
|
||||
/* GetActiveUserEMailAddrs gets the E-mail addresses of each user that's active in the conference, omitting those that have opted out of mass E-mails.
|
||||
* Parameters:
|
||||
* ctx - Standard Go context value.
|
||||
* userSelect - Selects which type of users to return:
|
||||
* ActiveUserReaders - Select users that have actively read.
|
||||
* ActiveUserPosters - Select users that have actively posted.
|
||||
* dayLimit - If less than 0, it is ignored. If equal to 0, this function is a no-op. Otherwise, specifies a limit on the number of days
|
||||
* between the user's activity and now.
|
||||
* Returns:
|
||||
* List of E-mail addresses matchin the criteria, in arbitrary order.
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func (c *Conference) GetActiveUserEMailAddrs(ctx context.Context, userSelect, dayLimit int) ([]string, error) {
|
||||
if dayLimit == 0 {
|
||||
return make([]string, 0), nil
|
||||
}
|
||||
var myfield string
|
||||
switch userSelect {
|
||||
case ActiveUserReaders:
|
||||
myfield = "s.last_read"
|
||||
case ActiveUserPosters:
|
||||
myfield = "s.last_post"
|
||||
default:
|
||||
return nil, errors.New("invalid user selection parameter")
|
||||
}
|
||||
sql := fmt.Sprintf(`SELECT c.email, %s FROM contacts c, users u, confsettings s, propuser p WHERE c.contactid = u.contactid AND u.uid = s.uid
|
||||
AND s.confid = ? AND u.is_anon = 0 AND u.uid = p.uid AND p.ndx = %d AND p.data NOT LIKE '%%%s%%' AND ISNULL(%s) = 0 ORDER BY %s DESC`, myfield, UserPropFlags,
|
||||
util.OptionCharFromIndex(UserFlagMassMailOptOut), myfield, myfield)
|
||||
rs, err := amdb.QueryContext(ctx, sql, c.ConfId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var stopPoint *time.Time = nil
|
||||
if dayLimit > 0 {
|
||||
mynow := time.Now().UTC()
|
||||
y, m, d := mynow.AddDate(0, 0, -dayLimit).Date()
|
||||
stopPointActual := time.Date(y, m, d, 0, 0, 0, 0, mynow.Location())
|
||||
stopPoint = &stopPointActual
|
||||
}
|
||||
rc := make([]string, 0)
|
||||
for rs.Next() {
|
||||
var addy string
|
||||
var point time.Time
|
||||
if err = rs.Scan(&addy, &point); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if stopPoint != nil && point.Before(*stopPoint) {
|
||||
break
|
||||
}
|
||||
rc = append(rc, addy)
|
||||
}
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
/* AmGetConference returns a conference given its ID.
|
||||
* Parameters:
|
||||
* ctx - Standard Go context value.
|
||||
|
||||
@@ -328,6 +328,60 @@ func (t *Topic) GetActivity(ctx context.Context, reportType int) ([]ActivityRepo
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
/* GetActiveUserEMailAddrs gets the E-mail addresses of each user that's active in the topic, omitting those that have opted out of mass E-mails.
|
||||
* Parameters:
|
||||
* ctx - Standard Go context value.
|
||||
* userSelect - Selects which type of users to return:
|
||||
* ActiveUserReaders - Select users that have actively read.
|
||||
* ActiveUserPosters - Select users that have actively posted.
|
||||
* dayLimit - If less than 0, it is ignored. If equal to 0, this function is a no-op. Otherwise, specifies a limit on the number of days
|
||||
* between the user's activity and now.
|
||||
* Returns:
|
||||
* List of E-mail addresses matchin the criteria, in arbitrary order.
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func (t *Topic) GetActiveUserEMailAddrs(ctx context.Context, userSelect, dayLimit int) ([]string, error) {
|
||||
if dayLimit == 0 {
|
||||
return make([]string, 0), nil
|
||||
}
|
||||
var myfield string
|
||||
switch userSelect {
|
||||
case ActiveUserReaders:
|
||||
myfield = "s.last_read"
|
||||
case ActiveUserPosters:
|
||||
myfield = "s.last_post"
|
||||
default:
|
||||
return nil, errors.New("invalid user selection parameter")
|
||||
}
|
||||
sql := fmt.Sprintf(`SELECT c.email, %s FROM contacts c, users u, topicsettings s, propuser p WHERE c.contactid = u.contactid AND u.uid = s.uid
|
||||
AND s.topicid = ? AND u.is_anon = 0 AND u.uid = p.uid AND p.ndx = %d AND p.data NOT LIKE '%%%s%%' AND ISNULL(%s) = 0 ORDER BY %s DESC`, myfield, UserPropFlags,
|
||||
util.OptionCharFromIndex(UserFlagMassMailOptOut), myfield, myfield)
|
||||
rs, err := amdb.QueryContext(ctx, sql, t.TopicId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var stopPoint *time.Time = nil
|
||||
if dayLimit > 0 {
|
||||
mynow := time.Now().UTC()
|
||||
y, m, d := mynow.AddDate(0, 0, -dayLimit).Date()
|
||||
stopPointActual := time.Date(y, m, d, 0, 0, 0, 0, mynow.Location())
|
||||
stopPoint = &stopPointActual
|
||||
}
|
||||
rc := make([]string, 0)
|
||||
for rs.Next() {
|
||||
var addy string
|
||||
var point time.Time
|
||||
if err = rs.Scan(&addy, &point); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if stopPoint != nil && point.Before(*stopPoint) {
|
||||
break
|
||||
}
|
||||
rc = append(rc, addy)
|
||||
}
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
// backgroundPurgeTopic removes all posts from a topic that's been deleted.
|
||||
func backgroundPurgeTopic(ctx context.Context, topicid int32) error {
|
||||
success := false
|
||||
|
||||
@@ -69,7 +69,7 @@ _(italicized items can be deferred)_
|
||||
- ~~Manage members~~
|
||||
- ~~Custom appearance~~
|
||||
- ~~Activity reports~~
|
||||
- E-mail
|
||||
- ~~E-mail~~
|
||||
- Export Messages
|
||||
- Import Messages
|
||||
- Delete Conference
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{*
|
||||
* 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/.
|
||||
*}
|
||||
{{ text }}
|
||||
|
||||
You are receiving this message because you are a participant in the
|
||||
"{{ confName }}" conference in the Amsterdam community "{{ commName }}".
|
||||
The sender is a host of this conference. To stop receiving mass E-mailed
|
||||
notices from all Amsterdam community and conference hosts, visit Amsterdam, click
|
||||
on the "Profile" link, check the box labeled "Don't send me mass E-mail from
|
||||
community/conference hosts," and click Update to save this preference.
|
||||
@@ -0,0 +1,16 @@
|
||||
{*
|
||||
* 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/.
|
||||
*}
|
||||
{{ text }}
|
||||
|
||||
You are receiving this message because you are a reader of the
|
||||
"{{ confName }}" conference in the Amsterdam community "{{ commName }}".
|
||||
The sender is a host of this conference. To stop receiving mass E-mailed
|
||||
notices from all Amsterdam community and conference hosts, visit Amsterdam, click
|
||||
on the "Profile" link, check the box labeled "Don't send me mass E-mail from
|
||||
community/conference hosts," and click Update to save this preference.
|
||||
@@ -0,0 +1,17 @@
|
||||
{*
|
||||
* 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/.
|
||||
*}
|
||||
{{ text }}
|
||||
|
||||
You are receiving this message because you are a participant in the
|
||||
"{{ topicName }}" topic in the "{{ confName }}" conference in the
|
||||
Amsterdam community "{{ commName }}". The sender is a host of the
|
||||
"{{ confName }}" conference. To stop receiving mass E-mailed notices from
|
||||
all Amsterdam community and conference hosts, visit Amsterdam, click
|
||||
on the "Profile" link, check the box labeled "Don't send me mass E-mail from
|
||||
community/conference hosts," and click Update to save this preference.
|
||||
@@ -0,0 +1,17 @@
|
||||
{*
|
||||
* 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/.
|
||||
*}
|
||||
{{ text }}
|
||||
|
||||
You are receiving this message because you are a reader of the
|
||||
"{{ topicName }}" topic in the "{{ confName }}" conference in the
|
||||
Amsterdam community "{{ commName }}". The sender is a host of the
|
||||
"{{ confName }}" conference. To stop receiving mass E-mailed notices from
|
||||
all Amsterdam community and conference hosts, visit Amsterdam, click
|
||||
on the "Profile" link, check the box labeled "Don't send me mass E-mail from
|
||||
community/conference hosts," and click Update to save this preference.
|
||||
@@ -123,6 +123,7 @@ func setupEcho() *echo.Echo {
|
||||
confGroup.POST("/custom", ui.AmWrap(ConfCustom))
|
||||
confGroup.GET("/activity", ui.AmWrap(ConfReports))
|
||||
confGroup.GET("/email", ui.AmWrap(ConferenceEmailForm))
|
||||
confGroup.POST("/email", ui.AmWrap(ConferenceEmail))
|
||||
confGroup.GET("/hotlist", ui.AmWrap(AddToHotlist))
|
||||
confGroup.GET("/invite", ui.AmWrap(InviteToConference))
|
||||
confGroup.GET("/r/:topic", ui.AmWrap(ReadPosts), ui.SetTopic)
|
||||
|
||||
+9
-1
@@ -17,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
// optionAlphabet is the alphabet from which OptionSets serialize to and from strings.
|
||||
const optionAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&()*+,-./:;<=>?@[]^_`{|}~"
|
||||
const optionAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$&()*+,-./:;<=>?@[]^_`{|}~"
|
||||
|
||||
// OptionSet is a bit set that can be persisted as a specially-constructed string.
|
||||
type OptionSet struct {
|
||||
@@ -65,3 +65,11 @@ func OptionSetFromString(s string) *OptionSet {
|
||||
}
|
||||
return &OptionSet{bits: bs}
|
||||
}
|
||||
|
||||
// OptionCharFromIndex converts an integer into the matching character from the option alphabet.
|
||||
func OptionCharFromIndex(ndx uint) string {
|
||||
if ndx > uint(len(optionAlphabet)) {
|
||||
return ""
|
||||
}
|
||||
return optionAlphabet[ndx : ndx+1]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user