2 Commits

Author SHA1 Message Date
amy a2c2a1f750 cleanups to startup code and goroutine code 2026-05-06 22:19:08 -06:00
amy 08a10a55dd factoring out a lot of string constant values 2026-05-03 22:39:11 -06:00
21 changed files with 242 additions and 191 deletions
+10 -7
View File
@@ -29,9 +29,9 @@ import (
// Error classifications // Error classifications
const ( const (
classUnspecified = 0 classUnspecified = iota // unspecified, barf
classNeedInstall = 1 classNeedInstall // need to install the database
classNeedConvert = 2 classNeedConvert // need to convert a Venice database
) )
// MySQL Errors // MySQL Errors
@@ -210,11 +210,11 @@ func prepareDB() (string, error) {
} }
// SetupDb sets up the database and associated items. // SetupDb sets up the database and associated items.
func SetupDb() (func(), error) { func SetupDb() (string, func(), error) {
exitfns := make([]func(), 0, 2) exitfns := make([]func(), 0, 2)
version, err := prepareDB() version, err := prepareDB()
if err != nil { if err != nil {
return nil, err return "X", nil, err
} }
db, err := sqlx.Connect(config.GlobalComputedConfig.DatabaseDriver, buildMysqlDSN(false)) db, err := sqlx.Connect(config.GlobalComputedConfig.DatabaseDriver, buildMysqlDSN(false))
if err == nil { if err == nil {
@@ -223,6 +223,7 @@ func SetupDb() (func(), error) {
if err == nil { if err == nil {
if g.Version != version { if g.Version != version {
log.Warnf("!! database version %s does not match prepared version %s", g.Version, version) log.Warnf("!! database version %s does not match prepared version %s", g.Version, version)
version = g.Version
} }
setupAdCache() setupAdCache()
setupUserCache() setupUserCache()
@@ -232,11 +233,11 @@ func SetupDb() (func(), error) {
setupConferenceCache() setupConferenceCache()
exitfns = append(exitfns, setupAuditWriter()) exitfns = append(exitfns, setupAuditWriter())
exitfns = append(exitfns, setupIPBanSweep()) exitfns = append(exitfns, setupIPBanSweep())
log.Infof("SetupDb(): database version %s", g.Version) log.Infof("SetupDb(): database version %s", version)
} }
} }
slices.Reverse(exitfns) slices.Reverse(exitfns)
return func() { return version, func() {
for _, f := range exitfns { for _, f := range exitfns {
f() f()
} }
@@ -262,6 +263,8 @@ func transaction(ctx context.Context) (*sqlx.Tx, func() error, func()) {
err = tx.Commit() err = tx.Commit()
if err == nil { if err == nil {
live = false live = false
} else {
log.Errorf("***COMMIT ERROR*** %v", err)
} }
} }
return err return err
+24 -24
View File
@@ -254,10 +254,10 @@ func (p *PostHeader) Text(ctx context.Context) (string, error) {
// Link returns a link string to this post. // Link returns a link string to this post.
func (p *PostHeader) Link(ctx context.Context, commid int32, scope string) (string, error) { func (p *PostHeader) Link(ctx context.Context, commid int32, scope string) (string, error) {
if scope == "topic" { if scope == PLSCOPE_TOPIC {
return fmt.Sprintf("%d", p.Num), nil return fmt.Sprintf("%d", p.Num), nil
} }
if scope == "conference" || scope == "community" || scope == "global" { if scope == PLSCOPE_CONFERENCE || scope == PLSCOPE_COMMUNITY || scope == PLSCOPE_GLOBAL {
topic, err := AmGetTopic(ctx, p.TopicId) topic, err := AmGetTopic(ctx, p.TopicId)
if err != nil { if err != nil {
return "", err return "", err
@@ -720,55 +720,55 @@ func decodeSearchScope(ctx context.Context, scopeValues []any) (string, *Communi
} }
if thisComm, ok := scopeValues[i].(*Community); ok { if thisComm, ok := scopeValues[i].(*Community); ok {
if myComm != nil { if myComm != nil {
return "error", nil, nil, nil, errors.New("cannot specify multiple communities") return PLSCOPE_ERROR, nil, nil, nil, errors.New("cannot specify multiple communities")
} }
myComm = thisComm myComm = thisComm
continue continue
} }
if thisConf, ok := scopeValues[i].(*Conference); ok { if thisConf, ok := scopeValues[i].(*Conference); ok {
if myConf != nil { if myConf != nil {
return "error", nil, nil, nil, errors.New("cannot specify multiple conferences") return PLSCOPE_ERROR, nil, nil, nil, errors.New("cannot specify multiple conferences")
} }
myConf = thisConf myConf = thisConf
continue continue
} }
if thisTopic, ok := scopeValues[i].(*Topic); ok { if thisTopic, ok := scopeValues[i].(*Topic); ok {
if myTopic != nil { if myTopic != nil {
return "error", nil, nil, nil, errors.New("cannot specify multiple topics") return PLSCOPE_ERROR, nil, nil, nil, errors.New("cannot specify multiple topics")
} }
myTopic = thisTopic myTopic = thisTopic
continue continue
} }
return "error", nil, nil, nil, errors.New("invalid item specified in scope") return PLSCOPE_ERROR, nil, nil, nil, errors.New("invalid item specified in scope")
} }
// Based on which slots are full, determine the scope. Also error-check relations between the specified slots. // Based on which slots are full, determine the scope. Also error-check relations between the specified slots.
if myComm == nil { if myComm == nil {
if myConf != nil || myTopic != nil { if myConf != nil || myTopic != nil {
return "error", nil, nil, nil, errors.New("conference/topic specified without community") return PLSCOPE_ERROR, nil, nil, nil, errors.New("conference/topic specified without community")
} }
return "global", nil, nil, nil, nil return PLSCOPE_GLOBAL, nil, nil, nil, nil
} }
if myConf == nil { if myConf == nil {
if myTopic != nil { if myTopic != nil {
return "error", nil, nil, nil, errors.New("topic specified without conference") return PLSCOPE_ERROR, nil, nil, nil, errors.New("topic specified without conference")
} }
return "community", myComm, nil, nil, nil return PLSCOPE_COMMUNITY, myComm, nil, nil, nil
} }
f, err := myConf.InCommunity(ctx, myComm) f, err := myConf.InCommunity(ctx, myComm)
if err != nil { if err != nil {
return "error", nil, nil, nil, err return PLSCOPE_ERROR, nil, nil, nil, err
} }
if !f { if !f {
return "error", nil, nil, nil, errors.New("community does not contain conference") return PLSCOPE_ERROR, nil, nil, nil, errors.New("community does not contain conference")
} }
if myTopic == nil { if myTopic == nil {
return "conference", myComm, myConf, nil, nil return PLSCOPE_CONFERENCE, myComm, myConf, nil, nil
} }
if myTopic.ConfId != myConf.ConfId { if myTopic.ConfId != myConf.ConfId {
return "error", nil, nil, nil, errors.New("conference does not contain topic") return PLSCOPE_ERROR, nil, nil, nil, errors.New("conference does not contain topic")
} }
return "topic", myComm, myConf, myTopic, nil return PLSCOPE_TOPIC, myComm, myConf, myTopic, nil
} }
/* AmSearchPosts finds posts by using full text search on their contents. /* AmSearchPosts finds posts by using full text search on their contents.
@@ -794,7 +794,7 @@ func AmSearchPosts(ctx context.Context, searchTerms string, u *User, offset, max
} }
// Get the proper service index to match against the community services. // Get the proper service index to match against the community services.
confService, err := AmGetServiceIndex("community", "Conference") confService, err := AmGetServiceIndex(AM_DOMAIN_COMMUNITY, AM_SVC_CONFERENCE)
if err != nil { if err != nil {
return nil, -1, err return nil, -1, err
} }
@@ -802,7 +802,7 @@ func AmSearchPosts(ctx context.Context, searchTerms string, u *User, offset, max
// Get the count of matching posts. // Get the count of matching posts.
var count int var count int
switch scope { switch scope {
case "global": case PLSCOPE_GLOBAL:
err = amdb.GetContext(ctx, &count, `SELECT COUNT(*) err = amdb.GetContext(ctx, &count, `SELECT COUNT(*)
FROM communities q JOIN commtoconf s ON s.commid = q.commid JOIN confs c ON c.confid = s.confid FROM communities q JOIN commtoconf s ON s.commid = q.commid JOIN confs c ON c.confid = s.confid
JOIN commmember m ON m.commid = q.commid JOIN users u ON u.uid = m.uid JOIN commftrs f ON f.commid = q.commid JOIN commmember m ON m.commid = q.commid JOIN users u ON u.uid = m.uid JOIN commftrs f ON f.commid = q.commid
@@ -810,7 +810,7 @@ func AmSearchPosts(ctx context.Context, searchTerms string, u *User, offset, max
LEFT JOIN confmember x ON (c.confid = x.confid AND u.uid = x.uid) LEFT JOIN confmember x ON (c.confid = x.confid AND u.uid = x.uid)
WHERE u.uid = ? AND f.ftr_code = ? AND GREATEST(u.base_lvl,m.granted_lvl,s.granted_lvl,COALESCE(x.granted_lvl,0)) >= c.read_lvl WHERE u.uid = ? AND f.ftr_code = ? AND GREATEST(u.base_lvl,m.granted_lvl,s.granted_lvl,COALESCE(x.granted_lvl,0)) >= c.read_lvl
AND p.scribble_uid IS NULL AND MATCH(d.data) AGAINST (?)`, u.Uid, confService, searchTerms) AND p.scribble_uid IS NULL AND MATCH(d.data) AGAINST (?)`, u.Uid, confService, searchTerms)
case "community": case PLSCOPE_COMMUNITY:
err = amdb.GetContext(ctx, &count, `SELECT COUNT(*) err = amdb.GetContext(ctx, &count, `SELECT COUNT(*)
FROM communities q JOIN commtoconf s ON s.commid = q.commid JOIN confs c ON c.confid = s.confid FROM communities q JOIN commtoconf s ON s.commid = q.commid JOIN confs c ON c.confid = s.confid
JOIN commmember m ON m.commid = q.commid JOIN users u ON u.uid = m.uid JOIN commftrs f ON f.commid = q.commid JOIN commmember m ON m.commid = q.commid JOIN users u ON u.uid = m.uid JOIN commftrs f ON f.commid = q.commid
@@ -818,7 +818,7 @@ func AmSearchPosts(ctx context.Context, searchTerms string, u *User, offset, max
LEFT JOIN confmember x ON (c.confid = x.confid AND u.uid = x.uid) LEFT JOIN confmember x ON (c.confid = x.confid AND u.uid = x.uid)
WHERE u.uid = ? AND f.ftr_code = ? AND GREATEST(u.base_lvl,m.granted_lvl,s.granted_lvl,COALESCE(x.granted_lvl,0)) >= c.read_lvl WHERE u.uid = ? AND f.ftr_code = ? AND GREATEST(u.base_lvl,m.granted_lvl,s.granted_lvl,COALESCE(x.granted_lvl,0)) >= c.read_lvl
AND q.commid = ? AND p.scribble_uid IS NULL AND MATCH(d.data) AGAINST (?)`, u.Uid, confService, comm.Id, searchTerms) AND q.commid = ? AND p.scribble_uid IS NULL AND MATCH(d.data) AGAINST (?)`, u.Uid, confService, comm.Id, searchTerms)
case "conference": case PLSCOPE_CONFERENCE:
err = amdb.GetContext(ctx, &count, `SELECT COUNT(*) err = amdb.GetContext(ctx, &count, `SELECT COUNT(*)
FROM communities q JOIN commtoconf s ON s.commid = q.commid JOIN confs c ON c.confid = s.confid FROM communities q JOIN commtoconf s ON s.commid = q.commid JOIN confs c ON c.confid = s.confid
JOIN commmember m ON m.commid = q.commid JOIN users u ON u.uid = m.uid JOIN commftrs f ON f.commid = q.commid JOIN commmember m ON m.commid = q.commid JOIN users u ON u.uid = m.uid JOIN commftrs f ON f.commid = q.commid
@@ -826,7 +826,7 @@ func AmSearchPosts(ctx context.Context, searchTerms string, u *User, offset, max
LEFT JOIN confmember x ON (c.confid = x.confid AND u.uid = x.uid) LEFT JOIN confmember x ON (c.confid = x.confid AND u.uid = x.uid)
WHERE u.uid = ? AND f.ftr_code = ? AND GREATEST(u.base_lvl,m.granted_lvl,s.granted_lvl,COALESCE(x.granted_lvl,0)) >= c.read_lvl WHERE u.uid = ? AND f.ftr_code = ? AND GREATEST(u.base_lvl,m.granted_lvl,s.granted_lvl,COALESCE(x.granted_lvl,0)) >= c.read_lvl
AND q.commid = ? AND c.confid = ? AND p.scribble_uid IS NULL AND MATCH(d.data) AGAINST (?)`, u.Uid, confService, comm.Id, conf.ConfId, searchTerms) AND q.commid = ? AND c.confid = ? AND p.scribble_uid IS NULL AND MATCH(d.data) AGAINST (?)`, u.Uid, confService, comm.Id, conf.ConfId, searchTerms)
case "topic": case PLSCOPE_TOPIC:
err = amdb.GetContext(ctx, &count, `SELECT COUNT(*) err = amdb.GetContext(ctx, &count, `SELECT COUNT(*)
FROM communities q JOIN commtoconf s ON s.commid = q.commid JOIN confs c ON c.confid = s.confid FROM communities q JOIN commtoconf s ON s.commid = q.commid JOIN confs c ON c.confid = s.confid
JOIN commmember m ON m.commid = q.commid JOIN users u ON u.uid = m.uid JOIN commftrs f ON f.commid = q.commid JOIN commmember m ON m.commid = q.commid JOIN users u ON u.uid = m.uid JOIN commftrs f ON f.commid = q.commid
@@ -844,7 +844,7 @@ func AmSearchPosts(ctx context.Context, searchTerms string, u *User, offset, max
// Get the matching posts themselves. // Get the matching posts themselves.
var rs *sql.Rows var rs *sql.Rows
switch scope { switch scope {
case "global": case PLSCOPE_GLOBAL:
rs, err = amdb.QueryContext(ctx, `SELECT q.commid, q.alias, c.confid, t.topicid, t.num, p.postid, p.num, u2.username, p.posted, p.linecount, d.data rs, err = amdb.QueryContext(ctx, `SELECT q.commid, q.alias, c.confid, t.topicid, t.num, p.postid, p.num, u2.username, p.posted, p.linecount, d.data
FROM communities q JOIN commtoconf s ON s.commid = q.commid JOIN confs c ON c.confid = s.confid FROM communities q JOIN commtoconf s ON s.commid = q.commid JOIN confs c ON c.confid = s.confid
JOIN commmember m ON m.commid = q.commid JOIN users u ON u.uid = m.uid JOIN commftrs f ON f.commid = q.commid JOIN commmember m ON m.commid = q.commid JOIN users u ON u.uid = m.uid JOIN commftrs f ON f.commid = q.commid
@@ -853,7 +853,7 @@ func AmSearchPosts(ctx context.Context, searchTerms string, u *User, offset, max
WHERE u.uid = ? AND f.ftr_code = ? AND GREATEST(u.base_lvl,m.granted_lvl,s.granted_lvl,COALESCE(x.granted_lvl,0)) >= c.read_lvl WHERE u.uid = ? AND f.ftr_code = ? AND GREATEST(u.base_lvl,m.granted_lvl,s.granted_lvl,COALESCE(x.granted_lvl,0)) >= c.read_lvl
AND p.scribble_uid IS NULL AND MATCH(d.data) AGAINST (?) ORDER BY q.commname, c.name, t.num, p.num AND p.scribble_uid IS NULL AND MATCH(d.data) AGAINST (?) ORDER BY q.commname, c.name, t.num, p.num
LIMIT ? OFFSET ?`, u.Uid, confService, searchTerms, max, offset) LIMIT ? OFFSET ?`, u.Uid, confService, searchTerms, max, offset)
case "community": case PLSCOPE_COMMUNITY:
rs, err = amdb.QueryContext(ctx, `SELECT q.commid, q.alias, c.confid, t.topicid, t.num, p.postid, p.num, u2.username, p.posted, p.linecount, d.data rs, err = amdb.QueryContext(ctx, `SELECT q.commid, q.alias, c.confid, t.topicid, t.num, p.postid, p.num, u2.username, p.posted, p.linecount, d.data
FROM communities q JOIN commtoconf s ON s.commid = q.commid JOIN confs c ON c.confid = s.confid FROM communities q JOIN commtoconf s ON s.commid = q.commid JOIN confs c ON c.confid = s.confid
JOIN commmember m ON m.commid = q.commid JOIN users u ON u.uid = m.uid JOIN commftrs f ON f.commid = q.commid JOIN commmember m ON m.commid = q.commid JOIN users u ON u.uid = m.uid JOIN commftrs f ON f.commid = q.commid
@@ -862,7 +862,7 @@ func AmSearchPosts(ctx context.Context, searchTerms string, u *User, offset, max
WHERE u.uid = ? AND f.ftr_code = ? AND GREATEST(u.base_lvl,m.granted_lvl,s.granted_lvl,COALESCE(x.granted_lvl,0)) >= c.read_lvl WHERE u.uid = ? AND f.ftr_code = ? AND GREATEST(u.base_lvl,m.granted_lvl,s.granted_lvl,COALESCE(x.granted_lvl,0)) >= c.read_lvl
AND q.commid = ? AND p.scribble_uid IS NULL AND MATCH(d.data) AGAINST (?) ORDER BY q.commname, c.name, t.num, p.num AND q.commid = ? AND p.scribble_uid IS NULL AND MATCH(d.data) AGAINST (?) ORDER BY q.commname, c.name, t.num, p.num
LIMIT ? OFFSET ?`, u.Uid, confService, comm.Id, searchTerms, max, offset) LIMIT ? OFFSET ?`, u.Uid, confService, comm.Id, searchTerms, max, offset)
case "conference": case PLSCOPE_CONFERENCE:
rs, err = amdb.QueryContext(ctx, `SELECT q.commid, q.alias, c.confid, t.topicid, t.num, p.postid, p.num, u2.username, p.posted, p.linecount, d.data rs, err = amdb.QueryContext(ctx, `SELECT q.commid, q.alias, c.confid, t.topicid, t.num, p.postid, p.num, u2.username, p.posted, p.linecount, d.data
FROM communities q JOIN commtoconf s ON s.commid = q.commid JOIN confs c ON c.confid = s.confid FROM communities q JOIN commtoconf s ON s.commid = q.commid JOIN confs c ON c.confid = s.confid
JOIN commmember m ON m.commid = q.commid JOIN users u ON u.uid = m.uid JOIN commftrs f ON f.commid = q.commid JOIN commmember m ON m.commid = q.commid JOIN users u ON u.uid = m.uid JOIN commftrs f ON f.commid = q.commid
@@ -871,7 +871,7 @@ func AmSearchPosts(ctx context.Context, searchTerms string, u *User, offset, max
WHERE u.uid = ? AND f.ftr_code = ? AND GREATEST(u.base_lvl,m.granted_lvl,s.granted_lvl,COALESCE(x.granted_lvl,0)) >= c.read_lvl WHERE u.uid = ? AND f.ftr_code = ? AND GREATEST(u.base_lvl,m.granted_lvl,s.granted_lvl,COALESCE(x.granted_lvl,0)) >= c.read_lvl
AND q.commid = ? AND c.confid = ? AND p.scribble_uid IS NULL AND MATCH(d.data) AGAINST (?) ORDER BY q.commname, c.name, t.num, p.num AND q.commid = ? AND c.confid = ? AND p.scribble_uid IS NULL AND MATCH(d.data) AGAINST (?) ORDER BY q.commname, c.name, t.num, p.num
LIMIT ? OFFSET ?`, u.Uid, confService, comm.Id, conf.ConfId, searchTerms, max, offset) LIMIT ? OFFSET ?`, u.Uid, confService, comm.Id, conf.ConfId, searchTerms, max, offset)
case "topic": case PLSCOPE_TOPIC:
rs, err = amdb.QueryContext(ctx, `SELECT q.commid, q.alias, c.confid, t.topicid, t.num, p.postid, p.num, u2.username, p.posted, p.linecount, d.data rs, err = amdb.QueryContext(ctx, `SELECT q.commid, q.alias, c.confid, t.topicid, t.num, p.postid, p.num, u2.username, p.posted, p.linecount, d.data
FROM communities q JOIN commtoconf s ON s.commid = q.commid JOIN confs c ON c.confid = s.confid FROM communities q JOIN commtoconf s ON s.commid = q.commid JOIN confs c ON c.confid = s.confid
JOIN commmember m ON m.commid = q.commid JOIN users u ON u.uid = m.uid JOIN commftrs f ON f.commid = q.commid JOIN commmember m ON m.commid = q.commid JOIN users u ON u.uid = m.uid JOIN commftrs f ON f.commid = q.commid
+63 -43
View File
@@ -20,6 +20,25 @@ import (
"strings" "strings"
) )
// Post link scopes.
const (
PLSCOPE_GLOBAL = "global"
PLSCOPE_COMMUNITY = "community"
PLSCOPE_CONFERENCE = "conference"
PLSCOPE_TOPIC = "topic"
PLSCOPE_ERROR = "error"
)
// Post link classifications.
const (
PLCLASS_COMMUNITY = "community"
PLCLASS_CONFERENCE = "conference"
PLCLASS_TOPIC = "topic"
PLCLASS_POST = "post"
PLCLASS_POSTRANGE = "postrange"
PLCLASS_POSTOPENRANGE = "postopenrange"
)
// PostLinkData is the structure holding the decoded parts of the post link. // PostLinkData is the structure holding the decoded parts of the post link.
type PostLinkData struct { type PostLinkData struct {
Community string Community string
@@ -121,53 +140,53 @@ func (d *PostLinkData) Classify() (string, string) {
if d.FirstPost == -1 { if d.FirstPost == -1 {
return "", "" return "", ""
} else if d.LastPost == -1 { } else if d.LastPost == -1 {
return "topic", "postopenrange" return PLSCOPE_TOPIC, PLCLASS_POSTOPENRANGE
} else if d.LastPost == d.FirstPost { } else if d.LastPost == d.FirstPost {
return "topic", "post" return PLSCOPE_TOPIC, PLCLASS_POST
} else { } else {
return "topic", "postrange" return PLSCOPE_TOPIC, PLCLASS_POSTRANGE
} }
} else { } else {
if d.FirstPost == -1 { if d.FirstPost == -1 {
return "conference", "topic" return PLSCOPE_CONFERENCE, PLCLASS_TOPIC
} else if d.LastPost == -1 { } else if d.LastPost == -1 {
return "conference", "postopenrange" return PLSCOPE_CONFERENCE, PLCLASS_POSTOPENRANGE
} else if d.LastPost == d.FirstPost { } else if d.LastPost == d.FirstPost {
return "conference", "post" return PLSCOPE_CONFERENCE, PLCLASS_POST
} else { } else {
return "conference", "postrange" return PLSCOPE_CONFERENCE, PLCLASS_POSTRANGE
} }
} }
} else { } else {
if d.Topic == -1 { if d.Topic == -1 {
return "community", "conference" return PLSCOPE_COMMUNITY, PLCLASS_CONFERENCE
} else { } else {
if d.FirstPost == -1 { if d.FirstPost == -1 {
return "community", "topic" return PLSCOPE_COMMUNITY, PLCLASS_TOPIC
} else if d.LastPost == -1 { } else if d.LastPost == -1 {
return "community", "postopenrange" return PLSCOPE_COMMUNITY, PLCLASS_POSTOPENRANGE
} else if d.LastPost == d.FirstPost { } else if d.LastPost == d.FirstPost {
return "community", "post" return PLSCOPE_COMMUNITY, PLCLASS_POST
} else { } else {
return "community", "postrange" return PLSCOPE_COMMUNITY, PLCLASS_POSTRANGE
} }
} }
} }
} else { } else {
if d.Conference == "" { if d.Conference == "" {
return "global", "community" return PLSCOPE_GLOBAL, PLCLASS_COMMUNITY
} else { } else {
if d.Topic == -1 { if d.Topic == -1 {
return "global", "conference" return PLSCOPE_GLOBAL, PLCLASS_CONFERENCE
} else { } else {
if d.FirstPost == -1 { if d.FirstPost == -1 {
return "global", "topic" return PLSCOPE_GLOBAL, PLCLASS_TOPIC
} else if d.LastPost == -1 { } else if d.LastPost == -1 {
return "global", "postopenrange" return PLSCOPE_GLOBAL, PLCLASS_POSTOPENRANGE
} else if d.LastPost == d.FirstPost { } else if d.LastPost == d.FirstPost {
return "global", "post" return PLSCOPE_GLOBAL, PLCLASS_POST
} else { } else {
return "global", "postrange" return PLSCOPE_GLOBAL, PLCLASS_POSTRANGE
} }
} }
} }
@@ -276,25 +295,25 @@ func AmDecodePostLink(data string) (*PostLinkData, error) {
if len(data) > maxLinkLength { if len(data) > maxLinkLength {
return nil, errors.New("post link string too long") return nil, errors.New("post link string too long")
} }
rc := PostLinkData{ rc := new(PostLinkData{
Community: "", Community: "",
Conference: "", Conference: "",
Topic: -1, Topic: -1,
FirstPost: -1, FirstPost: -1,
LastPost: -1, LastPost: -1,
} })
work := data work := data
// First test: Bang // First test: Bang
pos := strings.IndexByte(work, '!') pos := strings.IndexByte(work, '!')
if pos > 0 { if pos > 0 {
err := validateCommunity(work[:pos], &rc) err := validateCommunity(work[:pos], rc)
if err != nil { if err != nil {
return nil, err return nil, err
} }
work = work[pos+1:] work = work[pos+1:]
if len(work) == 0 { if len(work) == 0 {
return &rc, nil // community link return rc, nil // community link
} }
} else if pos == 0 { } else if pos == 0 {
return nil, errors.New("cannot have ! at beginning") return nil, errors.New("cannot have ! at beginning")
@@ -306,14 +325,14 @@ func AmDecodePostLink(data string) (*PostLinkData, error) {
// no dots in here, must be either "postlink" or "community!conference" // no dots in here, must be either "postlink" or "community!conference"
var err error var err error
if rc.Community == "" { if rc.Community == "" {
err = decodePostRange(work, &rc) err = decodePostRange(work, rc)
} else { } else {
err = validateConference(work, &rc) err = validateConference(work, rc)
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &rc, nil return rc, nil
} }
// Peel off the initial substring before the dot. // Peel off the initial substring before the dot.
@@ -324,19 +343,19 @@ func AmDecodePostLink(data string) (*PostLinkData, error) {
var err error var err error
if rc.Community == "" { if rc.Community == "" {
// it's either "conference." or "topic." - try the latter first // it's either "conference." or "topic." - try the latter first
err = decodeTopicNumber(confOrTopic, &rc) err = decodeTopicNumber(confOrTopic, rc)
if err != nil { if err != nil {
// it's not a topic number, try it as a conference name // it's not a topic number, try it as a conference name
err = validateConference(confOrTopic, &rc) err = validateConference(confOrTopic, rc)
} }
} else { } else {
// it was "community!conference." // it was "community!conference."
err = validateConference(confOrTopic, &rc) err = validateConference(confOrTopic, rc)
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &rc, nil return rc, nil
} }
// Third test: Dot #2 // Third test: Dot #2
@@ -347,38 +366,38 @@ func AmDecodePostLink(data string) (*PostLinkData, error) {
if rc.Community == "" { if rc.Community == "" {
// either "conference.topic" or "topic.posts" // either "conference.topic" or "topic.posts"
isTopic := false isTopic := false
err = decodeTopicNumber(confOrTopic, &rc) err = decodeTopicNumber(confOrTopic, rc)
if err != nil { if err != nil {
// it's "conference.topic" // it's "conference.topic"
err = validateConference(confOrTopic, &rc) err = validateConference(confOrTopic, rc)
isTopic = true isTopic = true
} }
if err == nil { if err == nil {
if isTopic { if isTopic {
err = decodeTopicNumber(work, &rc) err = decodeTopicNumber(work, rc)
} else { } else {
err = decodePostRange(work, &rc) err = decodePostRange(work, rc)
} }
} }
} else { } else {
// we have "community!conference.topic" // we have "community!conference.topic"
err = validateConference(confOrTopic, &rc) err = validateConference(confOrTopic, rc)
if err == nil { if err == nil {
err = decodeTopicNumber(work, &rc) err = decodeTopicNumber(work, rc)
} }
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &rc, nil return rc, nil
} else if pos == 0 { } else if pos == 0 {
return nil, errors.New("cannot have . at beginning of string") return nil, errors.New("cannot have . at beginning of string")
} }
// We definitely have "conference.topic.something" or "community!conference.topic.something" // We definitely have "conference.topic.something" or "community!conference.topic.something"
err := validateConference(confOrTopic, &rc) err := validateConference(confOrTopic, rc)
if err == nil { if err == nil {
err = decodeTopicNumber(work[:pos], &rc) err = decodeTopicNumber(work[:pos], rc)
} }
if err != nil { if err != nil {
return nil, err return nil, err
@@ -386,22 +405,23 @@ func AmDecodePostLink(data string) (*PostLinkData, error) {
work = work[pos+1:] work = work[pos+1:]
if len(work) == 0 { if len(work) == 0 {
// we had "conference.topic." or "communtiy!conference.topic.", those are both valid // we had "conference.topic." or "communtiy!conference.topic.", those are both valid
return &rc, nil return rc, nil
} }
err = decodePostRange(work, &rc) // the rest must be the post range err = decodePostRange(work, rc) // the rest must be the post range
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &rc, nil return rc, nil
} }
// AmCreatePostLinkContext creates a new empty post link context.
func AmCreatePostLinkContext(community string, commid int32, conference string, topic int16) *PostLinkData { func AmCreatePostLinkContext(community string, commid int32, conference string, topic int16) *PostLinkData {
return &PostLinkData{ return new(PostLinkData{
Community: community, Community: community,
CommId: commid, CommId: commid,
Conference: conference, Conference: conference,
Topic: topic, Topic: topic,
FirstPost: -1, FirstPost: -1,
LastPost: -1, LastPost: -1,
} })
} }
+23 -9
View File
@@ -25,6 +25,20 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
// The service domain names.
const (
AM_DOMAIN_COMMUNITY = "community"
)
// The service names.
const (
AM_SVC_PROFILE = "Profile"
AM_SVC_ADMIN = "Admin"
AM_SVC_SYSADMIN = "SysAdmin"
AM_SVC_CONFERENCE = "Conference"
AM_SVC_MEMBERS = "Members"
)
// ServiceVTable is a series of functions called for services on specific events. // ServiceVTable is a series of functions called for services on specific events.
type ServiceVTable interface { type ServiceVTable interface {
OnNewCommunity(context.Context, *sqlx.Tx, *Community) error OnNewCommunity(context.Context, *sqlx.Tx, *Community) error
@@ -114,13 +128,13 @@ func init() {
serviceRoot.Domains[i].seqOrder = sqo serviceRoot.Domains[i].seqOrder = sqo
serviceRoot.byName[dom.DomainName] = &(serviceRoot.Domains[i]) serviceRoot.byName[dom.DomainName] = &(serviceRoot.Domains[i])
} }
dom := serviceRoot.byName["community"] dom := serviceRoot.byName[AM_DOMAIN_COMMUNITY]
empty := emptyServiceVTable{} empty := emptyServiceVTable{}
dom.byId["Profile"].vtable = &empty dom.byId[AM_SVC_PROFILE].vtable = &empty
dom.byId["Admin"].vtable = &empty dom.byId[AM_SVC_ADMIN].vtable = &empty
dom.byId["SysAdmin"].vtable = &empty dom.byId[AM_SVC_SYSADMIN].vtable = &empty
dom.byId["Conference"].vtable = &(conferenceServiceVTable{}) dom.byId[AM_SVC_CONFERENCE].vtable = &(conferenceServiceVTable{})
dom.byId["Members"].vtable = &empty dom.byId[AM_SVC_MEMBERS].vtable = &empty
} }
// setupServicesCache sets up the services cache. // setupServicesCache sets up the services cache.
@@ -166,7 +180,7 @@ func AmGetCommunityServices(ctx context.Context, cid int32) ([]*ServiceDef, erro
if err != nil { if err != nil {
return nil, err return nil, err
} }
dom := serviceRoot.byName["community"] dom := serviceRoot.byName[AM_DOMAIN_COMMUNITY]
a := make([]*ServiceDef, 0, len(dom.Services)) a := make([]*ServiceDef, 0, len(dom.Services))
for rs.Next() { for rs.Next() {
var ndx int16 var ndx int16
@@ -198,7 +212,7 @@ func AmGetCommunityServicesTx(ctx context.Context, tx *sqlx.Tx, cid int32) ([]*S
if err != nil { if err != nil {
return nil, err return nil, err
} }
dom := serviceRoot.byName["community"] dom := serviceRoot.byName[AM_DOMAIN_COMMUNITY]
a := make([]*ServiceDef, 0, len(dom.Services)) a := make([]*ServiceDef, 0, len(dom.Services))
for rs.Next() { for rs.Next() {
var ndx int16 var ndx int16
@@ -222,7 +236,7 @@ func AmGetCommunityServicesTx(ctx context.Context, tx *sqlx.Tx, cid int32) ([]*S
* Standard Go error status. * Standard Go error status.
*/ */
func AmEstablishCommunityServices(ctx context.Context, tx *sqlx.Tx, c *Community) error { func AmEstablishCommunityServices(ctx context.Context, tx *sqlx.Tx, c *Community) error {
dom := serviceRoot.byName["community"] dom := serviceRoot.byName[AM_DOMAIN_COMMUNITY]
a := make([]*ServiceDef, 0, len(dom.Services)) a := make([]*ServiceDef, 0, len(dom.Services))
for i, svc := range dom.Services { for i, svc := range dom.Services {
if svc.Default { if svc.Default {
+2 -2
View File
@@ -44,10 +44,10 @@ type Topic struct {
// Link returns a link string to this topic. // Link returns a link string to this topic.
func (t *Topic) Link(ctx context.Context, commid int32, scope string) (string, error) { func (t *Topic) Link(ctx context.Context, commid int32, scope string) (string, error) {
if scope == "conference" { if scope == PLSCOPE_CONFERENCE {
return fmt.Sprintf("%d.", t.Number), nil return fmt.Sprintf("%d.", t.Number), nil
} }
if scope == "community" || scope == "global" { if scope == PLSCOPE_COMMUNITY || scope == PLSCOPE_GLOBAL {
conf, err := AmGetConference(ctx, t.ConfId) conf, err := AmGetConference(ctx, t.ConfId)
if err == nil { if err == nil {
var plink string var plink string
+3
View File
@@ -234,6 +234,9 @@ func SetupMailSender() func() {
emailRenderer.AddGlobal("AmsterdamVersion", config.AMSTERDAM_VERSION) emailRenderer.AddGlobal("AmsterdamVersion", config.AMSTERDAM_VERSION)
emailRenderer.AddGlobal("AmsterdamCopyright", config.AMSTERDAM_COPYRIGHT) emailRenderer.AddGlobal("AmsterdamCopyright", config.AMSTERDAM_COPYRIGHT)
emailRenderer.AddGlobal("GlobalConfig", config.GlobalConfig) emailRenderer.AddGlobal("GlobalConfig", config.GlobalConfig)
emailRenderer.AddGlobal("PLSCOPE_COMMUNITY", database.PLSCOPE_COMMUNITY)
emailRenderer.AddGlobal("PLSCOPE_CONFERENCE", database.PLSCOPE_CONFERENCE)
emailRenderer.AddGlobal("PLSCOPE_TOPIC", database.PLSCOPE_TOPIC)
// Start the recycler. // Start the recycler.
messageRecycleBin = make(chan *amMessage, config.GlobalConfig.Tuning.Queues.EmailRecycle) messageRecycleBin = make(chan *amMessage, config.GlobalConfig.Tuning.Queues.EmailRecycle)
+2 -2
View File
@@ -18,12 +18,12 @@ a Amsterdam account. Once you have completed the process, click the "Join Now"
button. You will be prompted for the "password" for this community, which is button. You will be prompted for the "password" for this community, which is
"{{ comm.JoinKey }}". You will then be able to take part in the conferences that are "{{ comm.JoinKey }}". You will then be able to take part in the conferences that are
going on in the community. going on in the community.
{{ if mode == "conference" }} {{ if mode == INVMODE_CONFERENCE }}
After you've joined the "{{ comm.Name }}" community, check out the After you've joined the "{{ comm.Name }}" community, check out the
"{{ conf.Name }}" conference. To find it, after joining the community, "{{ conf.Name }}" conference. To find it, after joining the community,
click "Conferences" on the left menu bar, then click on the click "Conferences" on the left menu bar, then click on the
"{{ conf.Name }}" conference name in the conference list. "{{ conf.Name }}" conference name in the conference list.
{{ else if mode == "topic" }} {{ else if mode == INVMODE_TOPIC }}
After you've joined the "{{ comm.Name }}" community, check out the After you've joined the "{{ comm.Name }}" community, check out the
"{{ topic.Name | raw }}" topic in the "{{ conf.Name }}" conference. To find it, "{{ topic.Name | raw }}" topic in the "{{ conf.Name }}" conference. To find it,
after joining the community, click "Conferences" on the left menu bar, then after joining the community, click "Conferences" on the left menu bar, then
+2 -2
View File
@@ -17,12 +17,12 @@ link at the top of the page, or click the "Log In" link if you already have
a Amsterdam account. Once you have completed the process, click the "Join Now" a Amsterdam account. Once you have completed the process, click the "Join Now"
button. You will then be able to take part in the conferences that are button. You will then be able to take part in the conferences that are
going on in the community. going on in the community.
{{ if mode == "conference" }} {{ if mode == INVMODE_CONFERENCE }}
After you've joined the "{{ comm.Name }}" community, check out the After you've joined the "{{ comm.Name }}" community, check out the
"{{ conf.Name }}" conference. To find it, after joining the community, "{{ conf.Name }}" conference. To find it, after joining the community,
click "Conferences" on the left menu bar, then click on the click "Conferences" on the left menu bar, then click on the
"{{ conf.Name }}" conference name in the conference list. "{{ conf.Name }}" conference name in the conference list.
{{ else if mode == "topic" }} {{ else if mode == INVMODE_TOPIC }}
After you've joined the "{{ comm.Name }}" community, check out the After you've joined the "{{ comm.Name }}" community, check out the
"{{ topic.Name | raw }}" topic in the "{{ conf.Name }}" conference. To find it, "{{ topic.Name | raw }}" topic in the "{{ conf.Name }}" conference. To find it,
after joining the community, click "Conferences" on the left menu bar, then after joining the community, click "Conferences" on the left menu bar, then
+6 -6
View File
@@ -330,7 +330,7 @@ func commonFindGetBackend(ctxt ui.AmContext) (string, any) {
*/ */
func FindPostsPageCommunity(ctxt ui.AmContext) (string, any) { func FindPostsPageCommunity(ctxt ui.AmContext) (string, any) {
comm := ctxt.CurrentCommunity() comm := ctxt.CurrentCommunity()
ctxt.VarMap().Set("scope", "community") ctxt.VarMap().Set("scope", database.PLSCOPE_COMMUNITY)
ctxt.VarMap().Set("entityName", comm.Name) ctxt.VarMap().Set("entityName", comm.Name)
ctxt.VarMap().Set("backlink", fmt.Sprintf("/comm/%s/conf", comm.Alias)) ctxt.VarMap().Set("backlink", fmt.Sprintf("/comm/%s/conf", comm.Alias))
ctxt.VarMap().Set("postlink", fmt.Sprintf("/comm/%s/find", comm.Alias)) ctxt.VarMap().Set("postlink", fmt.Sprintf("/comm/%s/find", comm.Alias))
@@ -346,7 +346,7 @@ func FindPostsPageCommunity(ctxt ui.AmContext) (string, any) {
*/ */
func FindPostsPageConference(ctxt ui.AmContext) (string, any) { func FindPostsPageConference(ctxt ui.AmContext) (string, any) {
conf := ctxt.GetScratch("currentConference").(*database.Conference) conf := ctxt.GetScratch("currentConference").(*database.Conference)
ctxt.VarMap().Set("scope", "conference") ctxt.VarMap().Set("scope", database.PLSCOPE_CONFERENCE)
ctxt.VarMap().Set("entityName", conf.Name) ctxt.VarMap().Set("entityName", conf.Name)
ctxt.VarMap().Set("backlink", ctxt.GetScratch("ConferenceLink").(string)) ctxt.VarMap().Set("backlink", ctxt.GetScratch("ConferenceLink").(string))
ctxt.VarMap().Set("postlink", fmt.Sprintf("%s/find", ctxt.GetScratch("ConferenceLink"))) ctxt.VarMap().Set("postlink", fmt.Sprintf("%s/find", ctxt.GetScratch("ConferenceLink")))
@@ -362,7 +362,7 @@ func FindPostsPageConference(ctxt ui.AmContext) (string, any) {
*/ */
func FindPostsPageTopic(ctxt ui.AmContext) (string, any) { func FindPostsPageTopic(ctxt ui.AmContext) (string, any) {
topic := ctxt.GetScratch("currentTopic").(*database.Topic) topic := ctxt.GetScratch("currentTopic").(*database.Topic)
ctxt.VarMap().Set("scope", "topic") ctxt.VarMap().Set("scope", database.PLSCOPE_TOPIC)
ctxt.VarMap().Set("entityName", topic.Name) ctxt.VarMap().Set("entityName", topic.Name)
ctxt.VarMap().Set("backlink", fmt.Sprintf("%s/r/%d", ctxt.GetScratch("ConferenceLink"), topic.Number)) ctxt.VarMap().Set("backlink", fmt.Sprintf("%s/r/%d", ctxt.GetScratch("ConferenceLink"), topic.Number))
ctxt.VarMap().Set("postlink", fmt.Sprintf("%s/op/%d/find", ctxt.GetScratch("ConferenceLink"), topic.Number)) ctxt.VarMap().Set("postlink", fmt.Sprintf("%s/op/%d/find", ctxt.GetScratch("ConferenceLink"), topic.Number))
@@ -417,7 +417,7 @@ func commonFindPostBackend(ctxt ui.AmContext, comm *database.Community, conf *da
*/ */
func FindPostsCommunity(ctxt ui.AmContext) (string, any) { func FindPostsCommunity(ctxt ui.AmContext) (string, any) {
comm := ctxt.CurrentCommunity() comm := ctxt.CurrentCommunity()
ctxt.VarMap().Set("scope", "community") ctxt.VarMap().Set("scope", database.PLSCOPE_COMMUNITY)
ctxt.VarMap().Set("entityName", comm.Name) ctxt.VarMap().Set("entityName", comm.Name)
ctxt.VarMap().Set("backlink", fmt.Sprintf("/comm/%s/conf", comm.Alias)) ctxt.VarMap().Set("backlink", fmt.Sprintf("/comm/%s/conf", comm.Alias))
ctxt.VarMap().Set("postlink", fmt.Sprintf("/comm/%s/find", comm.Alias)) ctxt.VarMap().Set("postlink", fmt.Sprintf("/comm/%s/find", comm.Alias))
@@ -434,7 +434,7 @@ func FindPostsCommunity(ctxt ui.AmContext) (string, any) {
func FindPostsConference(ctxt ui.AmContext) (string, any) { func FindPostsConference(ctxt ui.AmContext) (string, any) {
comm := ctxt.CurrentCommunity() comm := ctxt.CurrentCommunity()
conf := ctxt.GetScratch("currentConference").(*database.Conference) conf := ctxt.GetScratch("currentConference").(*database.Conference)
ctxt.VarMap().Set("scope", "conference") ctxt.VarMap().Set("scope", database.PLSCOPE_CONFERENCE)
ctxt.VarMap().Set("entityName", conf.Name) ctxt.VarMap().Set("entityName", conf.Name)
ctxt.VarMap().Set("backlink", ctxt.GetScratch("ConferenceLink").(string)) ctxt.VarMap().Set("backlink", ctxt.GetScratch("ConferenceLink").(string))
ctxt.VarMap().Set("postlink", fmt.Sprintf("%s/find", ctxt.GetScratch("ConferenceLink"))) ctxt.VarMap().Set("postlink", fmt.Sprintf("%s/find", ctxt.GetScratch("ConferenceLink")))
@@ -452,7 +452,7 @@ func FindPostsTopic(ctxt ui.AmContext) (string, any) {
comm := ctxt.CurrentCommunity() comm := ctxt.CurrentCommunity()
conf := ctxt.GetScratch("currentConference").(*database.Conference) conf := ctxt.GetScratch("currentConference").(*database.Conference)
topic := ctxt.GetScratch("currentTopic").(*database.Topic) topic := ctxt.GetScratch("currentTopic").(*database.Topic)
ctxt.VarMap().Set("scope", "topic") ctxt.VarMap().Set("scope", database.PLSCOPE_TOPIC)
ctxt.VarMap().Set("entityName", topic.Name) ctxt.VarMap().Set("entityName", topic.Name)
ctxt.VarMap().Set("backlink", fmt.Sprintf("%s/r/%d", ctxt.GetScratch("ConferenceLink"), topic.Number)) ctxt.VarMap().Set("backlink", fmt.Sprintf("%s/r/%d", ctxt.GetScratch("ConferenceLink"), topic.Number))
ctxt.VarMap().Set("postlink", fmt.Sprintf("%s/op/%d/find", ctxt.GetScratch("ConferenceLink"), topic.Number)) ctxt.VarMap().Set("postlink", fmt.Sprintf("%s/op/%d/find", ctxt.GetScratch("ConferenceLink"), topic.Number))
+6 -10
View File
@@ -1,6 +1,6 @@
/* /*
* Amsterdam Web Communities System * Amsterdam Web Communities System
* Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved
* *
* This Source Code Form is subject to the terms of the Mozilla Public * 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 * License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -44,17 +44,17 @@ func (d *TrieDictionary) Size() int {
// CheckWord returns true if a word is in the dictionary, false if not. // CheckWord returns true if a word is in the dictionary, false if not.
func (d *TrieDictionary) CheckWord(word string) bool { func (d *TrieDictionary) CheckWord(word string) bool {
d.mutex.Lock() d.mutex.Lock()
defer d.mutex.Unlock()
_, rc := d.trie.Find(strings.ToLower(word)) _, rc := d.trie.Find(strings.ToLower(word))
d.mutex.Unlock()
return rc return rc
} }
// AddWord adds a new word to the dictionary. // AddWord adds a new word to the dictionary.
func (d *TrieDictionary) AddWord(word string) { func (d *TrieDictionary) AddWord(word string) {
d.mutex.Lock() d.mutex.Lock()
defer d.mutex.Unlock()
d.trie.Add(strings.ToLower(word), true) d.trie.Add(strings.ToLower(word), true)
d.count++ d.count++
d.mutex.Unlock()
} }
// DelWord deletes a word from the dictionary. // DelWord deletes a word from the dictionary.
@@ -89,12 +89,8 @@ func loadDict(d *TrieDictionary, words []byte) {
// LoadTrieDict creates a TrieDictionary from a byte array that represents a word list (one word per line). // LoadTrieDict creates a TrieDictionary from a byte array that represents a word list (one word per line).
func LoadTrieDict(words []byte) *TrieDictionary { func LoadTrieDict(words []byte) *TrieDictionary {
rc := TrieDictionary{ rc := new(TrieDictionary{loaded: atomic.Bool{}, trie: trie.New(), count: 0})
loaded: atomic.Bool{},
trie: trie.New(),
count: 0,
}
rc.loaded.Store(false) rc.loaded.Store(false)
go loadDict(&rc, words) go loadDict(rc, words)
return &rc return rc
} }
+4 -6
View File
@@ -55,10 +55,8 @@ func SetupDicts() {
log.Errorf("failed to load external dictionary %s: %v", config.GlobalConfig.Posting.ExternalDictionary, err) log.Errorf("failed to load external dictionary %s: %v", config.GlobalConfig.Posting.ExternalDictionary, err)
} }
} }
rw := spellingRewriter{ rw := new(spellingRewriter{dict: NewCompositeDict(dicts)})
dict: NewCompositeDict(dicts), rewriterRegistry[rw.Name()] = rw
}
rewriterRegistry[rw.Name()] = &rw
} }
// spellingRewriter is a rewriter that flags spelling errors. // spellingRewriter is a rewriter that flags spelling errors.
@@ -89,10 +87,10 @@ func (rw *spellingRewriter) Rewrite(ctx context.Context, data string, svc rewrit
if rw.dict.CheckWord(data) { if rw.dict.CheckWord(data) {
return nil return nil
} }
return &markupData{ return new(markupData{
beginMarkup: defaultBeginError, beginMarkup: defaultBeginError,
text: data, text: data,
endMarkup: defaultEndError, endMarkup: defaultEndError,
rescan: false, rescan: false,
} })
} }
+13 -3
View File
@@ -21,6 +21,13 @@ import (
"git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/ui"
) )
// Invitation modes.
const (
INVMODE_COMMUNITY = "community"
INVMODE_CONFERENCE = "conference"
INVMODE_TOPIC = "topic"
)
/* InviteToCommunity displays the community invitation form. /* InviteToCommunity displays the community invitation form.
* Parameters: * Parameters:
* ctxt - The AmContext for the request. * ctxt - The AmContext for the request.
@@ -116,7 +123,7 @@ func InviteSend(ctxt ui.AmContext) (string, any) {
} else { } else {
return "error", EPARAM return "error", EPARAM
} }
mode := "community" mode := INVMODE_COMMUNITY
var conf *database.Conference = nil var conf *database.Conference = nil
var topic *database.Topic = nil var topic *database.Topic = nil
if ctxt.FormFieldIsSet("confid") { if ctxt.FormFieldIsSet("confid") {
@@ -145,9 +152,9 @@ func InviteSend(ctxt ui.AmContext) (string, any) {
if err != nil { if err != nil {
return "errors", err return "errors", err
} }
mode = "topic" mode = INVMODE_TOPIC
} else { } else {
mode = "conference" mode = INVMODE_CONFERENCE
} }
} }
addr := ctxt.FormField("addr") addr := ctxt.FormField("addr")
@@ -168,6 +175,9 @@ func InviteSend(ctxt ui.AmContext) (string, any) {
mailMessage.SetTemplate("invite_private.jet") mailMessage.SetTemplate("invite_private.jet")
} }
mailMessage.AddTo(addr, "") mailMessage.AddTo(addr, "")
mailMessage.AddVariable("INVMODE_COMMUNITY", INVMODE_COMMUNITY)
mailMessage.AddVariable("INVMODE_CONFERENCE", INVMODE_CONFERENCE)
mailMessage.AddVariable("INVMODE_TOPIC", INVMODE_TOPIC)
mailMessage.AddVariable("comm", comm) mailMessage.AddVariable("comm", comm)
mailMessage.AddVariable("conf", conf) mailMessage.AddVariable("conf", conf)
mailMessage.AddVariable("topic", topic) mailMessage.AddVariable("topic", topic)
+21 -15
View File
@@ -31,6 +31,12 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// DEFAULT_MAXLOG is the default maximum log file size (16 megabytes).
const DEFAULT_MAXLOG = 16 * 1024 * 1024
// LOG_ROTATE_INTERVAL is the interval, in seconds, at which we try to rotate the logfile.
const LOG_ROTATE_INTERVAL = 10
/*---------------------------------------------------------------------------- /*----------------------------------------------------------------------------
* slog handler that outputs to Logrus * slog handler that outputs to Logrus
*---------------------------------------------------------------------------- *----------------------------------------------------------------------------
@@ -52,11 +58,7 @@ type SlogLogrusHandler struct {
// NewSlogLogrusHandler creates a SlogLogrusHandler with base information. // NewSlogLogrusHandler creates a SlogLogrusHandler with base information.
func NewSlogLogrusHandler() *SlogLogrusHandler { func NewSlogLogrusHandler() *SlogLogrusHandler {
rc := new(SlogLogrusHandler{ return new(SlogLogrusHandler{fields: make(log.Fields), groupPrefix: ""})
fields: make(log.Fields),
groupPrefix: "",
})
return rc
} }
// Enabled returns true if the specified log level is handled. // Enabled returns true if the specified log level is handled.
@@ -81,20 +83,18 @@ func (h *SlogLogrusHandler) Handle(ctx context.Context, r slog.Record) error {
// WithAttrs creates a new Handler from this one, with extra attributes. // WithAttrs creates a new Handler from this one, with extra attributes.
func (h *SlogLogrusHandler) WithAttrs(attrs []slog.Attr) slog.Handler { func (h *SlogLogrusHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
newh := new(SlogLogrusHandler{fields: make(log.Fields)}) newh := new(SlogLogrusHandler{fields: make(log.Fields), groupPrefix: h.groupPrefix})
maps.Copy(newh.fields, h.fields) maps.Copy(newh.fields, h.fields)
for _, a := range attrs { for _, a := range attrs {
newh.fields[a.Key] = a.Value.Any() newh.fields[a.Key] = a.Value.Any()
} }
newh.groupPrefix = h.groupPrefix
return newh return newh
} }
// WithGroup creates a new Handler from this one, with an extra group prefix. // WithGroup creates a new Handler from this one, with an extra group prefix.
func (h *SlogLogrusHandler) WithGroup(name string) slog.Handler { func (h *SlogLogrusHandler) WithGroup(name string) slog.Handler {
newh := new(SlogLogrusHandler{fields: make(log.Fields)}) newh := new(SlogLogrusHandler{fields: make(log.Fields), groupPrefix: h.groupPrefix + name + "."})
maps.Copy(newh.fields, h.fields) maps.Copy(newh.fields, h.fields)
newh.groupPrefix = h.groupPrefix + name + "."
return newh return newh
} }
@@ -160,6 +160,7 @@ func (lf *amLogFile) Close() error {
} }
// rotate closes the log file and moves it to a new name, shuffling the previously stored log files by the same amount. // rotate closes the log file and moves it to a new name, shuffling the previously stored log files by the same amount.
// N.B.: We must be holding lf.mutex.
func (lf *amLogFile) rotate() error { func (lf *amLogFile) rotate() error {
if lf.keep == 0 && lf.keepCompressed == 0 { if lf.keep == 0 && lf.keepCompressed == 0 {
return nil // degenerate case, keep the log file the same return nil // degenerate case, keep the log file the same
@@ -262,7 +263,9 @@ func (lf *amLogFile) tryRotate() {
if lf.curSize >= lf.maxSize { if lf.curSize >= lf.maxSize {
err := lf.rotate() err := lf.rotate()
if err != nil { if err != nil {
//log.Error("log rotation failed") log.SetOutput(os.Stderr)
log.Errorf("log rotation failed: %v", err)
log.SetOutput(lf)
} }
} }
lf.mutex.Unlock() lf.mutex.Unlock()
@@ -302,8 +305,7 @@ func (lf *amLogFile) open(path string) error {
// logScanner is a goroutine that monitors the log file to see when it needs rotating. // logScanner is a goroutine that monitors the log file to see when it needs rotating.
func logScanner(ctx context.Context, lf *amLogFile, done chan bool) { func logScanner(ctx context.Context, lf *amLogFile, done chan bool) {
d, _ := time.ParseDuration("10s") t := time.NewTicker(LOG_ROTATE_INTERVAL * time.Second)
t := time.NewTicker(d)
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@@ -319,8 +321,10 @@ func logScanner(ctx context.Context, lf *amLogFile, done chan bool) {
// SetupLogging sets up the log file based on the configuration data. // SetupLogging sets up the log file based on the configuration data.
func SetupLogging() func() { func SetupLogging() func() {
loglevel, err := log.ParseLevel(config.GlobalComputedConfig.LogLevel) loglevel, err := log.ParseLevel(config.GlobalComputedConfig.LogLevel)
if err != nil { if err == nil {
loglevel = log.ErrorLevel loglevel = log.ErrorLevel
} else {
log.Errorf("default log level not valid: %s (%v)", config.GlobalComputedConfig.LogLevel, err)
} }
if config.GlobalComputedConfig.DebugMode && loglevel != log.TraceLevel { if config.GlobalComputedConfig.DebugMode && loglevel != log.TraceLevel {
loglevel = log.DebugLevel loglevel = log.DebugLevel
@@ -333,7 +337,8 @@ func SetupLogging() func() {
amlog := new(amLogFile) amlog := new(amLogFile)
maxlog, err := humanize.ParseBytes(config.GlobalConfig.Logging.MaxLogSize) maxlog, err := humanize.ParseBytes(config.GlobalConfig.Logging.MaxLogSize)
if err != nil { if err != nil {
maxlog = 16 * 1024 * 1024 // default to 16 megabytes log.Errorf("invalid value for max log size: %s (%v)", config.GlobalConfig.Logging.MaxLogSize, err)
maxlog = DEFAULT_MAXLOG
} }
amlog.maxSize = int64(maxlog) amlog.maxSize = int64(maxlog)
amlog.keep = config.GlobalConfig.Logging.KeepLogFiles amlog.keep = config.GlobalConfig.Logging.KeepLogFiles
@@ -344,13 +349,14 @@ func SetupLogging() func() {
ctx, cancelfunc = context.WithCancel(context.Background()) ctx, cancelfunc = context.WithCancel(context.Background())
done = make(chan bool) done = make(chan bool)
go logScanner(ctx, amlog, done) go logScanner(ctx, amlog, done)
} else {
log.Errorf("**** failed to open amlog: %v - logs will go to stdout", err)
} }
} }
if logfile == nil { if logfile == nil {
log.SetOutput(os.Stdout) log.SetOutput(os.Stdout)
} else { } else {
log.SetOutput(logfile) log.SetOutput(logfile)
} }
log.SetLevel(loglevel) log.SetLevel(loglevel)
+22 -9
View File
@@ -20,6 +20,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"net"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
@@ -40,9 +41,23 @@ import (
// READ_HEADER_TIMEOUT is the timeout value for reading headers in seconds. (Deliberately NOT configurable because this is a security issue) // READ_HEADER_TIMEOUT is the timeout value for reading headers in seconds. (Deliberately NOT configurable because this is a security issue)
const READ_HEADER_TIMEOUT = 2 const READ_HEADER_TIMEOUT = 2
// GRACEFUL_SHUTDOWN_TIMEOUT is the timeout value for a graceful shutdown.
const GRACEFUL_SHUTDOWN_TIMEOUT = 10 * time.Second
// GetAndPost is used to have functions that respond to both GET and POST on a URI. // GetAndPost is used to have functions that respond to both GET and POST on a URI.
var GetAndPost = []string{http.MethodGet, http.MethodPost} var GetAndPost = []string{http.MethodGet, http.MethodPost}
// myIPAddress returns the IP address of this computer.
func myIPAddress() net.IP {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
panic(err)
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP
}
// setupEcho creates, configures, and returns a new Echo instance. // setupEcho creates, configures, and returns a new Echo instance.
func setupEcho() *echo.Echo { func setupEcho() *echo.Echo {
e := echo.New() e := echo.New()
@@ -225,11 +240,15 @@ var SystemStartTime time.Time
// main is Ye Olde Main Function. // main is Ye Olde Main Function.
func main() { func main() {
SystemStartTime = time.Now() SystemStartTime = time.Now()
// Determine my IP address.
myIP := myIPAddress()
// Configure the system. // Configure the system.
config.SetupConfig() config.SetupConfig()
closer := SetupLogging() closer := SetupLogging()
defer closer() defer closer()
closer, err := database.SetupDb() dbVersion, closer, err := database.SetupDb()
if err != nil { if err != nil {
panic(fmt.Sprintf("Database open failure: %v", err)) panic(fmt.Sprintf("Database open failure: %v", err))
} }
@@ -240,12 +259,6 @@ func main() {
closer = ui.SetupUILayer() closer = ui.SetupUILayer()
defer closer() defer closer()
// Determine my IP address and the admin user.
myIP, err := util.MyIPAddress()
if err != nil {
panic(err)
}
// Set up to trap SIGINT/SIGTERM and shut down gracefully // Set up to trap SIGINT/SIGTERM and shut down gracefully
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop() defer stop()
@@ -262,7 +275,7 @@ func main() {
// Audit the startup // Audit the startup
database.AmStoreAudit(database.AmNewAudit(database.AuditStartup, 0, myIP.String(), database.AmStoreAudit(database.AmNewAudit(database.AuditStartup, 0, myIP.String(),
fmt.Sprintf("version=%s", config.AMSTERDAM_VERSION))) fmt.Sprintf("version=%s", config.AMSTERDAM_VERSION), fmt.Sprintf("database=%s", dbVersion)))
defer func() { defer func() {
// Audit the shutdown // Audit the shutdown
database.AmStoreAudit(database.AmNewAudit(database.AuditShutdown, 0, myIP.String())) database.AmStoreAudit(database.AmNewAudit(database.AuditShutdown, 0, myIP.String()))
@@ -273,7 +286,7 @@ func main() {
Address: config.GlobalComputedConfig.Listen, Address: config.GlobalComputedConfig.Listen,
HideBanner: true, HideBanner: true,
HidePort: true, HidePort: true,
GracefulTimeout: 10 * time.Second, GracefulTimeout: GRACEFUL_SHUTDOWN_TIMEOUT,
OnShutdownError: func(err error) { OnShutdownError: func(err error) {
log.Fatalf("error in shutting down the server: %v", err) log.Fatalf("error in shutting down the server: %v", err)
}, },
+5 -5
View File
@@ -288,7 +288,7 @@ func JumpToShortcut(ctxt ui.AmContext) (string, any) {
return "error", echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("not found: %s", ctxt.URLParam("postlink"))).Wrap(err) return "error", echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("not found: %s", ctxt.URLParam("postlink"))).Wrap(err)
} }
scope, target := link.Classify() scope, target := link.Classify()
if scope != "global" { if scope != database.PLSCOPE_GLOBAL {
return "error", echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("not found: %s", ctxt.URLParam("postlink"))) return "error", echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("not found: %s", ctxt.URLParam("postlink")))
} }
if err = link.VerifyNames(ctxt.Ctx()); err != nil { if err = link.VerifyNames(ctxt.Ctx()); err != nil {
@@ -296,13 +296,13 @@ func JumpToShortcut(ctxt ui.AmContext) (string, any) {
} }
targetURL := "" targetURL := ""
switch target { switch target {
case "community": case database.PLCLASS_COMMUNITY:
targetURL = fmt.Sprintf("/comm/%s", link.Community) targetURL = fmt.Sprintf("/comm/%s", link.Community)
case "conference": case database.PLCLASS_CONFERENCE:
targetURL = fmt.Sprintf("/comm/%s/conf/%s", link.Community, link.Conference) targetURL = fmt.Sprintf("/comm/%s/conf/%s", link.Community, link.Conference)
case "topic": case database.PLCLASS_TOPIC:
targetURL = fmt.Sprintf("/comm/%s/conf/%s/r/%d", link.Community, link.Conference, link.Topic) targetURL = fmt.Sprintf("/comm/%s/conf/%s/r/%d", link.Community, link.Conference, link.Topic)
case "post", "postrange", "postopenrange": case database.PLCLASS_POST, database.PLCLASS_POSTRANGE, database.PLCLASS_POSTOPENRANGE:
targetURL = fmt.Sprintf("/comm/%s/conf/%s/r/%d?r=%d,%d", link.Community, link.Conference, link.Topic, link.FirstPost, link.LastPost) targetURL = fmt.Sprintf("/comm/%s/conf/%s/r/%d?r=%d,%d", link.Community, link.Conference, link.Topic, link.FirstPost, link.LastPost)
default: default:
return "error", fmt.Sprintf("invalid target '%s' for link: %s", target, ctxt.URLParam("postlink")) return "error", fmt.Sprintf("invalid target '%s' for link: %s", target, ctxt.URLParam("postlink"))
+20 -23
View File
@@ -18,7 +18,6 @@ import (
"net/http" "net/http"
"slices" "slices"
"sync" "sync"
"sync/atomic"
"time" "time"
"git.erbosoft.com/amy/amsterdam/config" "git.erbosoft.com/amy/amsterdam/config"
@@ -33,6 +32,12 @@ import (
be timed out as well as used to show the logged-in users. This is similar to the session support provided in J2EE servlets. be timed out as well as used to show the logged-in users. This is similar to the session support provided in J2EE servlets.
*/ */
// DEFAULT_SESSION_EXPIRE is the default time in which sessions will expire.
const DEFAULT_SESSION_EXPIRE = 1 * time.Hour
// The interval at which all sessions will be swept.
const SESSION_STORE_SWEEP_INTERVAL = 2 * time.Minute
// AmSessionOptions gives the options for the session. // AmSessionOptions gives the options for the session.
type AmSessionOptions struct { type AmSessionOptions struct {
Path string Path string
@@ -248,7 +253,6 @@ type amSessionStore struct {
sessions map[string]*amSession sessions map[string]*amSession
maxEntries int maxEntries int
expiry time.Duration expiry time.Duration
sweepRunning atomic.Bool
} }
// createAmSessionStore creates the session store. // createAmSessionStore creates the session store.
@@ -258,7 +262,6 @@ func createAmSessionStore(exp time.Duration) *amSessionStore {
maxEntries: 0, maxEntries: 0,
expiry: exp, expiry: exp,
} }
rc.sweepRunning.Store(true)
return rc return rc
} }
@@ -339,9 +342,15 @@ func (st *amSessionStore) SessionInfo() (int, []string, int) {
* tick - Channel that "pulses" periodically to run the task. * tick - Channel that "pulses" periodically to run the task.
* done - Channel we write to when we're done. * done - Channel we write to when we're done.
*/ */
func (st *amSessionStore) sweep(tick <-chan time.Time, done chan bool) { func (st *amSessionStore) sweep(ctx context.Context, done chan bool) {
for range tick { tkr := time.NewTicker(SESSION_STORE_SWEEP_INTERVAL)
if st.sweepRunning.Load() { for {
select {
case <-ctx.Done():
tkr.Stop()
done <- true
return
case <-tkr.C:
// phase 1 - identify expired sessions // phase 1 - identify expired sessions
st.mutex.RLock() st.mutex.RLock()
zap := make([]string, 0, len(st.sessions)) zap := make([]string, 0, len(st.sessions))
@@ -366,11 +375,8 @@ func (st *amSessionStore) sweep(tick <-chan time.Time, done chan bool) {
} }
st.mutex.Unlock() st.mutex.Unlock()
} }
} else {
break
} }
} }
done <- true
} }
// sessionStore is the global session store. // sessionStore is the global session store.
@@ -381,30 +387,21 @@ func setupSessionManager() func() {
// get the time for the session to expire // get the time for the session to expire
d, err := time.ParseDuration(config.GlobalConfig.Site.SessionExpire) d, err := time.ParseDuration(config.GlobalConfig.Site.SessionExpire)
if err != nil { if err != nil {
d, err = time.ParseDuration("1h") log.Errorf("invalid session timeout value: %s", config.GlobalConfig.Site.SessionExpire)
if err != nil { d = DEFAULT_SESSION_EXPIRE
panic(err.Error())
}
} }
// create session store // create session store
sessionStore = createAmSessionStore(d) sessionStore = createAmSessionStore(d)
// get the clock value to run sweeps
d, err = time.ParseDuration("1s")
if err != nil {
panic(err.Error())
}
// set up the sweep runner // set up the sweep runner
tkr := time.NewTicker(d) ctx, cancel := context.WithCancel(context.Background())
done := make(chan bool) done := make(chan bool)
go sessionStore.sweep(tkr.C, done) go sessionStore.sweep(ctx, done)
return func() { return func() {
// stop the sweep runner // stop the sweep runner
sessionStore.sweepRunning.Store(false) cancel()
<-done <-done
tkr.Stop()
} }
} }
+2 -2
View File
@@ -14,6 +14,7 @@ package ui
import ( import (
"embed" "embed"
"errors"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
@@ -120,8 +121,7 @@ func AmLoadDialog(name string) (*Dialog, error) {
f, err = extDialogs.Open(fmt.Sprintf("%s.yaml", name)) f, err = extDialogs.Open(fmt.Sprintf("%s.yaml", name))
if err != nil { if err != nil {
f = nil f = nil
pe := err.(*fs.PathError) if errors.Is(err, os.ErrInvalid) || errors.Is(err, os.ErrNotExist) {
if pe.Err == os.ErrInvalid || pe.Err == os.ErrNotExist {
err = nil err = nil
} }
} }
+1 -1
View File
@@ -97,7 +97,7 @@ func ValidateConference(next echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) error { return func(c *echo.Context) error {
ctxt := AmContextFromEchoContext(c) ctxt := AmContextFromEchoContext(c)
comm := ctxt.CurrentCommunity() // set by middleware comm := ctxt.CurrentCommunity() // set by middleware
b, err := database.AmTestService(c.Request().Context(), comm, "Conference") b, err := database.AmTestService(c.Request().Context(), comm, database.AM_SVC_CONFERENCE)
if err != nil { if err != nil {
return AmSendPageData(c, ctxt, "error", err) return AmSendPageData(c, ctxt, "error", err)
} }
+3
View File
@@ -309,6 +309,9 @@ func setupTemplates() {
views.AddGlobal("AmsterdamVersion", config.AMSTERDAM_VERSION) views.AddGlobal("AmsterdamVersion", config.AMSTERDAM_VERSION)
views.AddGlobal("AmsterdamCopyright", config.AMSTERDAM_COPYRIGHT) views.AddGlobal("AmsterdamCopyright", config.AMSTERDAM_COPYRIGHT)
views.AddGlobal("GlobalConfig", config.GlobalConfig) views.AddGlobal("GlobalConfig", config.GlobalConfig)
views.AddGlobal("PLSCOPE_COMMUNITY", database.PLSCOPE_COMMUNITY)
views.AddGlobal("PLSCOPE_CONFERENCE", database.PLSCOPE_CONFERENCE)
views.AddGlobal("PLSCOPE_TOPIC", database.PLSCOPE_TOPIC)
views.AddGlobalFunc("iif", immediateIf) views.AddGlobalFunc("iif", immediateIf)
views.AddGlobalFunc("postRewrite", postRewrite) views.AddGlobalFunc("postRewrite", postRewrite)
views.AddGlobalFunc("MakeIntRange", makeIntRange) views.AddGlobalFunc("MakeIntRange", makeIntRange)
+6 -6
View File
@@ -13,11 +13,11 @@
<div class="mb-2"> <div class="mb-2">
<div class=" flex items-baseline gap-2"> <div class=" flex items-baseline gap-2">
<h1 class="text-blue-800 text-4xl font-bold">Find Posts</h1> <h1 class="text-blue-800 text-4xl font-bold">Find Posts</h1>
{{ if scope == "community" }} {{ if scope == PLSCOPE_COMMUNITY }}
<span class="text-blue-800 text-xl font-bold ml-2">in Community: {{ entityName }}</span> <span class="text-blue-800 text-xl font-bold ml-2">in Community: {{ entityName }}</span>
{{ else if scope == "conference" }} {{ else if scope == PLSCOPE_CONFERENCE }}
<span class="text-blue-800 text-xl font-bold ml-2">in Conference: {{ entityName }}</span> <span class="text-blue-800 text-xl font-bold ml-2">in Conference: {{ entityName }}</span>
{{ else if scope == "topic" }} {{ else if scope == PLSCOPE_TOPIC }}
<span class="text-blue-800 text-xl font-bold ml-2">in Topic: {{ entityName | raw }}</span> <span class="text-blue-800 text-xl font-bold ml-2">in Topic: {{ entityName | raw }}</span>
{{ end }} {{ end }}
</div> </div>
@@ -26,11 +26,11 @@
<!-- Backlink --> <!-- Backlink -->
<div class="mb-4"> <div class="mb-4">
{{ if scope == "community" }} {{ if scope == PLSCOPE_COMMUNITY }}
<a class="text-blue-700 hover:text-blue-900 text-sm font-medium" href="{{ backlink }}">Return to Conference List</a> <a class="text-blue-700 hover:text-blue-900 text-sm font-medium" href="{{ backlink }}">Return to Conference List</a>
{{ else if scope == "conference" }} {{ else if scope == PLSCOPE_CONFERENCE }}
<a class="text-blue-700 hover:text-blue-900 text-sm font-medium" href="{{ backlink }}">Return to Topic List</a> <a class="text-blue-700 hover:text-blue-900 text-sm font-medium" href="{{ backlink }}">Return to Topic List</a>
{{ else if scope == "topic" }} {{ else if scope == PLSCOPE_TOPIC }}
<a class="text-blue-700 hover:text-blue-900 text-sm font-medium" href="{{ backlink }}">Return to Topic</a> <a class="text-blue-700 hover:text-blue-900 text-sm font-medium" href="{{ backlink }}">Return to Topic</a>
{{ end }} {{ end }}
</div> </div>
-12
View File
@@ -13,7 +13,6 @@
package util package util
import ( import (
"net"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@@ -172,17 +171,6 @@ func Map[A, B any](in []A, fn func(A) B) []B {
return rc return rc
} }
// MyIPAddress returns the local IP address of this machine.
func MyIPAddress() (net.IP, error) {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
return nil, err
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP, nil
}
// IIF is an "immediate-if" function returning its second argument if the first one is true, the third one if not. // IIF is an "immediate-if" function returning its second argument if the first one is true, the third one if not.
func IIF[A any](expr bool, v1, v2 A) A { func IIF[A any](expr bool, v1, v2 A) A {
if expr { if expr {