8 Commits

35 changed files with 581 additions and 439 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ import (
"git.erbosoft.com/amy/amsterdam/ui"
"git.erbosoft.com/amy/amsterdam/util"
"github.com/biter777/countries"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
)
// ENOJOIN is an error for not being permitted to join a community.
+1 -1
View File
@@ -28,7 +28,7 @@ import (
"git.erbosoft.com/amy/amsterdam/exports"
"git.erbosoft.com/amy/amsterdam/ui"
"git.erbosoft.com/amy/amsterdam/util"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
)
+3 -3
View File
@@ -26,7 +26,7 @@ import (
"git.erbosoft.com/amy/amsterdam/htmlcheck"
"git.erbosoft.com/amy/amsterdam/ui"
"github.com/CloudyKit/jet/v6"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
)
@@ -513,11 +513,11 @@ func ReadPosts(ctxt ui.AmContext) (string, any) {
resetLastRead := false
if ctxt.HasParameter("r") {
if err := breakRange(topic, postRange, ctxt.Parameter("r"), ","); err != nil {
return "error", echo.NewHTTPError(http.StatusNotFound).SetInternal(err)
return "error", echo.NewHTTPError(http.StatusNotFound, err.Error()).Wrap(err)
}
} else if ctxt.HasParameter("rgo") {
if err := breakRange(topic, postRange, ctxt.Parameter("rgo"), "-"); err != nil {
return "error", echo.NewHTTPError(http.StatusNotFound).SetInternal(err)
return "error", echo.NewHTTPError(http.StatusNotFound, err.Error()).Wrap(err)
}
} else {
postRange[0] = lastRead + 1
+1 -1
View File
@@ -26,7 +26,7 @@ import (
"git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/email"
"git.erbosoft.com/amy/amsterdam/ui"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
)
+22 -3
View File
@@ -100,7 +100,10 @@ type AmConfig struct {
SessionExpire string `yaml:"sessionExpire"`
UserAgreementResource string `yaml:"userAgreementResource"`
PolicyResource string `yaml:"policyResource"`
FrameTemplate string `yaml:"frameTemplate"`
FooterTemplate string `yaml:"footerTemplate"`
TopMenuId string `yaml:"topMenuId"`
FixedMenuId string `yaml:"fixedMenuId"`
DefaultCommunityLogo string `yaml:"defaultCommunityLogo"`
DefaultUserPhoto string `yaml:"defaultUserPhoto"`
WelcomeTitle string `yaml:"welcomeTitle"`
@@ -148,6 +151,9 @@ type AmConfig struct {
Prioritize string `yaml:"prioritize"`
} `yaml:"countryList"`
VeniceCompatibleImageURLs bool `yaml:"veniceCompatibleImageURLs"`
PanicRecovery struct {
StackDataSize string `yaml:"stackDataSize"`
} `yaml:"panicRecovery"`
} `yaml:"rendering"`
Resources struct {
ViewTemplateDir string `yaml:"viewTemplateDir"`
@@ -167,6 +173,13 @@ type AmConfig struct {
} `yaml:"posting"`
Tuning struct {
WorkerTasks int `yaml:"workerTasks"`
Timeouts struct {
HttpRead int `yaml:"httpRead"`
HttpWrite int `yaml:"httpWrite"`
HttpIdle int `yaml:"httpIdle"`
PageExecute int `yaml:"pageExecute"`
PageRender int `yaml:"pageRender"`
} `yaml:"timeouts"`
Queues struct {
AuditWrites int `yaml:"auditWrites"`
ContextRecycle int `yaml:"contextRecycle"`
@@ -200,7 +213,7 @@ func (c *AmConfig) ExPath(path string) string {
return filepath.Join(c.baseDir, path)
}
// AmConfigComputed is the configuration values which are "computed" based only on values in AmConfig.
// AmConfigComputed is the configuration values which are "computed" based only on values in AmConfig and CommandLine.
type AmConfigComputed struct {
DebugMode bool // are we in debug mode?
LogLevel string // the logging level
@@ -216,6 +229,7 @@ type AmConfigComputed struct {
MailAuthType string // SMTP auth type
MailUser string // SMTP user name
MailPassword string // SMTP password
PanicRecoveryStack int32 // stack size for panic recovery
UploadMaxSize int32 // maximum upload size in bytes
UploadNoCompress map[string]bool // which upload types are not compressed?
}
@@ -321,7 +335,7 @@ func overlayStructValue(dest, loaded, defaults reflect.Value) {
}
} else {
// if we see this message, this function needs more work
log.Errorf("*** unable to deal with field %s of type %s", structField.Name, typ.Name())
log.Fatalf("*** unable to deal with field %s of type %s", structField.Name, typ.Name())
}
}
}
@@ -398,7 +412,12 @@ func SetupConfig() {
GlobalComputedConfig.MailAuthType = util.IIF(CommandLine.MailAuthType != "", CommandLine.MailAuthType, GlobalConfig.Email.AuthType)
GlobalComputedConfig.MailUser = util.IIF(CommandLine.MailUser != "", CommandLine.MailUser, GlobalConfig.Email.User)
GlobalComputedConfig.MailPassword = util.IIF(CommandLine.MailPassword != "", CommandLine.MailPassword, GlobalConfig.Email.Password)
tmp, err := humanize.ParseBytes(GlobalConfig.Posting.Uploads.MaxSize)
tmp, err := humanize.ParseBytes(GlobalConfig.Rendering.PanicRecovery.StackDataSize)
if err != nil {
panic(err.Error())
}
GlobalComputedConfig.PanicRecoveryStack = int32(tmp)
tmp, err = humanize.ParseBytes(GlobalConfig.Posting.Uploads.MaxSize)
if err != nil {
panic(err.Error())
}
+11
View File
@@ -25,7 +25,10 @@ site:
sessionExpire: "3h"
userAgreementResource: "useragreement.html"
policyResource: "policy.html"
frameTemplate: "frame.jet"
footerTemplate: "footer.jet"
topMenuId: "top"
fixedMenuId: "fixed"
defaultCommunityLogo: "/img/builtin/default-community.jpg"
defaultUserPhoto: "/img/builtin/no-user.png"
welcomeTitle: "Welcome to Amsterdam"
@@ -72,6 +75,8 @@ rendering:
countryList:
prioritize: US
veniceCompatibleImageURLs: false
panicRecovery:
stackDataSize: "4 KiB"
resources:
viewTemplateDir: ""
dialogTemplateDir: ""
@@ -91,6 +96,12 @@ posting:
- "image/png"
tuning:
workerTasks: 4
timeouts:
httpRead: 30
httpWrite: 30
httpIdle: 120
pageExecute: 15
pageRender: 15
queues:
auditWrites: 16
contextRecycle: 16
+10 -7
View File
@@ -29,9 +29,9 @@ import (
// Error classifications
const (
classUnspecified = 0
classNeedInstall = 1
classNeedConvert = 2
classUnspecified = iota // unspecified, barf
classNeedInstall // need to install the database
classNeedConvert // need to convert a Venice database
)
// MySQL Errors
@@ -210,11 +210,11 @@ func prepareDB() (string, error) {
}
// SetupDb sets up the database and associated items.
func SetupDb() (func(), error) {
func SetupDb() (string, func(), error) {
exitfns := make([]func(), 0, 2)
version, err := prepareDB()
if err != nil {
return nil, err
return "X", nil, err
}
db, err := sqlx.Connect(config.GlobalComputedConfig.DatabaseDriver, buildMysqlDSN(false))
if err == nil {
@@ -223,6 +223,7 @@ func SetupDb() (func(), error) {
if err == nil {
if g.Version != version {
log.Warnf("!! database version %s does not match prepared version %s", g.Version, version)
version = g.Version
}
setupAdCache()
setupUserCache()
@@ -232,11 +233,11 @@ func SetupDb() (func(), error) {
setupConferenceCache()
exitfns = append(exitfns, setupAuditWriter())
exitfns = append(exitfns, setupIPBanSweep())
log.Infof("SetupDb(): database version %s", g.Version)
log.Infof("SetupDb(): database version %s", version)
}
}
slices.Reverse(exitfns)
return func() {
return version, func() {
for _, f := range exitfns {
f()
}
@@ -262,6 +263,8 @@ func transaction(ctx context.Context) (*sqlx.Tx, func() error, func()) {
err = tx.Commit()
if err == nil {
live = false
} else {
log.Errorf("***COMMIT ERROR*** %v", 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.
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
}
if scope == "conference" || scope == "community" || scope == "global" {
if scope == PLSCOPE_CONFERENCE || scope == PLSCOPE_COMMUNITY || scope == PLSCOPE_GLOBAL {
topic, err := AmGetTopic(ctx, p.TopicId)
if err != nil {
return "", err
@@ -720,55 +720,55 @@ func decodeSearchScope(ctx context.Context, scopeValues []any) (string, *Communi
}
if thisComm, ok := scopeValues[i].(*Community); ok {
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
continue
}
if thisConf, ok := scopeValues[i].(*Conference); ok {
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
continue
}
if thisTopic, ok := scopeValues[i].(*Topic); ok {
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
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.
if myComm == 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 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)
if err != nil {
return "error", nil, nil, nil, err
return PLSCOPE_ERROR, nil, nil, nil, err
}
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 {
return "conference", myComm, myConf, nil, nil
return PLSCOPE_CONFERENCE, myComm, myConf, nil, nil
}
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.
@@ -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.
confService, err := AmGetServiceIndex("community", "Conference")
confService, err := AmGetServiceIndex(AM_DOMAIN_COMMUNITY, AM_SVC_CONFERENCE)
if err != nil {
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.
var count int
switch scope {
case "global":
case PLSCOPE_GLOBAL:
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
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)
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)
case "community":
case PLSCOPE_COMMUNITY:
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
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)
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)
case "conference":
case PLSCOPE_CONFERENCE:
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
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)
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)
case "topic":
case PLSCOPE_TOPIC:
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
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.
var rs *sql.Rows
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
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
@@ -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
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)
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
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
@@ -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
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)
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
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
@@ -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
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)
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
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
+63 -43
View File
@@ -20,6 +20,25 @@ import (
"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.
type PostLinkData struct {
Community string
@@ -121,53 +140,53 @@ func (d *PostLinkData) Classify() (string, string) {
if d.FirstPost == -1 {
return "", ""
} else if d.LastPost == -1 {
return "topic", "postopenrange"
return PLSCOPE_TOPIC, PLCLASS_POSTOPENRANGE
} else if d.LastPost == d.FirstPost {
return "topic", "post"
return PLSCOPE_TOPIC, PLCLASS_POST
} else {
return "topic", "postrange"
return PLSCOPE_TOPIC, PLCLASS_POSTRANGE
}
} else {
if d.FirstPost == -1 {
return "conference", "topic"
return PLSCOPE_CONFERENCE, PLCLASS_TOPIC
} else if d.LastPost == -1 {
return "conference", "postopenrange"
return PLSCOPE_CONFERENCE, PLCLASS_POSTOPENRANGE
} else if d.LastPost == d.FirstPost {
return "conference", "post"
return PLSCOPE_CONFERENCE, PLCLASS_POST
} else {
return "conference", "postrange"
return PLSCOPE_CONFERENCE, PLCLASS_POSTRANGE
}
}
} else {
if d.Topic == -1 {
return "community", "conference"
return PLSCOPE_COMMUNITY, PLCLASS_CONFERENCE
} else {
if d.FirstPost == -1 {
return "community", "topic"
return PLSCOPE_COMMUNITY, PLCLASS_TOPIC
} else if d.LastPost == -1 {
return "community", "postopenrange"
return PLSCOPE_COMMUNITY, PLCLASS_POSTOPENRANGE
} else if d.LastPost == d.FirstPost {
return "community", "post"
return PLSCOPE_COMMUNITY, PLCLASS_POST
} else {
return "community", "postrange"
return PLSCOPE_COMMUNITY, PLCLASS_POSTRANGE
}
}
}
} else {
if d.Conference == "" {
return "global", "community"
return PLSCOPE_GLOBAL, PLCLASS_COMMUNITY
} else {
if d.Topic == -1 {
return "global", "conference"
return PLSCOPE_GLOBAL, PLCLASS_CONFERENCE
} else {
if d.FirstPost == -1 {
return "global", "topic"
return PLSCOPE_GLOBAL, PLCLASS_TOPIC
} else if d.LastPost == -1 {
return "global", "postopenrange"
return PLSCOPE_GLOBAL, PLCLASS_POSTOPENRANGE
} else if d.LastPost == d.FirstPost {
return "global", "post"
return PLSCOPE_GLOBAL, PLCLASS_POST
} else {
return "global", "postrange"
return PLSCOPE_GLOBAL, PLCLASS_POSTRANGE
}
}
}
@@ -276,25 +295,25 @@ func AmDecodePostLink(data string) (*PostLinkData, error) {
if len(data) > maxLinkLength {
return nil, errors.New("post link string too long")
}
rc := PostLinkData{
rc := new(PostLinkData{
Community: "",
Conference: "",
Topic: -1,
FirstPost: -1,
LastPost: -1,
}
})
work := data
// First test: Bang
pos := strings.IndexByte(work, '!')
if pos > 0 {
err := validateCommunity(work[:pos], &rc)
err := validateCommunity(work[:pos], rc)
if err != nil {
return nil, err
}
work = work[pos+1:]
if len(work) == 0 {
return &rc, nil // community link
return rc, nil // community link
}
} else if pos == 0 {
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"
var err error
if rc.Community == "" {
err = decodePostRange(work, &rc)
err = decodePostRange(work, rc)
} else {
err = validateConference(work, &rc)
err = validateConference(work, rc)
}
if err != nil {
return nil, err
}
return &rc, nil
return rc, nil
}
// Peel off the initial substring before the dot.
@@ -324,19 +343,19 @@ func AmDecodePostLink(data string) (*PostLinkData, error) {
var err error
if rc.Community == "" {
// it's either "conference." or "topic." - try the latter first
err = decodeTopicNumber(confOrTopic, &rc)
err = decodeTopicNumber(confOrTopic, rc)
if err != nil {
// it's not a topic number, try it as a conference name
err = validateConference(confOrTopic, &rc)
err = validateConference(confOrTopic, rc)
}
} else {
// it was "community!conference."
err = validateConference(confOrTopic, &rc)
err = validateConference(confOrTopic, rc)
}
if err != nil {
return nil, err
}
return &rc, nil
return rc, nil
}
// Third test: Dot #2
@@ -347,38 +366,38 @@ func AmDecodePostLink(data string) (*PostLinkData, error) {
if rc.Community == "" {
// either "conference.topic" or "topic.posts"
isTopic := false
err = decodeTopicNumber(confOrTopic, &rc)
err = decodeTopicNumber(confOrTopic, rc)
if err != nil {
// it's "conference.topic"
err = validateConference(confOrTopic, &rc)
err = validateConference(confOrTopic, rc)
isTopic = true
}
if err == nil {
if isTopic {
err = decodeTopicNumber(work, &rc)
err = decodeTopicNumber(work, rc)
} else {
err = decodePostRange(work, &rc)
err = decodePostRange(work, rc)
}
}
} else {
// we have "community!conference.topic"
err = validateConference(confOrTopic, &rc)
err = validateConference(confOrTopic, rc)
if err == nil {
err = decodeTopicNumber(work, &rc)
err = decodeTopicNumber(work, rc)
}
}
if err != nil {
return nil, err
}
return &rc, nil
return rc, nil
} else if pos == 0 {
return nil, errors.New("cannot have . at beginning of string")
}
// We definitely have "conference.topic.something" or "community!conference.topic.something"
err := validateConference(confOrTopic, &rc)
err := validateConference(confOrTopic, rc)
if err == nil {
err = decodeTopicNumber(work[:pos], &rc)
err = decodeTopicNumber(work[:pos], rc)
}
if err != nil {
return nil, err
@@ -386,22 +405,23 @@ func AmDecodePostLink(data string) (*PostLinkData, error) {
work = work[pos+1:]
if len(work) == 0 {
// 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 {
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 {
return &PostLinkData{
return new(PostLinkData{
Community: community,
CommId: commid,
Conference: conference,
Topic: topic,
FirstPost: -1,
LastPost: -1,
}
})
}
+23 -9
View File
@@ -25,6 +25,20 @@ import (
"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.
type ServiceVTable interface {
OnNewCommunity(context.Context, *sqlx.Tx, *Community) error
@@ -114,13 +128,13 @@ func init() {
serviceRoot.Domains[i].seqOrder = sqo
serviceRoot.byName[dom.DomainName] = &(serviceRoot.Domains[i])
}
dom := serviceRoot.byName["community"]
dom := serviceRoot.byName[AM_DOMAIN_COMMUNITY]
empty := emptyServiceVTable{}
dom.byId["Profile"].vtable = &empty
dom.byId["Admin"].vtable = &empty
dom.byId["SysAdmin"].vtable = &empty
dom.byId["Conference"].vtable = &(conferenceServiceVTable{})
dom.byId["Members"].vtable = &empty
dom.byId[AM_SVC_PROFILE].vtable = &empty
dom.byId[AM_SVC_ADMIN].vtable = &empty
dom.byId[AM_SVC_SYSADMIN].vtable = &empty
dom.byId[AM_SVC_CONFERENCE].vtable = &(conferenceServiceVTable{})
dom.byId[AM_SVC_MEMBERS].vtable = &empty
}
// setupServicesCache sets up the services cache.
@@ -166,7 +180,7 @@ func AmGetCommunityServices(ctx context.Context, cid int32) ([]*ServiceDef, erro
if err != nil {
return nil, err
}
dom := serviceRoot.byName["community"]
dom := serviceRoot.byName[AM_DOMAIN_COMMUNITY]
a := make([]*ServiceDef, 0, len(dom.Services))
for rs.Next() {
var ndx int16
@@ -198,7 +212,7 @@ func AmGetCommunityServicesTx(ctx context.Context, tx *sqlx.Tx, cid int32) ([]*S
if err != nil {
return nil, err
}
dom := serviceRoot.byName["community"]
dom := serviceRoot.byName[AM_DOMAIN_COMMUNITY]
a := make([]*ServiceDef, 0, len(dom.Services))
for rs.Next() {
var ndx int16
@@ -222,7 +236,7 @@ func AmGetCommunityServicesTx(ctx context.Context, tx *sqlx.Tx, cid int32) ([]*S
* Standard Go error status.
*/
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))
for i, svc := range dom.Services {
if svc.Default {
+2 -2
View File
@@ -44,10 +44,10 @@ type Topic struct {
// Link returns a link string to this topic.
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
}
if scope == "community" || scope == "global" {
if scope == PLSCOPE_COMMUNITY || scope == PLSCOPE_GLOBAL {
conf, err := AmGetConference(ctx, t.ConfId)
if err == nil {
var plink string
+3
View File
@@ -234,6 +234,9 @@ func SetupMailSender() func() {
emailRenderer.AddGlobal("AmsterdamVersion", config.AMSTERDAM_VERSION)
emailRenderer.AddGlobal("AmsterdamCopyright", config.AMSTERDAM_COPYRIGHT)
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.
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
"{{ comm.JoinKey }}". You will then be able to take part in the conferences that are
going on in the community.
{{ if mode == "conference" }}
{{ if mode == INVMODE_CONFERENCE }}
After you've joined the "{{ comm.Name }}" community, check out the
"{{ conf.Name }}" conference. To find it, after joining the community,
click "Conferences" on the left menu bar, then click on the
"{{ 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
"{{ topic.Name | raw }}" topic in the "{{ conf.Name }}" conference. To find it,
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"
button. You will then be able to take part in the conferences that are
going on in the community.
{{ if mode == "conference" }}
{{ if mode == INVMODE_CONFERENCE }}
After you've joined the "{{ comm.Name }}" community, check out the
"{{ conf.Name }}" conference. To find it, after joining the community,
click "Conferences" on the left menu bar, then click on the
"{{ 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
"{{ topic.Name | raw }}" topic in the "{{ conf.Name }}" conference. To find it,
after joining the community, click "Conferences" on the left menu bar, then
+7 -8
View File
@@ -19,10 +19,9 @@ import (
"git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/ui"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/labstack/echo/v5"
"github.com/labstack/echo/v5/middleware"
log "github.com/sirupsen/logrus"
"golang.org/x/time/rate"
)
// EBUTTON is the standard error for an unknown button.
@@ -74,9 +73,9 @@ func AmNotFoundHandler(ctxt ui.AmContext) (string, any) {
* err - The error to be handled.
* c - The Echo context error is being handled on.
*/
func AmErrorHandler(err error, c echo.Context) {
func AmErrorHandler(c *echo.Context, err error) {
log.Infof("-> AmErrorHandler on path %s", c.Request().URL.Path)
if c.Response().Committed {
if c.Response().(*echo.Response).Committed {
return
}
cerr := ui.AmWithTempContext(c, func(ctxt ui.AmContext) (string, any) {
@@ -88,14 +87,14 @@ func AmErrorHandler(err error, c echo.Context) {
}
// rateLimitErrorHandler is called if there's an error getting the identifier for a connection (unlikely).
func rateLimitErrorHandler(c echo.Context, err error) error {
func rateLimitErrorHandler(c *echo.Context, err error) error {
return ui.AmWithTempContext(c, func(ctxt ui.AmContext) (string, any) {
return "error", err
})
}
// rateLimitDenyHandler is called if the rate limit is exceeded by a connection.
func rateLimitDenyHandler(c echo.Context, identifier string, err error) error {
func rateLimitDenyHandler(c *echo.Context, identifier string, err error) error {
return ui.AmWithTempContext(c, func(ctxt ui.AmContext) (string, any) {
ctxt.VarMap().Set("identifier", identifier)
return "ratelimit", err
@@ -105,7 +104,7 @@ func rateLimitDenyHandler(c echo.Context, identifier string, err error) error {
// AmSetupRateLimiter sets up the rate-limiting middleware.
func AmSetupRateLimiter() echo.MiddlewareFunc {
rcfg := middleware.RateLimiterMemoryStoreConfig{
Rate: rate.Limit(config.GlobalConfig.Site.RateLimit.Rate),
Rate: config.GlobalConfig.Site.RateLimit.Rate,
Burst: config.GlobalConfig.Site.RateLimit.Burst,
ExpiresIn: time.Duration(config.GlobalConfig.Site.RateLimit.ExpireMinutes) * time.Minute,
}
+6 -6
View File
@@ -330,7 +330,7 @@ func commonFindGetBackend(ctxt ui.AmContext) (string, any) {
*/
func FindPostsPageCommunity(ctxt ui.AmContext) (string, any) {
comm := ctxt.CurrentCommunity()
ctxt.VarMap().Set("scope", "community")
ctxt.VarMap().Set("scope", database.PLSCOPE_COMMUNITY)
ctxt.VarMap().Set("entityName", comm.Name)
ctxt.VarMap().Set("backlink", fmt.Sprintf("/comm/%s/conf", 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) {
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("backlink", ctxt.GetScratch("ConferenceLink").(string))
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) {
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("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))
@@ -417,7 +417,7 @@ func commonFindPostBackend(ctxt ui.AmContext, comm *database.Community, conf *da
*/
func FindPostsCommunity(ctxt ui.AmContext) (string, any) {
comm := ctxt.CurrentCommunity()
ctxt.VarMap().Set("scope", "community")
ctxt.VarMap().Set("scope", database.PLSCOPE_COMMUNITY)
ctxt.VarMap().Set("entityName", comm.Name)
ctxt.VarMap().Set("backlink", fmt.Sprintf("/comm/%s/conf", 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) {
comm := ctxt.CurrentCommunity()
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("backlink", ctxt.GetScratch("ConferenceLink").(string))
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()
conf := ctxt.GetScratch("currentConference").(*database.Conference)
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("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))
+7 -8
View File
@@ -14,13 +14,12 @@ require (
github.com/hashicorp/golang-lru v1.0.2
github.com/jmoiron/sqlx v1.4.0
github.com/klauspost/lctime v0.1.0
github.com/labstack/echo/v4 v4.15.1
github.com/labstack/gommon v0.4.2
github.com/labstack/echo/v5 v5.1.1
github.com/labstack/gommon v0.5.0
github.com/sirupsen/logrus v1.9.4
github.com/tkuchiki/go-timezone v0.2.3
golang.org/x/net v0.52.0
golang.org/x/text v0.35.0
golang.org/x/time v0.15.0
golang.org/x/net v0.53.0
golang.org/x/text v0.36.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -29,10 +28,10 @@ require (
github.com/CloudyKit/fastprinter v0.0.0-20251202014920-1725d2651bd4 // indirect
github.com/alexflint/go-scalar v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/image v0.37.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/time v0.15.0 // indirect
)
+12 -15
View File
@@ -31,16 +31,16 @@ github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/klauspost/lctime v0.1.0 h1:nINsuFc860M9cyYhT6vfg6U1USh7kiVBj/s/2b04U70=
github.com/klauspost/lctime v0.1.0/go.mod h1:OwdMhr8tbQvusAsnilqkkgDQqivWlqyg0w5cfXkLiDk=
github.com/labstack/echo/v4 v4.15.1 h1:S9keusg26gZpjMmPqB5hOEvNKnmd1lNmcHrbbH2lnFs=
github.com/labstack/echo/v4 v4.15.1/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/labstack/echo/v5 v5.1.1 h1:4QkvKoS8ps5ch49t8b72QS9Z581ytgxhTzxuB/CBA2I=
github.com/labstack/echo/v5 v5.1.1/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo=
github.com/labstack/gommon v0.5.0 h1:6VSQ2NOzsnEJ5W6+84E0RbcaDDmgB6NIAzWCczTEe6c=
github.com/labstack/gommon v0.5.0/go.mod h1:Rzlg7HHy1maLfzBYGg9NZcVuz1sA68HHhLjhcEllYE0=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -56,19 +56,16 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA=
golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+6 -10
View File
@@ -1,6 +1,6 @@
/*
* 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
* 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.
func (d *TrieDictionary) CheckWord(word string) bool {
d.mutex.Lock()
defer d.mutex.Unlock()
_, rc := d.trie.Find(strings.ToLower(word))
d.mutex.Unlock()
return rc
}
// AddWord adds a new word to the dictionary.
func (d *TrieDictionary) AddWord(word string) {
d.mutex.Lock()
defer d.mutex.Unlock()
d.trie.Add(strings.ToLower(word), true)
d.count++
d.mutex.Unlock()
}
// 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).
func LoadTrieDict(words []byte) *TrieDictionary {
rc := TrieDictionary{
loaded: atomic.Bool{},
trie: trie.New(),
count: 0,
}
rc := new(TrieDictionary{loaded: atomic.Bool{}, trie: trie.New(), count: 0})
rc.loaded.Store(false)
go loadDict(&rc, words)
return &rc
go loadDict(rc, words)
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)
}
}
rw := spellingRewriter{
dict: NewCompositeDict(dicts),
}
rewriterRegistry[rw.Name()] = &rw
rw := new(spellingRewriter{dict: NewCompositeDict(dicts)})
rewriterRegistry[rw.Name()] = rw
}
// 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) {
return nil
}
return &markupData{
return new(markupData{
beginMarkup: defaultBeginError,
text: data,
endMarkup: defaultEndError,
rescan: false,
}
})
}
+13 -3
View File
@@ -21,6 +21,13 @@ import (
"git.erbosoft.com/amy/amsterdam/ui"
)
// Invitation modes.
const (
INVMODE_COMMUNITY = "community"
INVMODE_CONFERENCE = "conference"
INVMODE_TOPIC = "topic"
)
/* InviteToCommunity displays the community invitation form.
* Parameters:
* ctxt - The AmContext for the request.
@@ -116,7 +123,7 @@ func InviteSend(ctxt ui.AmContext) (string, any) {
} else {
return "error", EPARAM
}
mode := "community"
mode := INVMODE_COMMUNITY
var conf *database.Conference = nil
var topic *database.Topic = nil
if ctxt.FormFieldIsSet("confid") {
@@ -145,9 +152,9 @@ func InviteSend(ctxt ui.AmContext) (string, any) {
if err != nil {
return "errors", err
}
mode = "topic"
mode = INVMODE_TOPIC
} else {
mode = "conference"
mode = INVMODE_CONFERENCE
}
}
addr := ctxt.FormField("addr")
@@ -168,6 +175,9 @@ func InviteSend(ctxt ui.AmContext) (string, any) {
mailMessage.SetTemplate("invite_private.jet")
}
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("conf", conf)
mailMessage.AddVariable("topic", topic)
+74 -93
View File
@@ -13,104 +13,90 @@
package main
import (
"bufio"
"bytes"
"compress/gzip"
"context"
"errors"
"fmt"
"io"
"log/slog"
"maps"
"os"
"path/filepath"
"strings"
"sync"
"time"
"git.erbosoft.com/amy/amsterdam/config"
"github.com/dustin/go-humanize"
"github.com/labstack/echo/v4"
glog "github.com/labstack/gommon/log"
"github.com/labstack/echo/v5"
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
/*----------------------------------------------------------------------------
* Gommon-log to logrus adapter
* slog handler that outputs to Logrus
*----------------------------------------------------------------------------
*/
/* toglog converts a Logrus logging level to a glog one.
* Parameters:
* l - The Logrus log level to be converted.
* Returns:
* The equivalent glog log level.
*/
func toglog(l log.Level) glog.Lvl {
switch l {
case log.DebugLevel:
return glog.DEBUG
case log.InfoLevel:
return glog.INFO
case log.WarnLevel:
return glog.WARN
case log.ErrorLevel:
return glog.ERROR
default:
return glog.OFF
}
// slog2logrus converts slog levels to Logrus levels.
var slog2logrus = map[slog.Level]log.Level{
slog.LevelDebug: log.DebugLevel,
slog.LevelInfo: log.InfoLevel,
slog.LevelWarn: log.WarnLevel,
slog.LevelError: log.ErrorLevel,
}
/* fromglog converts a glog logging level to a Logrus one.
* Parameters:
* l - The glog log level to be converted.
* Returns:
* The equivalent Logrus log level.
*/
func fromglog(l glog.Lvl) log.Level {
switch l {
case glog.DEBUG:
return log.DebugLevel
case glog.INFO:
return log.InfoLevel
case glog.WARN:
return log.WarnLevel
case glog.ERROR:
return log.ErrorLevel
default:
return log.PanicLevel
}
// SlogLogrusHandler implements slog.Handler and routes to Logrus.
type SlogLogrusHandler struct {
fields log.Fields // fields defined in this handler
groupPrefix string // group prefix
}
// EchoLogrusAdapter implements echo.Logger using logrus.
type EchoLogrusAdapter struct{}
// NewSlogLogrusHandler creates a SlogLogrusHandler with base information.
func NewSlogLogrusHandler() *SlogLogrusHandler {
return new(SlogLogrusHandler{fields: make(log.Fields), groupPrefix: ""})
}
func (l *EchoLogrusAdapter) Output() io.Writer { return log.StandardLogger().Out }
func (l *EchoLogrusAdapter) SetOutput(w io.Writer) { log.SetOutput(w) }
func (l *EchoLogrusAdapter) Prefix() string { return "" }
func (l *EchoLogrusAdapter) SetPrefix(p string) {}
func (l *EchoLogrusAdapter) Level() glog.Lvl { return toglog(log.GetLevel()) }
func (l *EchoLogrusAdapter) SetLevel(lvl glog.Lvl) { log.SetLevel(fromglog(lvl)) }
func (l *EchoLogrusAdapter) Print(i ...any) { log.Print(i...) }
func (l *EchoLogrusAdapter) Printf(format string, args ...any) { log.Printf(format, args...) }
func (l *EchoLogrusAdapter) Printj(j glog.JSON) { log.WithFields(log.Fields(j)).Print() }
func (l *EchoLogrusAdapter) Debug(i ...any) { log.Debug(i...) }
func (l *EchoLogrusAdapter) Debugf(format string, args ...any) { log.Debugf(format, args...) }
func (l *EchoLogrusAdapter) Debugj(j glog.JSON) { log.WithFields(log.Fields(j)).Debug() }
func (l *EchoLogrusAdapter) Info(i ...any) { log.Info(i...) }
func (l *EchoLogrusAdapter) Infof(format string, args ...any) { log.Infof(format, args...) }
func (l *EchoLogrusAdapter) Infoj(j glog.JSON) { log.WithFields(log.Fields(j)).Info() }
func (l *EchoLogrusAdapter) Warn(i ...any) { log.Warn(i...) }
func (l *EchoLogrusAdapter) Warnf(format string, args ...any) { log.Warnf(format, args...) }
func (l *EchoLogrusAdapter) Warnj(j glog.JSON) { log.WithFields(log.Fields(j)).Warn() }
func (l *EchoLogrusAdapter) Error(i ...any) { log.Error(i...) }
func (l *EchoLogrusAdapter) Errorf(format string, args ...any) { log.Errorf(format, args...) }
func (l *EchoLogrusAdapter) Errorj(j glog.JSON) { log.WithFields(log.Fields(j)).Error() }
func (l *EchoLogrusAdapter) Fatal(i ...any) { log.Fatal(i...) }
func (l *EchoLogrusAdapter) Fatalf(format string, args ...any) { log.Fatalf(format, args...) }
func (l *EchoLogrusAdapter) Fatalj(j glog.JSON) { log.WithFields(log.Fields(j)).Fatal() }
func (l *EchoLogrusAdapter) Panic(i ...any) { log.Panic(i...) }
func (l *EchoLogrusAdapter) Panicf(format string, args ...any) { log.Panicf(format, args...) }
func (l *EchoLogrusAdapter) Panicj(j glog.JSON) { log.WithFields(log.Fields(j)).Panic() }
func (l *EchoLogrusAdapter) SetHeader(h string) {}
// Enabled returns true if the specified log level is handled.
func (h *SlogLogrusHandler) Enabled(ctx context.Context, lvl slog.Level) bool {
return log.IsLevelEnabled(slog2logrus[lvl])
}
// Handle sends a slog.Record to the log output.
func (h *SlogLogrusHandler) Handle(ctx context.Context, r slog.Record) error {
flds := make(log.Fields)
for k, v := range h.fields {
flds[h.groupPrefix+k] = v
}
r.Attrs(func(a slog.Attr) bool {
flds[h.groupPrefix+a.Key] = a.Value.Any()
return true
})
ntry := log.NewEntry(log.StandardLogger()).WithTime(r.Time).WithFields(flds)
ntry.Log(slog2logrus[r.Level], r.Message)
return nil
}
// WithAttrs creates a new Handler from this one, with extra attributes.
func (h *SlogLogrusHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
newh := new(SlogLogrusHandler{fields: make(log.Fields), groupPrefix: h.groupPrefix})
maps.Copy(newh.fields, h.fields)
for _, a := range attrs {
newh.fields[a.Key] = a.Value.Any()
}
return newh
}
// WithGroup creates a new Handler from this one, with an extra group prefix.
func (h *SlogLogrusHandler) WithGroup(name string) slog.Handler {
newh := new(SlogLogrusHandler{fields: make(log.Fields), groupPrefix: h.groupPrefix + name + "."})
maps.Copy(newh.fields, h.fields)
return newh
}
/*----------------------------------------------------------------------------
* Echo middleware adapters
@@ -119,13 +105,13 @@ func (l *EchoLogrusAdapter) SetHeader(h string) {}
// LogrusMiddleware installs Logrus logging into the Echo middleware chain.
func LogrusMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return func(c *echo.Context) error {
start := time.Now()
err := next(c)
stop := time.Now()
req := c.Request()
res := c.Response()
res := c.Response().(*echo.Response)
log.WithFields(log.Fields{
"remote_ip": c.RealIP(),
@@ -139,17 +125,6 @@ func LogrusMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
}
}
// LogrusPanicLogging is a log function hooked into the recovery middleware.
func LogrusPanicLogging(c echo.Context, err error, stack []byte) error {
log.Errorf("[PANIC RECOVERY] %v", err)
scanner := bufio.NewScanner(bytes.NewReader(stack))
for scanner.Scan() {
line := strings.ReplaceAll(scanner.Text(), "\t", " ")
log.Error(line)
}
return scanner.Err()
}
/*----------------------------------------------------------------------------
* Log output file implementation
*----------------------------------------------------------------------------
@@ -185,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.
// N.B.: We must be holding lf.mutex.
func (lf *amLogFile) rotate() error {
if lf.keep == 0 && lf.keepCompressed == 0 {
return nil // degenerate case, keep the log file the same
@@ -287,7 +263,9 @@ func (lf *amLogFile) tryRotate() {
if lf.curSize >= lf.maxSize {
err := lf.rotate()
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()
@@ -327,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.
func logScanner(ctx context.Context, lf *amLogFile, done chan bool) {
d, _ := time.ParseDuration("10s")
t := time.NewTicker(d)
t := time.NewTicker(LOG_ROTATE_INTERVAL * time.Second)
for {
select {
case <-ctx.Done():
@@ -344,8 +321,10 @@ func logScanner(ctx context.Context, lf *amLogFile, done chan bool) {
// SetupLogging sets up the log file based on the configuration data.
func SetupLogging() func() {
loglevel, err := log.ParseLevel(config.GlobalComputedConfig.LogLevel)
if err != nil {
if err == nil {
loglevel = log.ErrorLevel
} else {
log.Errorf("default log level not valid: %s (%v)", config.GlobalComputedConfig.LogLevel, err)
}
if config.GlobalComputedConfig.DebugMode && loglevel != log.TraceLevel {
loglevel = log.DebugLevel
@@ -358,7 +337,8 @@ func SetupLogging() func() {
amlog := new(amLogFile)
maxlog, err := humanize.ParseBytes(config.GlobalConfig.Logging.MaxLogSize)
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.keep = config.GlobalConfig.Logging.KeepLogFiles
@@ -369,13 +349,14 @@ func SetupLogging() func() {
ctx, cancelfunc = context.WithCancel(context.Background())
done = make(chan bool)
go logScanner(ctx, amlog, done)
} else {
log.Errorf("**** failed to open amlog: %v - logs will go to stdout", err)
}
}
if logfile == nil {
log.SetOutput(os.Stdout)
} else {
log.SetOutput(logfile)
}
log.SetLevel(loglevel)
+53 -22
View File
@@ -17,7 +17,10 @@ package main
import (
"context"
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
@@ -30,24 +33,40 @@ import (
"git.erbosoft.com/amy/amsterdam/htmlcheck"
"git.erbosoft.com/amy/amsterdam/ui"
"git.erbosoft.com/amy/amsterdam/util"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/labstack/echo/v5"
"github.com/labstack/echo/v5/middleware"
log "github.com/sirupsen/logrus"
)
// 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
// 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.
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.
func setupEcho() *echo.Echo {
e := echo.New()
e.HideBanner = true
e.Logger = &EchoLogrusAdapter{}
e.Logger = slog.New(NewSlogLogrusHandler())
e.Renderer = &ui.TemplateRenderer{}
e.HTTPErrorHandler = AmErrorHandler
if !config.CommandLine.DebugPanic {
e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{
LogErrorFunc: LogrusPanicLogging,
StackSize: int(config.GlobalComputedConfig.PanicRecoveryStack),
}))
} else {
log.Warn("WARNING: --debug-panic in effect - DO NOT use this in production!")
@@ -215,17 +234,21 @@ func setupEcho() *echo.Echo {
// ampool is the worker pool for one-shot background tasks.
var ampool *util.WorkerPool
// SystemStartTime records the time since the system was started.
// SystemStartTime records the time the system was started.
var SystemStartTime time.Time
// main is Ye Olde Main Function.
func main() {
SystemStartTime = time.Now()
// Determine my IP address.
myIP := myIPAddress()
// Configure the system.
config.SetupConfig()
closer := SetupLogging()
defer closer()
closer, err := database.SetupDb()
dbVersion, closer, err := database.SetupDb()
if err != nil {
panic(fmt.Sprintf("Database open failure: %v", err))
}
@@ -236,12 +259,6 @@ func main() {
closer = ui.SetupUILayer()
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
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
@@ -258,27 +275,41 @@ func main() {
// Audit the startup
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() {
// Audit the shutdown
database.AmStoreAudit(database.AmNewAudit(database.AuditShutdown, 0, myIP.String()))
}()
// Set up the start configuration.
sconf := echo.StartConfig{
Address: config.GlobalComputedConfig.Listen,
HideBanner: true,
HidePort: true,
GracefulTimeout: GRACEFUL_SHUTDOWN_TIMEOUT,
OnShutdownError: func(err error) {
log.Fatalf("error in shutting down the server: %v", err)
},
BeforeServeFunc: func(s *http.Server) error {
s.ReadTimeout = time.Duration(config.GlobalConfig.Tuning.Timeouts.HttpRead) * time.Second
s.WriteTimeout = time.Duration(config.GlobalConfig.Tuning.Timeouts.HttpWrite) * time.Second
s.IdleTimeout = time.Duration(config.GlobalConfig.Tuning.Timeouts.HttpIdle) * time.Second
s.ReadHeaderTimeout = READ_HEADER_TIMEOUT * time.Second
return nil
},
}
stime := time.Since(SystemStartTime)
log.Infof("Amsterdam %s startup sequence completed in %v", config.AMSTERDAM_VERSION, stime)
// Start server
go func() {
if err := e.Start(config.GlobalComputedConfig.Listen); err != nil && err != http.ErrServerClosed {
e.Logger.Fatalf("shutting down the server: %v", err)
if err := sconf.Start(ctx, e); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("shutting down the server: %v", err)
}
}()
// Wait for the interrupt signal and then gracefully shut the server down.
// Wait for the context to be done, when the server is shut down.
<-ctx.Done()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := e.Shutdown(ctx); err != nil {
e.Logger.Fatal(err)
}
log.Infof("Amsterdam shut down")
}
+8 -8
View File
@@ -24,7 +24,7 @@ import (
"git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/ui"
"github.com/CloudyKit/jet/v6"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
)
@@ -285,24 +285,24 @@ func PolicyPage(ctxt ui.AmContext) (string, any) {
func JumpToShortcut(ctxt ui.AmContext) (string, any) {
link, err := database.AmDecodePostLink(ctxt.URLParam("postlink"))
if err != nil {
return "error", echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("not found: %s", ctxt.URLParam("postlink"))).SetInternal(err)
return "error", echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("not found: %s", ctxt.URLParam("postlink"))).Wrap(err)
}
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")))
}
if err = link.VerifyNames(ctxt.Ctx()); err != nil {
return "error", echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("not found: %s", ctxt.URLParam("postlink"))).SetInternal(err)
return "error", echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("not found: %s", ctxt.URLParam("postlink"))).Wrap(err)
}
targetURL := ""
switch target {
case "community":
case database.PLCLASS_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)
case "topic":
case database.PLCLASS_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)
default:
return "error", fmt.Sprintf("invalid target '%s' for link: %s", target, ctxt.URLParam("postlink"))
+6 -6
View File
@@ -27,7 +27,7 @@ import (
"git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/util"
"github.com/CloudyKit/jet/v6"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
)
@@ -98,7 +98,7 @@ type AmContext interface {
// amContext is the internal structure that implements AmContext.
type amContext struct {
echoContext echo.Context
echoContext *echo.Context
rendervars jet.VarMap
frameTitle string
frameMeta map[int]map[string]string
@@ -243,7 +243,7 @@ func (c *amContext) FormFieldIsSet(name string) bool {
// FormFieldValues returns all values for a specified parameter name.
func (c *amContext) FormFieldValues(name string) ([]string, error) {
vals, err := c.echoContext.FormParams()
vals, err := c.echoContext.FormValues()
if err != nil {
return make([]string, 0), err
}
@@ -525,7 +525,7 @@ var amContextRecycleBin chan *amContext
* Internal Amsterdam context structure pointer, or nil.
* Standard Go error status.
*/
func newContext(ctxt echo.Context) (*amContext, error) {
func newContext(ctxt *echo.Context) (*amContext, error) {
var rc *amContext
tmp := freeContext.Get()
if tmp == nil {
@@ -593,7 +593,7 @@ func newContext(ctxt echo.Context) (*amContext, error) {
* Returns:
* The associated AmContext.
*/
func AmContextFromEchoContext(ctxt echo.Context) AmContext {
func AmContextFromEchoContext(ctxt *echo.Context) AmContext {
myctxt := ctxt.Get("__amsterdam_context")
if myctxt != nil {
rc, ok := myctxt.(*amContext)
@@ -641,7 +641,7 @@ func setupContext() func() {
// ContextCreator is middleware that creates and recycles the AmContext.
func ContextCreator(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return func(c *echo.Context) error {
myctxt, err := newContext(c)
if err == nil {
err = next(c)
+22 -25
View File
@@ -18,12 +18,11 @@ import (
"net/http"
"slices"
"sync"
"sync/atomic"
"time"
"git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/database"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
)
@@ -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.
*/
// 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.
type AmSessionOptions struct {
Path string
@@ -248,7 +253,6 @@ type amSessionStore struct {
sessions map[string]*amSession
maxEntries int
expiry time.Duration
sweepRunning atomic.Bool
}
// createAmSessionStore creates the session store.
@@ -258,7 +262,6 @@ func createAmSessionStore(exp time.Duration) *amSessionStore {
maxEntries: 0,
expiry: exp,
}
rc.sweepRunning.Store(true)
return rc
}
@@ -339,9 +342,15 @@ func (st *amSessionStore) SessionInfo() (int, []string, int) {
* tick - Channel that "pulses" periodically to run the task.
* done - Channel we write to when we're done.
*/
func (st *amSessionStore) sweep(tick <-chan time.Time, done chan bool) {
for range tick {
if st.sweepRunning.Load() {
func (st *amSessionStore) sweep(ctx context.Context, done chan bool) {
tkr := time.NewTicker(SESSION_STORE_SWEEP_INTERVAL)
for {
select {
case <-ctx.Done():
tkr.Stop()
done <- true
return
case <-tkr.C:
// phase 1 - identify expired sessions
st.mutex.RLock()
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()
}
} else {
break
}
}
done <- true
}
// sessionStore is the global session store.
@@ -381,36 +387,27 @@ func setupSessionManager() func() {
// get the time for the session to expire
d, err := time.ParseDuration(config.GlobalConfig.Site.SessionExpire)
if err != nil {
d, err = time.ParseDuration("1h")
if err != nil {
panic(err.Error())
}
log.Errorf("invalid session timeout value: %s", config.GlobalConfig.Site.SessionExpire)
d = DEFAULT_SESSION_EXPIRE
}
// create session store
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
tkr := time.NewTicker(d)
ctx, cancel := context.WithCancel(context.Background())
done := make(chan bool)
go sessionStore.sweep(tkr.C, done)
go sessionStore.sweep(ctx, done)
return func() {
// stop the sweep runner
sessionStore.sweepRunning.Store(false)
cancel()
<-done
tkr.Stop()
}
}
// SessionStoreInjector is middleware that injects the session store into the context variables.
func SessionStoreInjector(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return func(c *echo.Context) error {
c.Set("AmSessionStore", sessionStore)
return next(c)
}
+2 -2
View File
@@ -14,6 +14,7 @@ package ui
import (
"embed"
"errors"
"fmt"
"io"
"io/fs"
@@ -120,8 +121,7 @@ func AmLoadDialog(name string) (*Dialog, error) {
f, err = extDialogs.Open(fmt.Sprintf("%s.yaml", name))
if err != nil {
f = nil
pe := err.(*fs.PathError)
if pe.Err == os.ErrInvalid || pe.Err == os.ErrNotExist {
if errors.Is(err, os.ErrInvalid) || errors.Is(err, os.ErrNotExist) {
err = nil
}
}
+3 -3
View File
@@ -29,7 +29,7 @@ import (
"git.erbosoft.com/amy/amsterdam/database"
"github.com/disintegration/imaging"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
)
//go:embed static_images/*
@@ -64,7 +64,7 @@ func mimeTypeFromFilename(filename string) string {
* Returns:
* Standard Go error return.
*/
func AmServeImage(c echo.Context) error {
func AmServeImage(c *echo.Context) error {
components := strings.SplitAfter(c.Request().URL.Path, "/")
var err error = nil
if len(components) == 4 {
@@ -105,7 +105,7 @@ func AmServeImage(c echo.Context) error {
* Returns:
* Standard Go error return.
*/
func AmServeVeniceCompatibleImage(c echo.Context) error {
func AmServeVeniceCompatibleImage(c *echo.Context) error {
id, err := strconv.Atoi(c.Param("id"))
if err == nil {
var img *database.ImageStore
+10 -10
View File
@@ -21,17 +21,17 @@ import (
"git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/database"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
)
// IPBanTest is middleware that handles the IP banning.
func IPBanTest(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return func(c *echo.Context) error {
// Check IP banning.
banmsg, banerr := database.AmTestIPBan(c.Request().Context(), c.RealIP())
if banerr != nil {
c.Logger().Warnf("address %s could not be tested: %v", c.RealIP(), banerr)
log.Warnf("address %s could not be tested: %v", c.RealIP(), banerr)
// but let the request pass anyway
} else if banmsg != "" {
amctxt := AmContextFromEchoContext(c)
@@ -43,7 +43,7 @@ func IPBanTest(next echo.HandlerFunc) echo.HandlerFunc {
// CookieLoginTest is middleware that handles cookie logins.
func CookieLoginTest(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return func(c *echo.Context) error {
amctxt := AmContextFromEchoContext(c)
// Check for cookie login.
if amctxt.CurrentUser().IsAnon {
@@ -77,11 +77,11 @@ func CookieLoginTest(next echo.HandlerFunc) echo.HandlerFunc {
// SetCommunity is middleware that sets the community context based on the URL.
func SetCommunity(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return func(c *echo.Context) error {
ctxt := AmContextFromEchoContext(c)
err := ctxt.SetCommunityContext(ctxt.URLParam("cid"))
if err != nil {
return AmSendPageData(c, ctxt, "error", echo.NewHTTPError(http.StatusNotFound).SetInternal(err))
return AmSendPageData(c, ctxt, "error", echo.NewHTTPError(http.StatusNotFound, err.Error()).Wrap(err))
}
var b strings.Builder
b.WriteString("/comm/")
@@ -94,10 +94,10 @@ func SetCommunity(next echo.HandlerFunc) echo.HandlerFunc {
// ValidateConference is middleware that validates the user has access to the community's conference facility.
func ValidateConference(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return func(c *echo.Context) error {
ctxt := AmContextFromEchoContext(c)
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 {
return AmSendPageData(c, ctxt, "error", err)
}
@@ -116,7 +116,7 @@ func ValidateConference(next echo.HandlerFunc) echo.HandlerFunc {
// SetConference is middleware that sets the conference context based on the URL.
func SetConference(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return func(c *echo.Context) error {
ctxt := AmContextFromEchoContext(c)
conf, err := database.AmGetConferenceByAlias(ctxt.Ctx(), ctxt.CurrentCommunity().Id, ctxt.URLParam("confid"))
if err != nil {
@@ -144,7 +144,7 @@ func SetConference(next echo.HandlerFunc) echo.HandlerFunc {
// SetTopic is middleware that sets the topic context based on the URL.
func SetTopic(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return func(c *echo.Context) error {
ctxt := AmContextFromEchoContext(c)
conf := ctxt.GetScratch("currentConference").(*database.Conference)
+133 -60
View File
@@ -1,6 +1,6 @@
/*
* 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
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -13,18 +13,82 @@
package ui
import (
"context"
"fmt"
"io"
"net/http"
"runtime"
"time"
"git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/database"
"github.com/klauspost/lctime"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
)
// panicRecoveryErr is the error created for panic recovery.
type panicRecoveryErr struct {
Phase string // phase of operation
Err error // error value
Stack []byte // stack trace
}
// Error returns the actual error string.
func (e *panicRecoveryErr) Error() string {
return fmt.Sprintf("[Panic Recovery in %s Phase] %s %s", e.Phase, e.Err.Error(), e.Stack)
}
// Unwrap returns the error "nested" inside this error.
func (e *panicRecoveryErr) Unwrap() error {
return e.Err
}
// doFrameRender renders the outer frame template with an inner template.
func doFrameRender(ctxt *echo.Context, amctxt AmContext, statusCode int, innerPage string) error {
if amctxt.FrameTitle() == "" {
log.Errorf("*** NO FRAME TITLE set for path %s", amctxt.URLPath())
amctxt.SetFrameTitle("<<< NO FRAME TITLE >>>")
}
amctxt.VarMap().Set("__innerPage", innerPage)
menus := make([]*MenuDefinition, 2)
switch amctxt.LeftMenu() {
case "top":
menus[0] = AmMenu(config.GlobalConfig.Site.TopMenuId)
case "community":
comm := amctxt.CurrentCommunity()
if comm != nil {
md, err := AmBuildCommunityMenu(ctxt.Request().Context(), comm)
if err != nil {
return err
}
menus[0] = md
} else {
menus[0] = AmMenu(config.GlobalConfig.Site.TopMenuId)
}
default:
return fmt.Errorf("AmSendPageData(): unknown left menu context: %s", amctxt.LeftMenu())
}
menus[1] = AmMenu(config.GlobalConfig.Site.FixedMenuId)
amctxt.VarMap().Set("__leftMenus", menus)
ad, err := database.AmGetRandomAd(ctxt.Request().Context())
if err != nil {
ad = &database.Advert{
AdId: -1,
ImagePath: "",
PathStyle: -1,
Caption: nil,
LinkURL: nil,
}
}
amctxt.VarMap().Set("__bannerad", ad)
amctxt.VarMap().Set("__debugMode", config.GlobalComputedConfig.DebugMode)
if tmp := amctxt.GetScratch("frame_suppressLogin"); tmp != nil {
amctxt.VarMap().Set("__suppressLogin", true)
}
return ctxt.Render(statusCode, config.GlobalConfig.Site.FrameTemplate, amctxt)
}
/* AmSendPageData sends page data to the output based on the command string.
* Parameters:
* ctxt - The Echo context from the request.
@@ -44,22 +108,37 @@ import (
* Returns:
* Standard Go error status.
*/
func AmSendPageData(ctxt echo.Context, amctxt AmContext, command string, data any) error {
func AmSendPageData(ctxt *echo.Context, amctxt AmContext, command string, data any) error {
// Enable panic recovery.
if !config.CommandLine.DebugPanic {
defer func() {
if r := recover(); r != nil {
if r == http.ErrAbortHandler {
panic(r)
}
tmperr, ok := r.(error)
if !ok {
tmperr = fmt.Errorf("%v", r)
}
stack := make([]byte, config.GlobalComputedConfig.PanicRecoveryStack)
length := runtime.Stack(stack, false)
log.Errorf("[Panic Recovery in SendData Phase] %s %s", tmperr.Error(), stack[:length])
}
}()
}
// Preprocess certain commands into different ones.
httprc := http.StatusOK
switch command {
case "error":
message := ""
if data == nil {
message = fmt.Sprintf("Unspecified error in %s", ctxt.Request().URL.String())
} else if he, ok := data.(*echo.HTTPError); ok {
message := fmt.Sprintf("Unspecified error in %s", ctxt.Request().URL.String())
if data != nil {
if he, ok := data.(*echo.HTTPError); ok {
httprc = he.Code
m1 := he.Message
e1 := he.Unwrap()
if m1 == nil || m1 == "" {
if e1 == nil {
message = fmt.Sprintf("Unspecified error in %s", ctxt.Request().URL.String())
} else {
if m1 == "" {
if e1 != nil {
message = e1.Error()
}
} else {
@@ -74,10 +153,11 @@ func AmSendPageData(ctxt echo.Context, amctxt AmContext, command string, data an
} else {
message = fmt.Sprintf("%v", data)
}
}
if httprc < 400 {
httprc = http.StatusInternalServerError
}
amctxt.SetFrameTitle("Internal Server Error")
amctxt.SetFrameTitle(http.StatusText(httprc))
amctxt.VarMap().Set("error", message)
if tmp := amctxt.GetSession("lastKnownGood"); tmp != nil {
amctxt.VarMap().Set("recovery", tmp)
@@ -98,6 +178,11 @@ func AmSendPageData(ctxt echo.Context, amctxt AmContext, command string, data an
}
// Process commands.
oldreq := ctxt.Request()
ctx, cancel := context.WithTimeout(oldreq.Context(), time.Duration(config.GlobalConfig.Tuning.Timeouts.PageRender)*time.Second)
defer cancel()
ctxt.SetRequest(oldreq.WithContext(ctx))
defer ctxt.SetRequest(oldreq)
var err error
switch command {
case "bytes":
@@ -113,47 +198,7 @@ func AmSendPageData(ctxt echo.Context, amctxt AmContext, command string, data an
case "template":
err = ctxt.Render(httprc, data.(string), amctxt)
case "framed":
if amctxt.FrameTitle() == "" {
ctxt.Logger().Errorf("*** NO FRAME TITLE set for path %s", amctxt.URLPath())
amctxt.SetFrameTitle("<<< NO FRAME TITLE >>>")
}
amctxt.VarMap().Set("__innerPage", data)
menus := make([]*MenuDefinition, 2)
switch amctxt.LeftMenu() {
case "top":
menus[0] = AmMenu("top")
case "community":
comm := amctxt.CurrentCommunity()
if comm != nil {
md, err := AmBuildCommunityMenu(ctxt.Request().Context(), comm)
if err != nil {
return err
}
menus[0] = md
} else {
menus[0] = AmMenu("top")
}
default:
return fmt.Errorf("AmSendPageData(): unknown left menu context: %s", amctxt.LeftMenu())
}
menus[1] = AmMenu("fixed")
amctxt.VarMap().Set("__leftMenus", menus)
ad, err := database.AmGetRandomAd(ctxt.Request().Context())
if err != nil {
ad = &database.Advert{
AdId: -1,
ImagePath: "",
PathStyle: -1,
Caption: nil,
LinkURL: nil,
}
}
amctxt.VarMap().Set("__bannerad", ad)
amctxt.VarMap().Set("__debugMode", config.GlobalComputedConfig.DebugMode)
if tmp := amctxt.GetScratch("frame_suppressLogin"); tmp != nil {
amctxt.VarMap().Set("__suppressLogin", true)
}
err = ctxt.Render(httprc, "frame.jet", amctxt)
err = doFrameRender(ctxt, amctxt, httprc, data.(string))
default:
err = fmt.Errorf("AmSendPageData(): unknown rendering type: %s", command)
}
@@ -169,6 +214,34 @@ var expireTime string = lctime.Strftime("%c", time.Unix(1, 0))
// AmPageFunc is the definition for an Amsterdam "page function" that handles most of the work and defers to the wrapper for rendering.
type AmPageFunc func(AmContext) (string, any)
// callWrappedPageFunc calls the specified page functon inside a wrapper that handles timeouts and panic recovery.
func callWrappedPageFunc(f AmPageFunc, ctxt *echo.Context, amctxt AmContext) (command string, arg any) {
if !config.CommandLine.DebugPanic {
defer func() {
if r := recover(); r != nil {
if r == http.ErrAbortHandler {
panic(r)
}
tmperr, ok := r.(error)
if !ok {
tmperr = fmt.Errorf("%v", r)
}
stack := make([]byte, config.GlobalComputedConfig.PanicRecoveryStack)
length := runtime.Stack(stack, false)
arg = &panicRecoveryErr{Phase: "PageFunc", Err: tmperr, Stack: stack[:length]}
command = "error"
}
}()
}
oldreq := ctxt.Request()
ctx, cancel := context.WithTimeout(oldreq.Context(), time.Duration(config.GlobalConfig.Tuning.Timeouts.PageExecute)*time.Second)
defer cancel()
ctxt.SetRequest(oldreq.WithContext(ctx))
defer ctxt.SetRequest(oldreq)
command, arg = f(amctxt)
return
}
/* AmWrap wraps the Amsterdam handler function in a wrapper that implements the spec for
* Echo handler functions.
* Parameters:
@@ -177,7 +250,7 @@ type AmPageFunc func(AmContext) (string, any)
* The wrapped function.
*/
func AmWrap(myfunc AmPageFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return func(c *echo.Context) error {
ctxt := AmContextFromEchoContext(c)
// Add the dynamic headers.
@@ -186,16 +259,16 @@ func AmWrap(myfunc AmPageFunc) echo.HandlerFunc {
c.Response().Header().Set("Expires", expireTime)
// Exec the wrapped function.
command, arg := myfunc(ctxt)
command, arg := callWrappedPageFunc(myfunc, c, ctxt)
if command != "error" && command != "ipban" {
ctxt.SetSession("lastKnownGood", ctxt.Locator())
}
if err := ctxt.SaveSession(); err != nil {
c.Logger().Errorf("Session save error: %v", err)
log.Errorf("Session save error: %v", err)
return err
}
if err := AmSendPageData(c, ctxt, command, arg); err != nil {
c.Logger().Errorf("Rendering error: %v", err)
log.Errorf("Rendering error: %v", err)
return err
}
return nil
@@ -203,7 +276,7 @@ func AmWrap(myfunc AmPageFunc) echo.HandlerFunc {
}
// AmWithTempContext runs a page function with a temporary context. Used in error handling.
func AmWithTempContext(c echo.Context, fn AmPageFunc) error {
func AmWithTempContext(c *echo.Context, fn AmPageFunc) error {
var ctxt AmContext = nil
myctxt := c.Get("__amsterdam_context")
if myctxt != nil {
@@ -225,7 +298,7 @@ func AmWithTempContext(c echo.Context, fn AmPageFunc) error {
}
// Call the function
command, arg := fn(ctxt)
command, arg := callWrappedPageFunc(fn, c, ctxt)
// Add the dynamic headers.
c.Response().Header().Set("Pragma", "No-cache")
@@ -233,7 +306,7 @@ func AmWithTempContext(c echo.Context, fn AmPageFunc) error {
c.Response().Header().Set("Expires", expireTime)
if err := AmSendPageData(c, ctxt, command, arg); err != nil {
c.Logger().Errorf("Rendering error: %v", err)
log.Errorf("Rendering error: %v", err)
return err
}
return nil
+1 -1
View File
@@ -23,7 +23,7 @@ import (
"strings"
"git.erbosoft.com/amy/amsterdam/config"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
"golang.org/x/net/html"
)
+5 -2
View File
@@ -31,7 +31,7 @@ import (
"github.com/CloudyKit/jet/v6"
"github.com/CloudyKit/jet/v6/loaders/embedfs"
"github.com/CloudyKit/jet/v6/loaders/multi"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
)
@@ -309,6 +309,9 @@ func setupTemplates() {
views.AddGlobal("AmsterdamVersion", config.AMSTERDAM_VERSION)
views.AddGlobal("AmsterdamCopyright", config.AMSTERDAM_COPYRIGHT)
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("postRewrite", postRewrite)
views.AddGlobalFunc("MakeIntRange", makeIntRange)
@@ -363,7 +366,7 @@ type TemplateRenderer struct{}
* Returns:
* Standard Go error status.
*/
func (r *TemplateRenderer) Render(w io.Writer, name string, data any, c echo.Context) error {
func (r *TemplateRenderer) Render(c *echo.Context, w io.Writer, name string, data any) error {
defer util.MeasureTime(fmt.Sprintf("ui.Render(%s)", name))()
view, err := views.GetTemplate(name)
+6 -6
View File
@@ -13,11 +13,11 @@
<div class="mb-2">
<div class=" flex items-baseline gap-2">
<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>
{{ else if scope == "conference" }}
{{ else if scope == PLSCOPE_CONFERENCE }}
<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>
{{ end }}
</div>
@@ -26,11 +26,11 @@
<!-- Backlink -->
<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>
{{ 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>
{{ 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>
{{ end }}
</div>
+2 -2
View File
@@ -26,7 +26,7 @@ import (
"git.erbosoft.com/amy/amsterdam/ui"
"git.erbosoft.com/amy/amsterdam/util"
"github.com/biter777/countries"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus"
)
@@ -382,7 +382,7 @@ func ShowProfile(ctxt ui.AmContext) (string, any) {
// Gather the info on the current user.
user, err := database.AmGetUserByName(ctxt.Ctx(), ctxt.URLParam("uname"), nil)
if err != nil {
return "error", echo.NewHTTPError(http.StatusNotFound).SetInternal(err)
return "error", echo.NewHTTPError(http.StatusNotFound, err.Error()).Wrap(err)
}
ci, err := user.ContactInfo(ctxt.Ctx())
if err != nil {
-12
View File
@@ -13,7 +13,6 @@
package util
import (
"net"
"regexp"
"strings"
"time"
@@ -172,17 +171,6 @@ func Map[A, B any](in []A, fn func(A) B) []B {
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.
func IIF[A any](expr bool, v1, v2 A) A {
if expr {