Compare commits
2 Commits
64161721bf
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a2c2a1f750 | |||
| 08a10a55dd |
+10
-7
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user