23 Commits

Author SHA1 Message Date
amy a2c2a1f750 cleanups to startup code and goroutine code 2026-05-06 22:19:08 -06:00
amy 08a10a55dd factoring out a lot of string constant values 2026-05-03 22:39:11 -06:00
amy 64161721bf Merge pull request 'changes required to convert to Echo v5' (#7) from echo5-update into main
Reviewed-on: #7
Closes issue #6.
2026-05-03 14:59:49 -06:00
amy d3e89b886e incorporated more configuration values into the timeout and panic recovery code 2026-05-03 14:44:14 -06:00
amy e962c4d0c5 worked in the timeout and panic protection for AmWrap and friends 2026-05-03 14:01:10 -06:00
amy 53ee2281bc timeout values now in config 2026-05-03 11:19:20 -06:00
amy d309b90953 inserted the timeout hooks 2026-05-02 23:30:15 -06:00
amy e05b34a866 changes required to convert to Echo v5 2026-05-02 00:05:30 -06:00
amy 6526c74ed2 update release build action 2026-04-29 23:02:25 -06:00
amy 10a347747e set up auto build of executables and advance version number
Build and Release Amsterdam Assets / build-and-upload (release) Failing after 1m0s
2026-04-29 22:29:35 -06:00
amy 631215f6a0 library tidying 2026-04-25 22:47:12 -06:00
amy 759996b4cd extended the use of ConferenceLink throughout the conferencing code 2026-04-24 22:40:33 -06:00
amy a068e17e65 fixed up CHANGELOG and edited the TODO 2026-04-22 17:49:08 -05:00
amy 57088e4680 added Apple site icon configuration and frame - closes issue #5 2026-04-21 16:30:40 -05:00
amy 17de55c5c2 Admin can now change the user name of a user account. Closes issue #2. 2026-04-21 15:40:50 -05:00
amy ea807cc55f update TODO 2026-04-20 21:20:46 -05:00
amy 14c6df9891 cleanup for deleting community conferences 2026-04-19 20:52:38 -05:00
amy 581319279a straighten out conference alias add and remove 2026-04-19 20:45:23 -05:00
amy 0e3c1a293e fixed showing of hidden posts 2026-04-19 20:18:23 -05:00
amy 391d8ccc99 bug fixes related to top page and post link rewriting 2026-04-19 17:26:38 -05:00
amy 3deb11e0a5 changes to conference alias map handling to account for community ID 2026-04-19 09:01:59 -05:00
amy 1348d0225f correct architectural goof where conference aliases had global scope instead of community scope (untested) 2026-04-18 21:13:00 -05:00
amy 70dcf82234 update README and TODO 2026-04-17 19:46:08 -05:00
55 changed files with 913 additions and 709 deletions
+33
View File
@@ -0,0 +1,33 @@
name: Build and Release Amsterdam Assets
on:
release:
types: [published]
jobs:
build-and-upload:
runs-on: host
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.26'
- name: Build Binaries
run: |
# Build targets: Linux AMD64, Linux ARM64 (RPi), Windows AMD64, macOS ARM64 (Apple Silicon), macOS AMD64 (Intel)
GOOS=linux GOARCH=amd64 go build -o amsterdam-linux-amd64
GOOS=linux GOARCH=arm64 go build -o amsterdam-linux-arm64
GOOS=windows GOARCH=amd64 go build -o amsterdam-windows-amd64.exe
GOOS=darwin GOARCH=arm64 go build -o amsterdam-macos-arm64
GOOS=darwin GOARCH=amd64 go build -o amsterdam-macos-amd64
- name: Upload Assets to Release
uses: softprops/action-gh-release@v2
with:
files: |
amsterdam-linux-amd64
amsterdam-linux-arm64
amsterdam-windows-amd64.exe
amsterdam-macos-arm64
amsterdam-macos-amd64
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -6,4 +6,4 @@ __debug_bin*
/test*.yaml /test*.yaml
# Ignore mac-specific files # Ignore mac-specific files
/.DS_Store .DS_Store
+4 -7
View File
@@ -114,14 +114,9 @@ in your `PATH`. Then run `go generate` to regenerate the CSS file before you run
## Installing Amsterdam ## Installing Amsterdam
You will need a MySQL database to store Amsterdam data. Create a new empty database, then, from the command line, use the command: You will need a MySQL database to store Amsterdam data. Create a new empty database.
> `mysql -u root -p _databasename_ \< setup/mysql-database.sql` Ensure a user in your database is granted all privileges to all tables in your new database.
(Replace _databasename_ with the name of your database. If you use a user other than `root` for administrative access to your
MySQL server, use that.)
Ensure a user in your database is granted SELECT, INSERT, UPDATE, and DELETE privileges to all tables in your new database.
This is the user that you will configure Amsterdam to use. This is the user that you will configure Amsterdam to use.
The database may be specified to Amsterdam with the following command line options or environment variables: The database may be specified to Amsterdam with the following command line options or environment variables:
@@ -133,6 +128,8 @@ The database may be specified to Amsterdam with the following command line optio
All these options may also be specified via the configuration file (see below). All these options may also be specified via the configuration file (see below).
The first time you execute Amsterdam, the necessary database tables will be created and populated.
Amsterdam also requires access to a local SMTP server, as it sends out E-mail messages such as account verification, Amsterdam also requires access to a local SMTP server, as it sends out E-mail messages such as account verification,
password reminders, subscribed posts, and messages from conference or community hosts. It may be specified to Amsterdam password reminders, subscribed posts, and messages from conference or community hosts. It may be specified to Amsterdam
with the following command line options or environment variables: with the following command line options or environment variables:
+4 -19
View File
@@ -1,22 +1,10 @@
# Future Directions for Amsterdam # Future Directions for Amsterdam
After the point where it reaches feature parity with Venice circa 2006.
## Pre-Launch Polish
* ~~Policy page support.~~
* ~~User agreement in a separate file rather than directly in settings.~~
* ~~Support all customizations that were done with the EMinds instance of Venice.~~
* ~~Gitea-like status page showing Go-specific internals.~~
* ~~Build static Tailwind CSS file rather than using remote-loaded version. (Gate on debug/prod flag)~~
* ~~Rate limiter.~~
* ~~Better logging configuration.~~
## Immediate Cleanups Required ## Immediate Cleanups Required
* A better way to set up the database than `setup/database.sql`. Bring the table setup into the application somehow. * <s>A better way to set up the database than `setup/database.sql`. Bring the table setup into the application somehow.
The [migrate](https://github.com/golang-migrate/migrate) library might be of use here. The [migrate](https://github.com/golang-migrate/migrate) library might be of use here.</s>
* Database format migrations. * ~~Database format migrations.~~
* Allow use of Postgres as a database. * Allow use of Postgres as a database.
* Dockerization. * Dockerization.
* Implement proper help and online documentation. * Implement proper help and online documentation.
@@ -27,12 +15,9 @@ After the point where it reaches feature parity with Venice circa 2006.
* Should those be community "services" instead? * Should those be community "services" instead?
* For Chat, if it's implemented, it should use XMPP. * For Chat, if it's implemented, it should use XMPP.
## Architectural Goofs
* Conference Aliases are effectively in a system-wide namespace. Should be per-community.
## Additional Items ## Additional Items
* Ensure design is responsive enough that we can use the site on mobile devices.
* Decouple from MySQL, introduce other database support. Postgres and SQLite are the two priorities here. * Decouple from MySQL, introduce other database support. Postgres and SQLite are the two priorities here.
* Fix password storage. Straight SHA-1 hashes aren't gonna cut it. There are better ways. * Fix password storage. Straight SHA-1 hashes aren't gonna cut it. There are better ways.
* Introduce OAuth authentication? (Related to above) * Introduce OAuth authentication? (Related to above)
+1 -1
View File
@@ -22,7 +22,7 @@ import (
"git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/ui"
"git.erbosoft.com/amy/amsterdam/util" "git.erbosoft.com/amy/amsterdam/util"
"github.com/biter777/countries" "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. // 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/exports"
"git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/ui"
"git.erbosoft.com/amy/amsterdam/util" "git.erbosoft.com/amy/amsterdam/util"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
+26 -21
View File
@@ -26,7 +26,7 @@ import (
"git.erbosoft.com/amy/amsterdam/htmlcheck" "git.erbosoft.com/amy/amsterdam/htmlcheck"
"git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/ui"
"github.com/CloudyKit/jet/v6" "github.com/CloudyKit/jet/v6"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -136,7 +136,7 @@ func Topics(ctxt ui.AmContext) (string, any) {
} }
// create the "read new" URL // create the "read new" URL
urlStem := fmt.Sprintf("/comm/%s/conf/%s", comm.Alias, ctxt.GetScratch("currentAlias")) urlStem := ctxt.GetScratch("ConferenceLink").(string)
if !ctxt.CurrentUser().IsAnon { if !ctxt.CurrentUser().IsAnon {
traverser := ui.NewTopicTraverser(topics) traverser := ui.NewTopicTraverser(topics)
ctxt.SetSession("topic.traverser", traverser) ctxt.SetSession("topic.traverser", traverser)
@@ -178,14 +178,13 @@ func Topics(ctxt ui.AmContext) (string, any) {
* Data as a parameter for the command string. * Data as a parameter for the command string.
*/ */
func NewTopicForm(ctxt ui.AmContext) (string, any) { func NewTopicForm(ctxt ui.AmContext) (string, any) {
comm := ctxt.CurrentCommunity()
conf := ctxt.GetScratch("currentConference").(*database.Conference) conf := ctxt.GetScratch("currentConference").(*database.Conference)
myLevel := ctxt.GetScratch("levelInConference").(uint16) myLevel := ctxt.GetScratch("levelInConference").(uint16)
if !conf.TestPermission("Conference.Create", myLevel) { if !conf.TestPermission("Conference.Create", myLevel) {
return "error", echo.NewHTTPError(http.StatusForbidden, "you are not permitted to create topics in this conference") return "error", echo.NewHTTPError(http.StatusForbidden, "you are not permitted to create topics in this conference")
} }
ctxt.VarMap().Set("conferenceName", conf.Name) ctxt.VarMap().Set("conferenceName", conf.Name)
ctxt.VarMap().Set("urlStem", fmt.Sprintf("/comm/%s/conf/%s", comm.Alias, ctxt.GetScratch("currentAlias"))) ctxt.VarMap().Set("urlStem", ctxt.GetScratch("ConferenceLink").(string))
ctxt.VarMap().Set("topicName", "") ctxt.VarMap().Set("topicName", "")
pseud, err := conf.DefaultPseud(ctxt.Ctx(), ctxt.CurrentUser()) pseud, err := conf.DefaultPseud(ctxt.Ctx(), ctxt.CurrentUser())
if err != nil { if err != nil {
@@ -212,7 +211,7 @@ func NewTopic(ctxt ui.AmContext) (string, any) {
return "error", echo.NewHTTPError(http.StatusForbidden, "you are not permitted to create topics in this conference") return "error", echo.NewHTTPError(http.StatusForbidden, "you are not permitted to create topics in this conference")
} }
urlStem := fmt.Sprintf("/comm/%s/conf/%s", comm.Alias, ctxt.GetScratch("currentAlias")) urlStem := ctxt.GetScratch("ConferenceLink").(string)
if ctxt.FormFieldIsSet("cancel") { if ctxt.FormFieldIsSet("cancel") {
return "redirect", urlStem return "redirect", urlStem
} }
@@ -247,7 +246,7 @@ func NewTopic(ctxt ui.AmContext) (string, any) {
if err != nil { if err != nil {
return "error", err return "error", err
} }
checker.SetContext("PostLinkDecoderContext", database.AmCreatePostLinkContext(comm.Alias, ctxt.GetScratch("currentAlias").(string), conf.TopTopic+1)) checker.SetContext("PostLinkDecoderContext", database.AmCreatePostLinkContext(comm.Alias, comm.Id, ctxt.GetScratch("currentAlias").(string), conf.TopTopic+1))
checker.Append(postdata) checker.Append(postdata)
checker.Finish() checker.Finish()
v, _ = checker.Value() v, _ = checker.Value()
@@ -282,7 +281,7 @@ func NewTopic(ctxt ui.AmContext) (string, any) {
if err != nil { if err != nil {
return "error", err return "error", err
} }
checker.SetContext("PostLinkDecoderContext", database.AmCreatePostLinkContext(comm.Alias, ctxt.GetScratch("currentAlias").(string), conf.TopTopic+1)) checker.SetContext("PostLinkDecoderContext", database.AmCreatePostLinkContext(comm.Alias, comm.Id, ctxt.GetScratch("currentAlias").(string), conf.TopTopic+1))
checker.Append(ctxt.FormField("pb")) checker.Append(ctxt.FormField("pb"))
checker.Finish() checker.Finish()
zeroPost, _ := checker.Value() zeroPost, _ := checker.Value()
@@ -385,7 +384,8 @@ func templatePostText(args jet.Arguments) reflect.Value {
// templateOverrideLine creates the "override line" for a post, that is, what gets displayed in place of the post text. // templateOverrideLine creates the "override line" for a post, that is, what gets displayed in place of the post text.
func templateOverrideLine(args jet.Arguments) reflect.Value { func templateOverrideLine(args jet.Arguments) reflect.Value {
post := args.Get(0).Convert(reflect.TypeFor[*database.PostHeader]()).Interface().(*database.PostHeader) post := args.Get(0).Convert(reflect.TypeFor[*database.PostHeader]()).Interface().(*database.PostHeader)
ctxt := args.Get(1).Convert(reflect.TypeFor[ui.AmContext]()).Interface().(ui.AmContext) advanced := args.Get(1).Convert(reflect.TypeFor[bool]()).Bool()
ctxt := args.Get(2).Convert(reflect.TypeFor[ui.AmContext]()).Interface().(ui.AmContext)
rc := "" rc := ""
if post.IsScribbled() { if post.IsScribbled() {
scr_date := "" scr_date := ""
@@ -402,7 +402,7 @@ func templateOverrideLine(args jet.Arguments) reflect.Value {
} else { } else {
rc = fmt.Sprintf("<<<%v>>>", err) rc = fmt.Sprintf("<<<%v>>>", err)
} }
} else if post.Hidden { } else if post.Hidden && !advanced {
rc = fmt.Sprintf("(Hidden Message: %d Lines)", *post.LineCount) rc = fmt.Sprintf("(Hidden Message: %d Lines)", *post.LineCount)
} }
return reflect.ValueOf(rc) return reflect.ValueOf(rc)
@@ -411,9 +411,10 @@ func templateOverrideLine(args jet.Arguments) reflect.Value {
// templateOverrideLink creates the "override link" for a post, which can make the override line a hyperlink. // templateOverrideLink creates the "override link" for a post, which can make the override line a hyperlink.
func templateOverrideLink(args jet.Arguments) reflect.Value { func templateOverrideLink(args jet.Arguments) reflect.Value {
post := args.Get(0).Convert(reflect.TypeFor[*database.PostHeader]()).Interface().(*database.PostHeader) post := args.Get(0).Convert(reflect.TypeFor[*database.PostHeader]()).Interface().(*database.PostHeader)
root := args.Get(1).Convert(reflect.TypeFor[string]()).String() advanced := args.Get(1).Convert(reflect.TypeFor[bool]()).Bool()
root := args.Get(2).Convert(reflect.TypeFor[string]()).String()
rc := "" rc := ""
if post.Hidden { if post.Hidden && !advanced {
rc = fmt.Sprintf("%s?r=%d&ac=1", root, post.Num) rc = fmt.Sprintf("%s?r=%d&ac=1", root, post.Num)
} }
return reflect.ValueOf(rc) return reflect.ValueOf(rc)
@@ -512,11 +513,11 @@ func ReadPosts(ctxt ui.AmContext) (string, any) {
resetLastRead := false resetLastRead := false
if ctxt.HasParameter("r") { if ctxt.HasParameter("r") {
if err := breakRange(topic, postRange, ctxt.Parameter("r"), ","); err != nil { 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") { } else if ctxt.HasParameter("rgo") {
if err := breakRange(topic, postRange, ctxt.Parameter("rgo"), "-"); err != nil { 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 { } else {
postRange[0] = lastRead + 1 postRange[0] = lastRead + 1
@@ -560,10 +561,11 @@ func ReadPosts(ctxt ui.AmContext) (string, any) {
summaryLine := fmt.Sprintf("%d Total; %d New; Last: %s", topic.TopMessage+1, topic.TopMessage-lastRead, prefs.Localizer().Strftime("%b %e, %Y %r", topic.LastUpdate)) summaryLine := fmt.Sprintf("%d Total; %d New; Last: %s", topic.TopMessage+1, topic.TopMessage-lastRead, prefs.Localizer().Strftime("%b %e, %Y %r", topic.LastUpdate))
ctxt.VarMap().Set("summaryLine", flags.String()+summaryLine) ctxt.VarMap().Set("summaryLine", flags.String()+summaryLine)
ctxt.SetFrameTitle(fmt.Sprintf("%s: %s%s", topic.Name, flags.String(), summaryLine)) ctxt.SetFrameTitle(fmt.Sprintf("%s: %s%s", topic.Name, flags.String(), summaryLine))
plc := database.AmCreatePostLinkContext("", ctxt.GetScratch("currentAlias").(string), topic.Number) plc := database.AmCreatePostLinkContext("", comm.Id, ctxt.GetScratch("currentAlias").(string), topic.Number)
ctxt.VarMap().Set("post_confRef", plc.AsString()) ctxt.VarMap().Set("post_confRef", plc.AsString())
plc.Community = comm.Alias plc.Community = comm.Alias
ctxt.VarMap().Set("post_topicPermalink", fmt.Sprintf("/go/%s", plc.AsString())) ctxt.VarMap().Set("post_topicPermalink", fmt.Sprintf("/go/%s", plc.AsString()))
ctxt.VarMap().Set("post_topicLink", fmt.Sprintf("%s/r/%d", ctxt.GetScratch("ConferenceLink"), topic.Number))
plc.FirstPost = postRange[0] plc.FirstPost = postRange[0]
plc.LastPost = postRange[1] plc.LastPost = postRange[1]
ctxt.VarMap().Set("postsPermalink", fmt.Sprintf("/go/%s", plc.AsString())) ctxt.VarMap().Set("postsPermalink", fmt.Sprintf("/go/%s", plc.AsString()))
@@ -631,7 +633,7 @@ func ReadPosts(ctxt ui.AmContext) (string, any) {
ctxt.VarMap().Set("advancedControls", advancedControls) ctxt.VarMap().Set("advancedControls", advancedControls)
// Adjust the traverser and get the "next" link. // Adjust the traverser and get the "next" link.
urlStem := fmt.Sprintf("/comm/%s/conf/%s", comm.Alias, ctxt.GetScratch("currentAlias").(string)) urlStem := ctxt.GetScratch("ConferenceLink").(string)
if traverser != nil { if traverser != nil {
traverser.ClearTopic(topic.Number) traverser.ClearTopic(topic.Number)
nextTopic := traverser.NextTopic(topic.Number) nextTopic := traverser.NextTopic(topic.Number)
@@ -686,7 +688,7 @@ func PostInTopic(ctxt ui.AmContext) (string, any) {
topic := ctxt.GetScratch("currentTopic").(*database.Topic) topic := ctxt.GetScratch("currentTopic").(*database.Topic)
ctxt.VarMap().Set("post_topic", topic) ctxt.VarMap().Set("post_topic", topic)
urlStem := fmt.Sprintf("/comm/%s/conf/%s/r/%d", comm.Alias, ctxt.GetScratch("currentAlias"), topic.Number) urlStem := fmt.Sprintf("%s/r/%d", ctxt.GetScratch("ConferenceLink"), topic.Number)
if ctxt.FormFieldIsSet("cancel") { if ctxt.FormFieldIsSet("cancel") {
return "redirect", urlStem return "redirect", urlStem
} }
@@ -734,7 +736,7 @@ func PostInTopic(ctxt ui.AmContext) (string, any) {
if err != nil { if err != nil {
return "error", err return "error", err
} }
checker.SetContext("PostLinkDecoderContext", database.AmCreatePostLinkContext(comm.Alias, ctxt.GetScratch("currentAlias").(string), topic.Number)) checker.SetContext("PostLinkDecoderContext", database.AmCreatePostLinkContext(comm.Alias, comm.Id, ctxt.GetScratch("currentAlias").(string), topic.Number))
checker.Append(postdata) checker.Append(postdata)
checker.Finish() checker.Finish()
v, _ = checker.Value() v, _ = checker.Value()
@@ -754,7 +756,7 @@ func PostInTopic(ctxt ui.AmContext) (string, any) {
} else if ctxt.FormFieldIsSet("postnext") && len(urlNextTopic) > 0 { } else if ctxt.FormFieldIsSet("postnext") && len(urlNextTopic) > 0 {
returnURL = urlNextTopic returnURL = urlNextTopic
} else if ctxt.FormFieldIsSet("posttopics") { } else if ctxt.FormFieldIsSet("posttopics") {
returnURL = fmt.Sprintf("/comm/%s/conf/%s", comm.Alias, ctxt.GetScratch("currentAlias")) returnURL = ctxt.GetScratch("ConferenceLink").(string)
} else { } else {
return "error", EBUTTON return "error", EBUTTON
} }
@@ -772,10 +774,13 @@ func PostInTopic(ctxt ui.AmContext) (string, any) {
return "error", err return "error", err
} }
plc := database.AmCreatePostLinkContext("", ctxt.GetScratch("currentAlias").(string), topic.Number) plc := database.AmCreatePostLinkContext("", comm.Id, ctxt.GetScratch("currentAlias").(string), topic.Number)
ctxt.VarMap().Set("post_confRef", plc.AsString()) ctxt.VarMap().Set("post_confRef", plc.AsString())
plc.Community = comm.Alias plc.Community = comm.Alias
ctxt.VarMap().Set("post_topicPermalink", fmt.Sprintf("/go/%s", plc.AsString())) ctxt.VarMap().Set("post_topicPermalink", fmt.Sprintf("/go/%s", plc.AsString()))
t := fmt.Sprintf("%s/r/%d", ctxt.GetScratch("ConferenceLink"), topic.Number)
ctxt.VarMap().Set("post_topicLink", t)
ctxt.VarMap().Set("post_stem", t)
ctxt.VarMap().SetFunc("post_getOverrideLine", templateOverrideLine) ctxt.VarMap().SetFunc("post_getOverrideLine", templateOverrideLine)
ctxt.VarMap().SetFunc("post_getOverrideLink", templateOverrideLink) ctxt.VarMap().SetFunc("post_getOverrideLink", templateOverrideLink)
@@ -783,7 +788,6 @@ func PostInTopic(ctxt ui.AmContext) (string, any) {
ctxt.VarMap().SetFunc("post_getUserName", templateExtractUserName) ctxt.VarMap().SetFunc("post_getUserName", templateExtractUserName)
ctxt.VarMap().SetFunc("post_getAttachmentInfo", templateAttachmentInfo) ctxt.VarMap().SetFunc("post_getAttachmentInfo", templateAttachmentInfo)
ctxt.VarMap().SetFunc("post_isBozo", templateBozo) ctxt.VarMap().SetFunc("post_isBozo", templateBozo)
ctxt.VarMap().Set("post_stem", fmt.Sprintf("/comm/%s/conf/%s/r/%d", comm.Alias, ctxt.GetScratch("currentAlias"), topic.Number))
ctxt.VarMap().Set("post_max", topic.TopMessage) ctxt.VarMap().Set("post_max", topic.TopMessage)
ctxt.VarMap().Set("posts", posts) ctxt.VarMap().Set("posts", posts)
ctxt.VarMap().Set("topicName", topic.Name) ctxt.VarMap().Set("topicName", topic.Name)
@@ -805,7 +809,8 @@ func PostInTopic(ctxt ui.AmContext) (string, any) {
if err != nil { if err != nil {
return "error", err return "error", err
} }
checker.SetContext("PostLinkDecoderContext", database.AmCreatePostLinkContext(comm.Alias, ctxt.GetScratch("currentAlias").(string), topic.Number)) checker.SetContext("PostLinkDecoderContext", database.AmCreatePostLinkContext(comm.Alias, comm.Id,
ctxt.GetScratch("currentAlias").(string), topic.Number))
checker.Append(ctxt.FormField("pb")) checker.Append(ctxt.FormField("pb"))
checker.Finish() checker.Finish()
postText, _ := checker.Value() postText, _ := checker.Value()
+30 -34
View File
@@ -26,7 +26,7 @@ import (
"git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/email" "git.erbosoft.com/amy/amsterdam/email"
"git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/ui"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -156,7 +156,7 @@ func ConfManage(ctxt ui.AmContext) (string, any) {
comm := ctxt.CurrentCommunity() comm := ctxt.CurrentCommunity()
conf := ctxt.GetScratch("currentConference").(*database.Conference) conf := ctxt.GetScratch("currentConference").(*database.Conference)
myLevel := ctxt.GetScratch("levelInConference").(uint16) myLevel := ctxt.GetScratch("levelInConference").(uint16)
urlStem := fmt.Sprintf("/comm/%s/conf/%s", comm.Alias, ctxt.GetScratch("currentAlias")) urlStem := ctxt.GetScratch("ConferenceLink").(string)
ctxt.VarMap().Set("confName", conf.Name) ctxt.VarMap().Set("confName", conf.Name)
ctxt.VarMap().Set("urlStem", urlStem) ctxt.VarMap().Set("urlStem", urlStem)
@@ -193,14 +193,13 @@ func ConfManage(ctxt ui.AmContext) (string, any) {
* Data as a parameter for the command string. * Data as a parameter for the command string.
*/ */
func SetPseud(ctxt ui.AmContext) (string, any) { func SetPseud(ctxt ui.AmContext) (string, any) {
comm := ctxt.CurrentCommunity()
conf := ctxt.GetScratch("currentConference").(*database.Conference) conf := ctxt.GetScratch("currentConference").(*database.Conference)
pseud := ctxt.FormField("pseud") pseud := ctxt.FormField("pseud")
err := conf.SetDefaultPseud(ctxt.Ctx(), ctxt.CurrentUser(), pseud) err := conf.SetDefaultPseud(ctxt.Ctx(), ctxt.CurrentUser(), pseud)
if err != nil { if err != nil {
return "error", err return "error", err
} }
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/manage", comm.Alias, ctxt.GetScratch("currentAlias")) return "redirect", fmt.Sprintf("%s/manage", ctxt.GetScratch("ConferenceLink"))
} }
/* ConfFixseen marks all messages in a conference as read. /* ConfFixseen marks all messages in a conference as read.
@@ -211,13 +210,12 @@ func SetPseud(ctxt ui.AmContext) (string, any) {
* Data as a parameter for the command string. * Data as a parameter for the command string.
*/ */
func ConfFixseen(ctxt ui.AmContext) (string, any) { func ConfFixseen(ctxt ui.AmContext) (string, any) {
comm := ctxt.CurrentCommunity()
conf := ctxt.GetScratch("currentConference").(*database.Conference) conf := ctxt.GetScratch("currentConference").(*database.Conference)
err := conf.Fixseen(ctxt.Ctx(), ctxt.CurrentUser()) err := conf.Fixseen(ctxt.Ctx(), ctxt.CurrentUser())
if err != nil { if err != nil {
return "error", err return "error", err
} }
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/manage", comm.Alias, ctxt.GetScratch("currentAlias")) return "redirect", fmt.Sprintf("%s/manage", ctxt.GetScratch("ConferenceLink"))
} }
/* AddToHotlist adds the current community and conference to the user's hotlist.. /* AddToHotlist adds the current community and conference to the user's hotlist..
@@ -234,7 +232,7 @@ func AddToHotlist(ctxt ui.AmContext) (string, any) {
if err != nil { if err != nil {
return "error", err return "error", err
} }
return "redirect", fmt.Sprintf("/comm/%s/conf/%s", comm.Alias, ctxt.GetScratch("currentAlias")) return "redirect", ctxt.GetScratch("ConferenceLink").(string)
} }
/* HideTopic hides or shows the current topic for the current user. /* HideTopic hides or shows the current topic for the current user.
@@ -254,7 +252,7 @@ func HideTopic(ctxt ui.AmContext) (string, any) {
if err != nil { if err != nil {
return "error", err return "error", err
} }
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/r/%d", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number) return "redirect", fmt.Sprintf("%s/r/%d", ctxt.GetScratch("ConferenceLink"), topic.Number)
} }
/* FreezeTopic freezes or unfreezes the current topic. /* FreezeTopic freezes or unfreezes the current topic.
@@ -275,7 +273,7 @@ func FreezeTopic(ctxt ui.AmContext) (string, any) {
if err != nil { if err != nil {
return "error", err return "error", err
} }
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/r/%d", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number) return "redirect", fmt.Sprintf("%s/r/%d", ctxt.GetScratch("ConferenceLink"), topic.Number)
} }
/* ArchiveTopic archives or unarchives the current topic. /* ArchiveTopic archives or unarchives the current topic.
@@ -296,7 +294,7 @@ func ArchiveTopic(ctxt ui.AmContext) (string, any) {
if err != nil { if err != nil {
return "error", err return "error", err
} }
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/r/%d", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number) return "redirect", fmt.Sprintf("%s/r/%d", ctxt.GetScratch("ConferenceLink"), topic.Number)
} }
/* StickTopic sticks or unsticks the current topic. /* StickTopic sticks or unsticks the current topic.
@@ -317,7 +315,7 @@ func StickTopic(ctxt ui.AmContext) (string, any) {
if err != nil { if err != nil {
return "error", err return "error", err
} }
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/r/%d", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number) return "redirect", fmt.Sprintf("%s/r/%d", ctxt.GetScratch("ConferenceLink"), topic.Number)
} }
/* DeleteTopic deletes the current topic. /* DeleteTopic deletes the current topic.
@@ -348,14 +346,14 @@ func DeleteTopic(ctxt ui.AmContext) (string, any) {
if err != nil { if err != nil {
return "error", err return "error", err
} }
return "redirect", fmt.Sprintf("/comm/%s/conf/%s", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias")) return "redirect", ctxt.GetScratch("ConferenceLink").(string)
} }
// Set up to display the message box. // Set up to display the message box.
mbox.SetMessage(fmt.Sprintf(`You are about to detele the topic <span class="font-bold text-red-600">"%s"</span> mbox.SetMessage(fmt.Sprintf(`You are about to detele the topic <span class="font-bold text-red-600">"%s"</span>
from the <span class="font-bold text-red-600">"%s"</span> conference!`, topic.Name, conf.Name)) from the <span class="font-bold text-red-600">"%s"</span> conference!`, topic.Name, conf.Name))
mbox.SetLink("no", fmt.Sprintf("/comm/%s/conf/%s/r/%d", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number)) mbox.SetLink("no", fmt.Sprintf("%s/r/%d", ctxt.GetScratch("ConferenceLink"), topic.Number))
mbox.SetLink("yes", fmt.Sprintf("/comm/%s/conf/%s/op/%d/delete", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number)) mbox.SetLink("yes", fmt.Sprintf("%s/op/%d/delete", ctxt.GetScratch("ConferenceLink"), topic.Number))
return mbox.Render(ctxt) return mbox.Render(ctxt)
} }
@@ -390,7 +388,7 @@ func HideMessage(ctxt ui.AmContext) (string, any) {
if err != nil { if err != nil {
return "error", err return "error", err
} }
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/r/%d?r=%d&ac=1", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number, hdrs[0].Num) return "redirect", fmt.Sprintf("%s/r/%d?r=%d&ac=1", ctxt.GetScratch("ConferenceLink"), topic.Number, hdrs[0].Num)
} }
/* ScribbleMessage scribbles a topic message. /* ScribbleMessage scribbles a topic message.
@@ -424,7 +422,7 @@ func ScribbleMessage(ctxt ui.AmContext) (string, any) {
if err != nil { if err != nil {
return "error", err return "error", err
} }
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/r/%d?r=%d&ac=1", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number, hdrs[0].Num) return "redirect", fmt.Sprintf("%s/r/%d?r=%d&ac=1", ctxt.GetScratch("ConferenceLink"), topic.Number, hdrs[0].Num)
} }
/* NukeMessage nukes (deletes entirely) a topic message. /* NukeMessage nukes (deletes entirely) a topic message.
@@ -466,11 +464,11 @@ func NukeMessage(ctxt ui.AmContext) (string, any) {
if err != nil { if err != nil {
return "error", err return "error", err
} }
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/r/%d", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number) return "redirect", fmt.Sprintf("%s/r/%d", ctxt.GetScratch("ConferenceLink"), topic.Number)
} }
// Set up to display the message box. // Set up to display the message box.
link, err := hdrs[0].Link(ctxt.Ctx(), "community") link, err := hdrs[0].Link(ctxt.Ctx(), ctxt.CurrentCommunity().Id, "community")
if err != nil { if err != nil {
return "error", err return "error", err
} }
@@ -480,8 +478,8 @@ func NukeMessage(ctxt ui.AmContext) (string, any) {
} }
mbox.SetMessage(fmt.Sprintf(`You are about to nuke message <span class="font-mono font-bold text-red-600">&lt;%s&gt;</span>, mbox.SetMessage(fmt.Sprintf(`You are about to nuke message <span class="font-mono font-bold text-red-600">&lt;%s&gt;</span>,
originally composed by <span class="font-bold text-red-600">&lt;%s&gt;</span>!`, link, creator.Username)) originally composed by <span class="font-bold text-red-600">&lt;%s&gt;</span>!`, link, creator.Username))
mbox.SetLink("no", fmt.Sprintf("/comm/%s/conf/%s/r/%d?r=%d&ac=1", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number, hdrs[0].Num)) mbox.SetLink("no", fmt.Sprintf("%s/r/%d?r=%d&ac=1", ctxt.GetScratch("ConferenceLink"), topic.Number, hdrs[0].Num))
mbox.SetLink("yes", fmt.Sprintf("/comm/%s/conf/%s/op/%d/nuke/%d", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number, hdrs[0].Num)) mbox.SetLink("yes", fmt.Sprintf("%s/op/%d/nuke/%d", ctxt.GetScratch("ConferenceLink"), topic.Number, hdrs[0].Num))
return mbox.Render(ctxt) return mbox.Render(ctxt)
} }
@@ -524,11 +522,11 @@ func PruneMessageAttachment(ctxt ui.AmContext) (string, any) {
if err != nil { if err != nil {
return "error", err return "error", err
} }
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/r/%d?r=%d&ac=1", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number, hdrs[0].Num) return "redirect", fmt.Sprintf("%s/r/%d?r=%d&ac=1", ctxt.GetScratch("ConferenceLink"), topic.Number, hdrs[0].Num)
} }
// Set up to display the message box. // Set up to display the message box.
link, err := hdrs[0].Link(ctxt.Ctx(), "community") link, err := hdrs[0].Link(ctxt.Ctx(), ctxt.CurrentCommunity().Id, "community")
if err != nil { if err != nil {
return "error", err return "error", err
} }
@@ -538,8 +536,8 @@ func PruneMessageAttachment(ctxt ui.AmContext) (string, any) {
} }
mbox.SetMessage(fmt.Sprintf(`You are about to prune the attachment of message <span class="font-mono font-bold text-red-600">&lt;%s&gt;</span>, mbox.SetMessage(fmt.Sprintf(`You are about to prune the attachment of message <span class="font-mono font-bold text-red-600">&lt;%s&gt;</span>,
originally composed by <span class="font-bold text-red-600">&lt;%s&gt;</span>!`, link, creator.Username)) originally composed by <span class="font-bold text-red-600">&lt;%s&gt;</span>!`, link, creator.Username))
mbox.SetLink("no", fmt.Sprintf("/comm/%s/conf/%s/r/%d?r=%d&ac=1", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number, hdrs[0].Num)) mbox.SetLink("no", fmt.Sprintf("%s/r/%d?r=%d&ac=1", ctxt.GetScratch("ConferenceLink"), topic.Number, hdrs[0].Num))
mbox.SetLink("yes", fmt.Sprintf("/comm/%s/conf/%s/op/%d/prune/%d", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number, hdrs[0].Num)) mbox.SetLink("yes", fmt.Sprintf("%s/op/%d/prune/%d", ctxt.GetScratch("ConferenceLink"), topic.Number, hdrs[0].Num))
return mbox.Render(ctxt) return mbox.Render(ctxt)
} }
@@ -591,7 +589,7 @@ func MoveMessageForm(ctxt ui.AmContext) (string, any) {
ctxt.VarMap().Set("post", hdrs[0]) ctxt.VarMap().Set("post", hdrs[0])
ctxt.VarMap().Set("topMessage", topic.TopMessage) ctxt.VarMap().Set("topMessage", topic.TopMessage)
formLink := fmt.Sprintf("/comm/%s/conf/%s/op/%d/move/%d", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number, hdrs[0].Num) formLink := fmt.Sprintf("%s/op/%d/move/%d", ctxt.GetScratch("ConferenceLink"), topic.Number, hdrs[0].Num)
ctxt.VarMap().Set("formLink", formLink) ctxt.VarMap().Set("formLink", formLink)
ctxt.SetFrameTitle("Move Message") ctxt.SetFrameTitle("Move Message")
@@ -625,7 +623,7 @@ func PublishMessage(ctxt ui.AmContext) (string, any) {
if err = hdrs[0].Publish(ctxt.Ctx(), comm, ctxt.CurrentUser(), ctxt.RemoteIP()); err != nil { if err = hdrs[0].Publish(ctxt.Ctx(), comm, ctxt.CurrentUser(), ctxt.RemoteIP()); err != nil {
return "error", err return "error", err
} }
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/r/%d?r=%d&ac=1", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number, hdrs[0].Num) return "redirect", fmt.Sprintf("%s/r/%d?r=%d&ac=1", ctxt.GetScratch("ConferenceLink"), topic.Number, hdrs[0].Num)
} }
/* MoveMessage moves a message to a different topic. /* MoveMessage moves a message to a different topic.
@@ -654,7 +652,7 @@ func MoveMessage(ctxt ui.AmContext) (string, any) {
return "error", EPOSTREF return "error", EPOSTREF
} }
if ctxt.FormFieldIsSet("cancel") { if ctxt.FormFieldIsSet("cancel") {
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/r/%d?r=%d&ac=1", comm.Alias, ctxt.GetScratch("currentAlias"), topic.Number, hdrs[0].Num) return "redirect", fmt.Sprintf("%s/r/%d?r=%d&ac=1", ctxt.GetScratch("ConferenceLink"), topic.Number, hdrs[0].Num)
} }
if !conf.TestPermission("Conference.Nuke", myLevel) || !conf.TestPermission("Conference.Post", myLevel) || topic.TopMessage == 0 { if !conf.TestPermission("Conference.Nuke", myLevel) || !conf.TestPermission("Conference.Post", myLevel) || topic.TopMessage == 0 {
return "error", ENOPERM return "error", ENOPERM
@@ -696,7 +694,7 @@ func MoveMessage(ctxt ui.AmContext) (string, any) {
}) })
} }
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/r/%d", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number) return "redirect", fmt.Sprintf("%s/r/%d", ctxt.GetScratch("ConferenceLink"), topic.Number)
} }
/* TopicManage displays the "manage topic" page. /* TopicManage displays the "manage topic" page.
@@ -709,8 +707,8 @@ func MoveMessage(ctxt ui.AmContext) (string, any) {
func TopicManage(ctxt ui.AmContext) (string, any) { func TopicManage(ctxt ui.AmContext) (string, any) {
comm := ctxt.CurrentCommunity() comm := ctxt.CurrentCommunity()
topic := ctxt.GetScratch("currentTopic").(*database.Topic) topic := ctxt.GetScratch("currentTopic").(*database.Topic)
ctxt.VarMap().Set("backlink", fmt.Sprintf("/comm/%s/conf/%s/r/%d", comm.Alias, ctxt.GetScratch("currentAlias"), topic.Number)) ctxt.VarMap().Set("backlink", fmt.Sprintf("%s/r/%d", ctxt.GetScratch("ConferenceLink"), topic.Number))
opsLink := fmt.Sprintf("/comm/%s/conf/%s/op/%d", comm.Alias, ctxt.GetScratch("currentAlias"), topic.Number) opsLink := fmt.Sprintf("%s/op/%d", ctxt.GetScratch("ConferenceLink"), topic.Number)
ctxt.VarMap().Set("opsLink", opsLink) ctxt.VarMap().Set("opsLink", opsLink)
ctxt.VarMap().Set("topicName", topic.Name) ctxt.VarMap().Set("topicName", topic.Name)
@@ -750,7 +748,6 @@ func TopicSetSubscribe(ctxt ui.AmContext) (string, any) {
if ctxt.CurrentUser().IsAnon { if ctxt.CurrentUser().IsAnon {
return "error", ENOPERM return "error", ENOPERM
} }
comm := ctxt.CurrentCommunity()
topic := ctxt.GetScratch("currentTopic").(*database.Topic) topic := ctxt.GetScratch("currentTopic").(*database.Topic)
flag, err := topic.IsSubscribed(ctxt.Ctx(), ctxt.CurrentUser()) flag, err := topic.IsSubscribed(ctxt.Ctx(), ctxt.CurrentUser())
if err != nil { if err != nil {
@@ -760,7 +757,7 @@ func TopicSetSubscribe(ctxt ui.AmContext) (string, any) {
if err != nil { if err != nil {
return "error", err return "error", err
} }
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/op/%d/manage", comm.Alias, ctxt.GetScratch("currentAlias"), topic.Number) return "redirect", fmt.Sprintf("%s/op/%d/manage", ctxt.GetScratch("ConferenceLink"), topic.Number)
} }
/* TopicRemoveBozo removes filtering from a specified user in the topic. /* TopicRemoveBozo removes filtering from a specified user in the topic.
@@ -771,7 +768,6 @@ func TopicSetSubscribe(ctxt ui.AmContext) (string, any) {
* Data as a parameter for the command string. * Data as a parameter for the command string.
*/ */
func TopicRemoveBozo(ctxt ui.AmContext) (string, any) { func TopicRemoveBozo(ctxt ui.AmContext) (string, any) {
comm := ctxt.CurrentCommunity()
topic := ctxt.GetScratch("currentTopic").(*database.Topic) topic := ctxt.GetScratch("currentTopic").(*database.Topic)
bozoUid, err := strconv.Atoi(ctxt.URLParam("uid")) bozoUid, err := strconv.Atoi(ctxt.URLParam("uid"))
if err != nil { if err != nil {
@@ -781,5 +777,5 @@ func TopicRemoveBozo(ctxt ui.AmContext) (string, any) {
if err != nil { if err != nil {
return "error", err return "error", err
} }
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/op/%d/manage", comm.Alias, ctxt.GetScratch("currentAlias"), topic.Number) return "redirect", fmt.Sprintf("%s/op/%d/manage", ctxt.GetScratch("ConferenceLink"), topic.Number)
} }
+33 -45
View File
@@ -99,7 +99,7 @@ func EditConference(ctxt ui.AmContext) (string, any) {
} }
button := dlg.WhichButton(ctxt) button := dlg.WhichButton(ctxt)
if button == "cancel" { if button == "cancel" {
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/manage", comm.Alias, ctxt.GetScratch("currentAlias")) return "redirect", fmt.Sprintf("%s/manage", ctxt.GetScratch("ConferenceLink"))
} else if button != "update" { } else if button != "update" {
dlg.SetCommunity(comm) dlg.SetCommunity(comm)
dlg.SetConference(conf, ctxt.GetScratch("currentAlias").(string)) dlg.SetConference(conf, ctxt.GetScratch("currentAlias").(string))
@@ -128,7 +128,7 @@ func EditConference(ctxt ui.AmContext) (string, any) {
return dlg.RenderError(ctxt, err.Error()) return dlg.RenderError(ctxt, err.Error())
} }
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/manage", comm.Alias, ctxt.GetScratch("currentAlias")) return "redirect", fmt.Sprintf("%s/manage", ctxt.GetScratch("ConferenceLink"))
} }
/* ConferenceAliasForm displays the form for managing conference aliases. /* ConferenceAliasForm displays the form for managing conference aliases.
@@ -148,8 +148,8 @@ func ConferenceAliasForm(ctxt ui.AmContext) (string, any) {
ctxt.VarMap().Set("newAlias", "") ctxt.VarMap().Set("newAlias", "")
ctxt.VarMap().Set("confName", conf.Name) ctxt.VarMap().Set("confName", conf.Name)
ctxt.VarMap().Set("backLink", fmt.Sprintf("/comm/%s/conf/%s/manage", comm.Alias, ctxt.GetScratch("currentAlias"))) ctxt.VarMap().Set("backLink", fmt.Sprintf("%s/manage", ctxt.GetScratch("ConferenceLink")))
ctxt.VarMap().Set("selfLink", fmt.Sprintf("/comm/%s/conf/%s/aliases", comm.Alias, ctxt.GetScratch("currentAlias"))) ctxt.VarMap().Set("selfLink", fmt.Sprintf("%s/aliases", ctxt.GetScratch("ConferenceLink")))
ctxt.SetFrameTitle(fmt.Sprintf("Manage Conference Aliases: %s", conf.Name)) ctxt.SetFrameTitle(fmt.Sprintf("Manage Conference Aliases: %s", conf.Name))
if ctxt.HasParameter("del") { if ctxt.HasParameter("del") {
@@ -159,7 +159,7 @@ func ConferenceAliasForm(ctxt ui.AmContext) (string, any) {
} }
} }
aliases, err := conf.Aliases(ctxt.Ctx()) aliases, err := conf.Aliases(ctxt.Ctx(), comm.Id)
if err != nil { if err != nil {
return "error", err return "error", err
} }
@@ -184,8 +184,8 @@ func ConferenceAliasAdd(ctxt ui.AmContext) (string, any) {
} }
ctxt.VarMap().Set("confName", conf.Name) ctxt.VarMap().Set("confName", conf.Name)
ctxt.VarMap().Set("backLink", fmt.Sprintf("/comm/%s/conf/%s/manage", comm.Alias, ctxt.GetScratch("currentAlias"))) ctxt.VarMap().Set("backLink", fmt.Sprintf("%s/manage", ctxt.GetScratch("ConferenceLink")))
ctxt.VarMap().Set("selfLink", fmt.Sprintf("/comm/%s/conf/%s/aliases", comm.Alias, ctxt.GetScratch("currentAlias"))) ctxt.VarMap().Set("selfLink", fmt.Sprintf("%s/aliases", ctxt.GetScratch("ConferenceLink")))
ctxt.SetFrameTitle(fmt.Sprintf("Manage Conference Aliases: %s", conf.Name)) ctxt.SetFrameTitle(fmt.Sprintf("Manage Conference Aliases: %s", conf.Name))
newAlias := ctxt.FormField("na") newAlias := ctxt.FormField("na")
@@ -206,7 +206,7 @@ func ConferenceAliasAdd(ctxt ui.AmContext) (string, any) {
ctxt.VarMap().Set("errorMessage", err.Error()) ctxt.VarMap().Set("errorMessage", err.Error())
} }
aliases, err := conf.Aliases(ctxt.Ctx()) aliases, err := conf.Aliases(ctxt.Ctx(), comm.Id)
if err != nil { if err != nil {
return "error", err return "error", err
} }
@@ -241,8 +241,8 @@ func ConferenceMembers(ctxt ui.AmContext) (string, any) {
// Set the first batch of page variables. // Set the first batch of page variables.
ctxt.VarMap().Set("commName", comm.Name) ctxt.VarMap().Set("commName", comm.Name)
ctxt.VarMap().Set("confName", conf.Name) ctxt.VarMap().Set("confName", conf.Name)
ctxt.VarMap().Set("backLink", fmt.Sprintf("/comm/%s/conf/%s/manage", comm.Alias, ctxt.GetScratch("currentAlias"))) ctxt.VarMap().Set("backLink", fmt.Sprintf("%s/manage", ctxt.GetScratch("ConferenceLink")))
ctxt.VarMap().Set("selfLink", fmt.Sprintf("/comm/%s/conf/%s/members", comm.Alias, ctxt.GetScratch("currentAlias"))) ctxt.VarMap().Set("selfLink", fmt.Sprintf("%s/members", ctxt.GetScratch("ConferenceLink")))
ctxt.VarMap().Set("roleList", database.AmRoleList("Conference.UserLevels")) ctxt.VarMap().Set("roleList", database.AmRoleList("Conference.UserLevels"))
ctxt.SetFrameTitle(fmt.Sprintf("Membership in Conference: %s", conf.Name)) ctxt.SetFrameTitle(fmt.Sprintf("Membership in Conference: %s", conf.Name))
@@ -381,7 +381,6 @@ func ConferenceMembers(ctxt ui.AmContext) (string, any) {
* Data as a parameter for the command string. * Data as a parameter for the command string.
*/ */
func ConfCustomForm(ctxt ui.AmContext) (string, any) { func ConfCustomForm(ctxt ui.AmContext) (string, any) {
comm := ctxt.CurrentCommunity()
conf := ctxt.GetScratch("currentConference").(*database.Conference) conf := ctxt.GetScratch("currentConference").(*database.Conference)
myLevel := ctxt.GetScratch("levelInConference").(uint16) myLevel := ctxt.GetScratch("levelInConference").(uint16)
if !conf.TestPermission("Conference.Change", myLevel) { if !conf.TestPermission("Conference.Change", myLevel) {
@@ -394,7 +393,7 @@ func ConfCustomForm(ctxt ui.AmContext) (string, any) {
} }
ctxt.VarMap().Set("confName", conf.Name) ctxt.VarMap().Set("confName", conf.Name)
ctxt.VarMap().Set("selfLink", fmt.Sprintf("/comm/%s/conf/%s/custom", comm.Alias, ctxt.GetScratch("currentAlias"))) ctxt.VarMap().Set("selfLink", fmt.Sprintf("%s/custom", ctxt.GetScratch("ConferenceLink")))
ctxt.VarMap().Set("topText", topBlock) ctxt.VarMap().Set("topText", topBlock)
ctxt.VarMap().Set("bottomText", bottomBlock) ctxt.VarMap().Set("bottomText", bottomBlock)
ctxt.SetFrameTitle(fmt.Sprintf("Customize Conference: %s", conf.Name)) ctxt.SetFrameTitle(fmt.Sprintf("Customize Conference: %s", conf.Name))
@@ -409,7 +408,6 @@ func ConfCustomForm(ctxt ui.AmContext) (string, any) {
* Data as a parameter for the command string. * Data as a parameter for the command string.
*/ */
func ConfCustom(ctxt ui.AmContext) (string, any) { func ConfCustom(ctxt ui.AmContext) (string, any) {
comm := ctxt.CurrentCommunity()
conf := ctxt.GetScratch("currentConference").(*database.Conference) conf := ctxt.GetScratch("currentConference").(*database.Conference)
myLevel := ctxt.GetScratch("levelInConference").(uint16) myLevel := ctxt.GetScratch("levelInConference").(uint16)
if !conf.TestPermission("Conference.Change", myLevel) { if !conf.TestPermission("Conference.Change", myLevel) {
@@ -429,7 +427,7 @@ func ConfCustom(ctxt ui.AmContext) (string, any) {
if err != nil { if err != nil {
return "error", err return "error", err
} }
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/manage", comm.Alias, ctxt.GetScratch("currentAlias")) return "redirect", fmt.Sprintf("%s/manage", ctxt.GetScratch("ConferenceLink"))
} }
/* ConfReports displays conference activity reports. /* ConfReports displays conference activity reports.
@@ -440,7 +438,6 @@ func ConfCustom(ctxt ui.AmContext) (string, any) {
* Data as a parameter for the command string. * Data as a parameter for the command string.
*/ */
func ConfReports(ctxt ui.AmContext) (string, any) { func ConfReports(ctxt ui.AmContext) (string, any) {
comm := ctxt.CurrentCommunity()
conf := ctxt.GetScratch("currentConference").(*database.Conference) conf := ctxt.GetScratch("currentConference").(*database.Conference)
myLevel := ctxt.GetScratch("levelInConference").(uint16) myLevel := ctxt.GetScratch("levelInConference").(uint16)
if !conf.TestPermission("Conference.Read", myLevel) { if !conf.TestPermission("Conference.Read", myLevel) {
@@ -448,7 +445,7 @@ func ConfReports(ctxt ui.AmContext) (string, any) {
} }
ctxt.VarMap().Set("confName", conf.Name) ctxt.VarMap().Set("confName", conf.Name)
ctxt.VarMap().Set("selfLink", fmt.Sprintf("/comm/%s/conf/%s/activity", comm.Alias, ctxt.GetScratch("currentAlias"))) ctxt.VarMap().Set("selfLink", fmt.Sprintf("%s/activity", ctxt.GetScratch("ConferenceLink")))
if ctxt.HasParameter("r") { if ctxt.HasParameter("r") {
// generate a report // generate a report
@@ -504,7 +501,7 @@ func ConfReports(ctxt ui.AmContext) (string, any) {
return "error", err return "error", err
} }
ctxt.VarMap().Set("topics", topicList) ctxt.VarMap().Set("topics", topicList)
ctxt.VarMap().Set("backLink", fmt.Sprintf("/comm/%s/conf/%s/manage", comm.Alias, ctxt.GetScratch("currentAlias"))) ctxt.VarMap().Set("backLink", fmt.Sprintf("%s/manage", ctxt.GetScratch("ConferenceLink")))
ctxt.SetFrameTitle(fmt.Sprintf("Conference Reports: %s", conf.Name)) ctxt.SetFrameTitle(fmt.Sprintf("Conference Reports: %s", conf.Name))
return "framed", "conf_reports.jet" return "framed", "conf_reports.jet"
} }
@@ -518,7 +515,6 @@ func ConfReports(ctxt ui.AmContext) (string, any) {
* Data as a parameter for the command string. * Data as a parameter for the command string.
*/ */
func ConferenceEmailForm(ctxt ui.AmContext) (string, any) { func ConferenceEmailForm(ctxt ui.AmContext) (string, any) {
comm := ctxt.CurrentCommunity()
conf := ctxt.GetScratch("currentConference").(*database.Conference) conf := ctxt.GetScratch("currentConference").(*database.Conference)
myLevel := ctxt.GetScratch("levelInConference").(uint16) myLevel := ctxt.GetScratch("levelInConference").(uint16)
if !conf.TestPermission("Conference.EMailParticipants", myLevel) { if !conf.TestPermission("Conference.EMailParticipants", myLevel) {
@@ -531,7 +527,7 @@ func ConferenceEmailForm(ctxt ui.AmContext) (string, any) {
} }
ctxt.VarMap().Set("topics", topics) ctxt.VarMap().Set("topics", topics)
ctxt.VarMap().Set("confName", conf.Name) ctxt.VarMap().Set("confName", conf.Name)
ctxt.VarMap().Set("selfLink", fmt.Sprintf("/comm/%s/conf/%s/email", comm.Alias, ctxt.GetScratch("currentAlias"))) ctxt.VarMap().Set("selfLink", fmt.Sprintf("%s/email", ctxt.GetScratch("ConferenceLink")))
ctxt.VarMap().Set("porl", 0).Set("top", 0).Set("xday", false) ctxt.VarMap().Set("porl", 0).Set("top", 0).Set("xday", false)
ctxt.VarMap().Set("day", 7).Set("subj", "").Set("pb", "") ctxt.VarMap().Set("day", 7).Set("subj", "").Set("pb", "")
ctxt.SetFrameTitle(fmt.Sprintf("Conference E-Mail: %s", conf.Name)) ctxt.SetFrameTitle(fmt.Sprintf("Conference E-Mail: %s", conf.Name))
@@ -555,7 +551,7 @@ func ConferenceEmail(ctxt ui.AmContext) (string, any) {
// Handle button presses. // Handle button presses.
if ctxt.FormFieldIsSet("cancel") { if ctxt.FormFieldIsSet("cancel") {
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/manage", comm.Alias, ctxt.GetScratch("currentAlias")) return "redirect", fmt.Sprintf("%s/manage", ctxt.GetScratch("ConferenceLink"))
} else if !ctxt.FormFieldIsSet("send") { } else if !ctxt.FormFieldIsSet("send") {
return "error", EBUTTON return "error", EBUTTON
} }
@@ -646,7 +642,7 @@ func ConferenceEmail(ctxt ui.AmContext) (string, any) {
log.Infof("ConferenceEmail delivery completed in %s", elapsed) log.Infof("ConferenceEmail delivery completed in %s", elapsed)
}) })
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/manage", comm.Alias, ctxt.GetScratch("currentAlias")) return "redirect", fmt.Sprintf("%s/manage", ctxt.GetScratch("ConferenceLink"))
} }
/* ConferenceExportForm displays the form for exporting data from a conference. /* ConferenceExportForm displays the form for exporting data from a conference.
@@ -657,7 +653,6 @@ func ConferenceEmail(ctxt ui.AmContext) (string, any) {
* Data as a parameter for the command string. * Data as a parameter for the command string.
*/ */
func ConferenceExportForm(ctxt ui.AmContext) (string, any) { func ConferenceExportForm(ctxt ui.AmContext) (string, any) {
comm := ctxt.CurrentCommunity()
conf := ctxt.GetScratch("currentConference").(*database.Conference) conf := ctxt.GetScratch("currentConference").(*database.Conference)
myLevel := ctxt.GetScratch("levelInConference").(uint16) myLevel := ctxt.GetScratch("levelInConference").(uint16)
if !conf.TestPermission("Conference.Change", myLevel) { if !conf.TestPermission("Conference.Change", myLevel) {
@@ -671,7 +666,7 @@ func ConferenceExportForm(ctxt ui.AmContext) (string, any) {
ctxt.VarMap().Set("topics", topics) ctxt.VarMap().Set("topics", topics)
ctxt.VarMap().Set("confName", conf.Name) ctxt.VarMap().Set("confName", conf.Name)
ctxt.VarMap().Set("selfLink", fmt.Sprintf("/comm/%s/conf/%s/export", comm.Alias, ctxt.GetScratch("currentAlias"))) ctxt.VarMap().Set("selfLink", fmt.Sprintf("%s/export", ctxt.GetScratch("ConferenceLink")))
ctxt.SetFrameTitle(fmt.Sprintf("Export Messages: %s", conf.Name)) ctxt.SetFrameTitle(fmt.Sprintf("Export Messages: %s", conf.Name))
return "framed", "conf_export.jet" return "framed", "conf_export.jet"
} }
@@ -684,7 +679,6 @@ func ConferenceExportForm(ctxt ui.AmContext) (string, any) {
* Data as a parameter for the command string. * Data as a parameter for the command string.
*/ */
func ConferenceExport(ctxt ui.AmContext) (string, any) { func ConferenceExport(ctxt ui.AmContext) (string, any) {
comm := ctxt.CurrentCommunity()
conf := ctxt.GetScratch("currentConference").(*database.Conference) conf := ctxt.GetScratch("currentConference").(*database.Conference)
myLevel := ctxt.GetScratch("levelInConference").(uint16) myLevel := ctxt.GetScratch("levelInConference").(uint16)
if !conf.TestPermission("Conference.Change", myLevel) { if !conf.TestPermission("Conference.Change", myLevel) {
@@ -692,7 +686,7 @@ func ConferenceExport(ctxt ui.AmContext) (string, any) {
} }
if ctxt.FormFieldIsSet("cancel") { if ctxt.FormFieldIsSet("cancel") {
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/manage", comm.Alias, ctxt.GetScratch("currentAlias")) return "redirect", fmt.Sprintf("%s/manage", ctxt.GetScratch("ConferenceLink"))
} else if !ctxt.FormFieldIsSet("export") { } else if !ctxt.FormFieldIsSet("export") {
return "error", EBUTTON return "error", EBUTTON
} }
@@ -765,15 +759,16 @@ func ConferenceImport(ctxt ui.AmContext) (string, any) {
return "error", ENOPERM return "error", ENOPERM
} }
if ctxt.Verb() == "GET" {
ctxt.VarMap().Set("confName", conf.Name) ctxt.VarMap().Set("confName", conf.Name)
ctxt.VarMap().Set("selfLink", fmt.Sprintf("/comm/%s/conf/%s/import", comm.Alias, ctxt.GetScratch("currentAlias"))) ctxt.VarMap().Set("selfLink", fmt.Sprintf("%s/import", ctxt.GetScratch("ConferenceLink")))
ctxt.SetFrameTitle("Import Messages: " + conf.Name) ctxt.SetFrameTitle("Import Messages: " + conf.Name)
if ctxt.Verb() == "GET" {
return "framed", "conf_import.jet" return "framed", "conf_import.jet"
} }
if ctxt.FormFieldIsSet("cancel") { if ctxt.FormFieldIsSet("cancel") {
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/manage", comm.Alias, ctxt.GetScratch("currentAlias")) return "redirect", fmt.Sprintf("%s/manage", ctxt.GetScratch("ConferenceLink"))
} else if !ctxt.FormFieldIsSet("import") { } else if !ctxt.FormFieldIsSet("import") {
return "error", EBUTTON return "error", EBUTTON
} }
@@ -786,27 +781,18 @@ func ConferenceImport(ctxt ui.AmContext) (string, any) {
mode = exports.VCIFTopicMatchNum mode = exports.VCIFTopicMatchNum
default: default:
ctxt.VarMap().Set("errorMessage", "Invalid matching parameter.") ctxt.VarMap().Set("errorMessage", "Invalid matching parameter.")
ctxt.VarMap().Set("confName", conf.Name)
ctxt.VarMap().Set("selfLink", fmt.Sprintf("/comm/%s/conf/%s/import", comm.Alias, ctxt.GetScratch("currentAlias")))
ctxt.SetFrameTitle("Import Messages: " + conf.Name)
return "framed", "conf_import.jet" return "framed", "conf_import.jet"
} }
importData, err := ctxt.FormFile("idata") importData, err := ctxt.FormFile("idata")
if err != nil { if err != nil {
ctxt.VarMap().Set("errorMessage", err.Error()) ctxt.VarMap().Set("errorMessage", err.Error())
ctxt.VarMap().Set("confName", conf.Name)
ctxt.VarMap().Set("selfLink", fmt.Sprintf("/comm/%s/conf/%s/import", comm.Alias, ctxt.GetScratch("currentAlias")))
ctxt.SetFrameTitle("Import Messages: " + conf.Name)
return "framed", "conf_import.jet" return "framed", "conf_import.jet"
} }
start := time.Now() start := time.Now()
f, err := importData.Open() f, err := importData.Open()
if err != nil { if err != nil {
ctxt.VarMap().Set("errorMessage", err.Error()) ctxt.VarMap().Set("errorMessage", err.Error())
ctxt.VarMap().Set("confName", conf.Name)
ctxt.VarMap().Set("selfLink", fmt.Sprintf("/comm/%s/conf/%s/import", comm.Alias, ctxt.GetScratch("currentAlias")))
ctxt.SetFrameTitle("Import Messages: " + conf.Name)
return "framed", "conf_import.jet" return "framed", "conf_import.jet"
} }
topics, posts, scroll, err := exports.VCIFImportMessages(ctxt.Ctx(), f, comm, conf, mode, ctxt.FormFieldIsSet("create"), ctxt.CurrentUser(), ctxt.RemoteIP()) topics, posts, scroll, err := exports.VCIFImportMessages(ctxt.Ctx(), f, comm, conf, mode, ctxt.FormFieldIsSet("create"), ctxt.CurrentUser(), ctxt.RemoteIP())
@@ -814,13 +800,10 @@ func ConferenceImport(ctxt ui.AmContext) (string, any) {
log.Infof("import messages operation completed in %v", time.Since(start)) log.Infof("import messages operation completed in %v", time.Since(start))
if err != nil { if err != nil {
ctxt.VarMap().Set("errorMessage", err.Error()) ctxt.VarMap().Set("errorMessage", err.Error())
ctxt.VarMap().Set("confName", conf.Name)
ctxt.VarMap().Set("selfLink", fmt.Sprintf("/comm/%s/conf/%s/import", comm.Alias, ctxt.GetScratch("currentAlias")))
ctxt.SetFrameTitle("Import Messages: " + conf.Name)
return "framed", "conf_import.jet" return "framed", "conf_import.jet"
} }
ctxt.VarMap().Set("backLink", fmt.Sprintf("/comm/%s/conf/%s/manage", comm.Alias, ctxt.GetScratch("currentAlias"))) ctxt.VarMap().Set("backLink", fmt.Sprintf("%s/manage", ctxt.GetScratch("ConferenceLink")))
ctxt.VarMap().Set("headline", fmt.Sprintf("Processed %d topic(s) and added %d new post(s).", topics, posts)) ctxt.VarMap().Set("headline", fmt.Sprintf("Processed %d topic(s) and added %d new post(s).", topics, posts))
ctxt.VarMap().Set("scroll", scroll) ctxt.VarMap().Set("scroll", scroll)
ctxt.SetFrameTitle("Import Results") ctxt.SetFrameTitle("Import Results")
@@ -858,8 +841,8 @@ func DeleteConference(ctxt ui.AmContext) (string, any) {
// Set up to display the message box. // Set up to display the message box.
mbox.SetMessage(fmt.Sprintf(`You are about to delete the conference <span class="font-bold text-red-600">"%s"</span> mbox.SetMessage(fmt.Sprintf(`You are about to delete the conference <span class="font-bold text-red-600">"%s"</span>
from the <span class="font-bold text-red-600">"%s"</span> community!`, conf.Name, comm.Name)) from the <span class="font-bold text-red-600">"%s"</span> community!`, conf.Name, comm.Name))
mbox.SetLink("no", fmt.Sprintf("/comm/%s/conf/%s/manage", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"))) mbox.SetLink("no", fmt.Sprintf("%s/manage", ctxt.GetScratch("ConferenceLink")))
mbox.SetLink("yes", fmt.Sprintf("/comm/%s/conf/%s/delete", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"))) mbox.SetLink("yes", fmt.Sprintf("%s/delete", ctxt.GetScratch("ConferenceLink")))
return mbox.Render(ctxt) return mbox.Render(ctxt)
} }
@@ -901,9 +884,12 @@ func CreateConference(ctxt ui.AmContext) (string, any) {
if err != nil { if err != nil {
return "error", err return "error", err
} }
var urlbuf strings.Builder
urlbuf.WriteString(ctxt.GetScratch("CommunityLink").(string))
urlbuf.WriteString("/conf")
button := dlg.WhichButton(ctxt) button := dlg.WhichButton(ctxt)
if button == "cancel" { if button == "cancel" {
return "redirect", fmt.Sprintf("/comm/%s/conf", comm.Alias) return "redirect", urlbuf.String()
} else if button != "create" { } else if button != "create" {
dlg.SetCommunity(comm) dlg.SetCommunity(comm)
return dlg.RenderError(ctxt, "invalid button pressed") return dlg.RenderError(ctxt, "invalid button pressed")
@@ -917,7 +903,9 @@ func CreateConference(ctxt ui.AmContext) (string, any) {
return dlg.RenderError(ctxt, err.Error()) return dlg.RenderError(ctxt, err.Error())
} }
log.Infof("Created conference '%s'", conf.Name) log.Infof("Created conference '%s'", conf.Name)
return "redirect", fmt.Sprintf("/comm/%s/conf/%s", comm.Alias, alias) urlbuf.WriteString("/")
urlbuf.WriteString(alias)
return "redirect", urlbuf.String()
} }
/* ManageConferenceList displays the list for managing conferences. /* ManageConferenceList displays the list for managing conferences.
+24 -4
View File
@@ -30,7 +30,7 @@ import (
) )
// AMSTERDAM_VERSION contains the version number of Amsterdam. // AMSTERDAM_VERSION contains the version number of Amsterdam.
const AMSTERDAM_VERSION = "0.1.1" const AMSTERDAM_VERSION = "0.2.0"
// AMSTERDAM_COPYRIGHT contains the copyright dates for Amsterdam. // AMSTERDAM_COPYRIGHT contains the copyright dates for Amsterdam.
const AMSTERDAM_COPYRIGHT = "2025-2026" const AMSTERDAM_COPYRIGHT = "2025-2026"
@@ -92,6 +92,7 @@ type AmConfig struct {
Type string `yaml:"type"` Type string `yaml:"type"`
} `yaml:"siteIcon"` } `yaml:"siteIcon"`
SiteShortcutIcon string `yaml:"siteShortcutIcon"` SiteShortcutIcon string `yaml:"siteShortcutIcon"`
SiteAppleIcon string `yaml:"siteAppleIcon"`
SiteLogo string `yaml:"siteLogo"` SiteLogo string `yaml:"siteLogo"`
TopRefresh int `yaml:"topRefresh"` TopRefresh int `yaml:"topRefresh"`
LoginCookieName string `yaml:"loginCookieName"` LoginCookieName string `yaml:"loginCookieName"`
@@ -99,7 +100,10 @@ type AmConfig struct {
SessionExpire string `yaml:"sessionExpire"` SessionExpire string `yaml:"sessionExpire"`
UserAgreementResource string `yaml:"userAgreementResource"` UserAgreementResource string `yaml:"userAgreementResource"`
PolicyResource string `yaml:"policyResource"` PolicyResource string `yaml:"policyResource"`
FrameTemplate string `yaml:"frameTemplate"`
FooterTemplate string `yaml:"footerTemplate"` FooterTemplate string `yaml:"footerTemplate"`
TopMenuId string `yaml:"topMenuId"`
FixedMenuId string `yaml:"fixedMenuId"`
DefaultCommunityLogo string `yaml:"defaultCommunityLogo"` DefaultCommunityLogo string `yaml:"defaultCommunityLogo"`
DefaultUserPhoto string `yaml:"defaultUserPhoto"` DefaultUserPhoto string `yaml:"defaultUserPhoto"`
WelcomeTitle string `yaml:"welcomeTitle"` WelcomeTitle string `yaml:"welcomeTitle"`
@@ -147,6 +151,9 @@ type AmConfig struct {
Prioritize string `yaml:"prioritize"` Prioritize string `yaml:"prioritize"`
} `yaml:"countryList"` } `yaml:"countryList"`
VeniceCompatibleImageURLs bool `yaml:"veniceCompatibleImageURLs"` VeniceCompatibleImageURLs bool `yaml:"veniceCompatibleImageURLs"`
PanicRecovery struct {
StackDataSize string `yaml:"stackDataSize"`
} `yaml:"panicRecovery"`
} `yaml:"rendering"` } `yaml:"rendering"`
Resources struct { Resources struct {
ViewTemplateDir string `yaml:"viewTemplateDir"` ViewTemplateDir string `yaml:"viewTemplateDir"`
@@ -166,6 +173,13 @@ type AmConfig struct {
} `yaml:"posting"` } `yaml:"posting"`
Tuning struct { Tuning struct {
WorkerTasks int `yaml:"workerTasks"` 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 { Queues struct {
AuditWrites int `yaml:"auditWrites"` AuditWrites int `yaml:"auditWrites"`
ContextRecycle int `yaml:"contextRecycle"` ContextRecycle int `yaml:"contextRecycle"`
@@ -199,7 +213,7 @@ func (c *AmConfig) ExPath(path string) string {
return filepath.Join(c.baseDir, path) 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 { type AmConfigComputed struct {
DebugMode bool // are we in debug mode? DebugMode bool // are we in debug mode?
LogLevel string // the logging level LogLevel string // the logging level
@@ -215,6 +229,7 @@ type AmConfigComputed struct {
MailAuthType string // SMTP auth type MailAuthType string // SMTP auth type
MailUser string // SMTP user name MailUser string // SMTP user name
MailPassword string // SMTP password MailPassword string // SMTP password
PanicRecoveryStack int32 // stack size for panic recovery
UploadMaxSize int32 // maximum upload size in bytes UploadMaxSize int32 // maximum upload size in bytes
UploadNoCompress map[string]bool // which upload types are not compressed? UploadNoCompress map[string]bool // which upload types are not compressed?
} }
@@ -320,7 +335,7 @@ func overlayStructValue(dest, loaded, defaults reflect.Value) {
} }
} else { } else {
// if we see this message, this function needs more work // 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())
} }
} }
} }
@@ -397,7 +412,12 @@ func SetupConfig() {
GlobalComputedConfig.MailAuthType = util.IIF(CommandLine.MailAuthType != "", CommandLine.MailAuthType, GlobalConfig.Email.AuthType) GlobalComputedConfig.MailAuthType = util.IIF(CommandLine.MailAuthType != "", CommandLine.MailAuthType, GlobalConfig.Email.AuthType)
GlobalComputedConfig.MailUser = util.IIF(CommandLine.MailUser != "", CommandLine.MailUser, GlobalConfig.Email.User) GlobalComputedConfig.MailUser = util.IIF(CommandLine.MailUser != "", CommandLine.MailUser, GlobalConfig.Email.User)
GlobalComputedConfig.MailPassword = util.IIF(CommandLine.MailPassword != "", CommandLine.MailPassword, GlobalConfig.Email.Password) 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 { if err != nil {
panic(err.Error()) panic(err.Error())
} }
+12
View File
@@ -17,6 +17,7 @@ site:
path: "/img/builtin/AmsterdamIcon32.png" path: "/img/builtin/AmsterdamIcon32.png"
type: "image/png" type: "image/png"
siteShortcutIcon: "/img/builtin/AmsterdamIcon32.ico" siteShortcutIcon: "/img/builtin/AmsterdamIcon32.ico"
siteAppleIcon: "/img/builtin/AmsterdamAppleIcon.png"
siteLogo: "/img/builtin/powered-by-amsterdam.png" siteLogo: "/img/builtin/powered-by-amsterdam.png"
topRefresh: 300 topRefresh: 300
loginCookieName: AmsterdamAuth loginCookieName: AmsterdamAuth
@@ -24,7 +25,10 @@ site:
sessionExpire: "3h" sessionExpire: "3h"
userAgreementResource: "useragreement.html" userAgreementResource: "useragreement.html"
policyResource: "policy.html" policyResource: "policy.html"
frameTemplate: "frame.jet"
footerTemplate: "footer.jet" footerTemplate: "footer.jet"
topMenuId: "top"
fixedMenuId: "fixed"
defaultCommunityLogo: "/img/builtin/default-community.jpg" defaultCommunityLogo: "/img/builtin/default-community.jpg"
defaultUserPhoto: "/img/builtin/no-user.png" defaultUserPhoto: "/img/builtin/no-user.png"
welcomeTitle: "Welcome to Amsterdam" welcomeTitle: "Welcome to Amsterdam"
@@ -71,6 +75,8 @@ rendering:
countryList: countryList:
prioritize: US prioritize: US
veniceCompatibleImageURLs: false veniceCompatibleImageURLs: false
panicRecovery:
stackDataSize: "4 KiB"
resources: resources:
viewTemplateDir: "" viewTemplateDir: ""
dialogTemplateDir: "" dialogTemplateDir: ""
@@ -90,6 +96,12 @@ posting:
- "image/png" - "image/png"
tuning: tuning:
workerTasks: 4 workerTasks: 4
timeouts:
httpRead: 30
httpWrite: 30
httpIdle: 120
pageExecute: 15
pageRender: 15
queues: queues:
auditWrites: 16 auditWrites: 16
contextRecycle: 16 contextRecycle: 16
+1
View File
@@ -93,6 +93,7 @@ const (
AuditAdminChangeUserAccount = 111 AuditAdminChangeUserAccount = 111
AuditAdminSetAccountSecurity = 112 AuditAdminSetAccountSecurity = 112
AuditAdminLockUnlockAccount = 113 AuditAdminLockUnlockAccount = 113
AuditAdminSetUserName = 114
AuditCommunityCreate = 201 AuditCommunityCreate = 201
AuditCommunitySetMembership = 202 AuditCommunitySetMembership = 202
AuditCommunityContactInfo = 203 AuditCommunityContactInfo = 203
+2
View File
@@ -44,6 +44,8 @@ auditReference:
text: "Admin Set Account Security" text: "Admin Set Account Security"
- code: 113 - code: 113
text: "Admin Lock/Unlock Account" text: "Admin Lock/Unlock Account"
- code: 114
text: "Admin Set User Name"
- code: 201 - code: 201
text: "Create New Community" text: "Create New Community"
- code: 202 - code: 202
+10 -7
View File
@@ -29,9 +29,9 @@ import (
// Error classifications // Error classifications
const ( const (
classUnspecified = 0 classUnspecified = iota // unspecified, barf
classNeedInstall = 1 classNeedInstall // need to install the database
classNeedConvert = 2 classNeedConvert // need to convert a Venice database
) )
// MySQL Errors // MySQL Errors
@@ -210,11 +210,11 @@ func prepareDB() (string, error) {
} }
// SetupDb sets up the database and associated items. // SetupDb sets up the database and associated items.
func SetupDb() (func(), error) { func SetupDb() (string, func(), error) {
exitfns := make([]func(), 0, 2) exitfns := make([]func(), 0, 2)
version, err := prepareDB() version, err := prepareDB()
if err != nil { if err != nil {
return nil, err return "X", nil, err
} }
db, err := sqlx.Connect(config.GlobalComputedConfig.DatabaseDriver, buildMysqlDSN(false)) db, err := sqlx.Connect(config.GlobalComputedConfig.DatabaseDriver, buildMysqlDSN(false))
if err == nil { if err == nil {
@@ -223,6 +223,7 @@ func SetupDb() (func(), error) {
if err == nil { if err == nil {
if g.Version != version { if g.Version != version {
log.Warnf("!! database version %s does not match prepared version %s", g.Version, version) log.Warnf("!! database version %s does not match prepared version %s", g.Version, version)
version = g.Version
} }
setupAdCache() setupAdCache()
setupUserCache() setupUserCache()
@@ -232,11 +233,11 @@ func SetupDb() (func(), error) {
setupConferenceCache() setupConferenceCache()
exitfns = append(exitfns, setupAuditWriter()) exitfns = append(exitfns, setupAuditWriter())
exitfns = append(exitfns, setupIPBanSweep()) exitfns = append(exitfns, setupIPBanSweep())
log.Infof("SetupDb(): database version %s", g.Version) log.Infof("SetupDb(): database version %s", version)
} }
} }
slices.Reverse(exitfns) slices.Reverse(exitfns)
return func() { return version, func() {
for _, f := range exitfns { for _, f := range exitfns {
f() f()
} }
@@ -262,6 +263,8 @@ func transaction(ctx context.Context) (*sqlx.Tx, func() error, func()) {
err = tx.Commit() err = tx.Commit()
if err == nil { if err == nil {
live = false live = false
} else {
log.Errorf("***COMMIT ERROR*** %v", err)
} }
} }
return err return err
+54 -59
View File
@@ -17,6 +17,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"slices" "slices"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -152,8 +153,8 @@ func (cs *ConferenceSettings) Save(ctx context.Context) error {
} }
// Aliases returns the list of aliases for this conference. // Aliases returns the list of aliases for this conference.
func (c *Conference) Aliases(ctx context.Context) ([]string, error) { func (c *Conference) Aliases(ctx context.Context, commid int32) ([]string, error) {
rs, err := amdb.QueryContext(ctx, "SELECT alias FROM confalias WHERE confid = ? ORDER BY alias", c.ConfId) rs, err := amdb.QueryContext(ctx, "SELECT alias FROM confalias WHERE commid = ? AND confid = ? ORDER BY alias", commid, c.ConfId)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -169,25 +170,22 @@ func (c *Conference) Aliases(ctx context.Context) ([]string, error) {
return rc, nil return rc, nil
} }
// AliasesQ returns the list of aliases for this conference, quietly.
func (c *Conference) AliasesQ(ctx context.Context) []string {
rc, _ := c.Aliases(ctx)
return rc
}
// AddAlias adds an alias to the conference. // AddAlias adds an alias to the conference.
func (c *Conference) AddAlias(ctx context.Context, alias string, u *User, comm *Community, ipaddr string) error { func (c *Conference) AddAlias(ctx context.Context, alias string, u *User, comm *Community, ipaddr string) error {
tmp := "" tmp := ""
if err := amdb.GetContext(ctx, &tmp, "SELECT alias FROM confalias WHERE confid = ? AND alias = ?", c.ConfId, alias); err != sql.ErrNoRows { if err := amdb.GetContext(ctx, &tmp, "SELECT alias FROM confalias WHERE commid = ? AND confid = ? AND alias = ?",
comm.Id, c.ConfId, alias); err != sql.ErrNoRows {
if err == nil { if err == nil {
return fmt.Errorf("the alias '%s' is already in use by another conference", alias) return fmt.Errorf("the alias '%s' is already in use by another conference", alias)
} }
return err return err
} }
if _, err := amdb.ExecContext(ctx, "INSERT INTO confalias (confid, alias) VALUES (?, ?)", c.ConfId, alias); err != nil { if _, err := amdb.ExecContext(ctx, "INSERT INTO confalias (commid, confid, alias) VALUES (?, ?, ?)",
comm.Id, c.ConfId, alias); err != nil {
return err return err
} }
conferenceAliasMap.Store(confAliasMapKey(comm.Id, alias), c.ConfId)
AmStoreAudit(AmNewCommAudit(AuditConferenceAlias, u.Uid, comm.Id, ipaddr, fmt.Sprintf("conf=%d", c.ConfId), fmt.Sprintf("add=%s", alias))) AmStoreAudit(AmNewCommAudit(AuditConferenceAlias, u.Uid, comm.Id, ipaddr, fmt.Sprintf("conf=%d", c.ConfId), fmt.Sprintf("add=%s", alias)))
return nil return nil
} }
@@ -195,21 +193,23 @@ func (c *Conference) AddAlias(ctx context.Context, alias string, u *User, comm *
// RemoveAlias removes an alias from the conference. // RemoveAlias removes an alias from the conference.
func (c *Conference) RemoveAlias(ctx context.Context, alias string, u *User, comm *Community, ipaddr string) error { func (c *Conference) RemoveAlias(ctx context.Context, alias string, u *User, comm *Community, ipaddr string) error {
aliasCount := 0 aliasCount := 0
if err := amdb.GetContext(ctx, &aliasCount, "SELECT COUNT(*) FROM confalias WHERE confid = ?", c.ConfId); err != nil { if err := amdb.GetContext(ctx, &aliasCount, "SELECT COUNT(*) FROM confalias WHERE commid = ? AND confid = ?",
comm.Id, c.ConfId); err != nil {
return err return err
} }
if aliasCount == 1 { if aliasCount == 1 {
tmp := "" tmp := ""
err := amdb.GetContext(ctx, &tmp, "SELECT alias FROM confalias WHERE confid = ? AND alias = ?", c.ConfId, alias) err := amdb.GetContext(ctx, &tmp, "SELECT alias FROM confalias WHERE commid = ? AND confid = ? AND alias = ?",
comm.Id, c.ConfId, alias)
if err == nil { if err == nil {
return errors.New("the conference must have at least one alias") return errors.New("the conference must have at least one alias")
} else if err != sql.ErrNoRows { } else if !errors.Is(err, sql.ErrNoRows) {
return err return err
} }
} }
rs, err := amdb.ExecContext(ctx, "DELETE FROM confalias WHERE confid = ? AND alias = ?", c.ConfId, alias) rs, err := amdb.ExecContext(ctx, "DELETE FROM confalias WHERE commid = ? AND confid = ? AND alias = ?", comm.Id, c.ConfId, alias)
if err != nil { if err != nil {
return err return err
} }
@@ -221,6 +221,7 @@ func (c *Conference) RemoveAlias(ctx context.Context, alias string, u *User, com
return errors.New("alias not found") return errors.New("alias not found")
} }
conferenceAliasMap.Delete(confAliasMapKey(comm.Id, alias))
AmStoreAudit(AmNewCommAudit(AuditConferenceAlias, u.Uid, comm.Id, ipaddr, fmt.Sprintf("conf=%d", c.ConfId), fmt.Sprintf("remove=%s", alias))) AmStoreAudit(AmNewCommAudit(AuditConferenceAlias, u.Uid, comm.Id, ipaddr, fmt.Sprintf("conf=%d", c.ConfId), fmt.Sprintf("remove=%s", alias)))
return nil return nil
} }
@@ -431,8 +432,8 @@ func (c *Conference) Settings(ctx context.Context, u *User) (*ConferenceSettings
} }
// Link returns a link string to this conference. // Link returns a link string to this conference.
func (c *Conference) Link(ctx context.Context, scope string) (string, error) { func (c *Conference) Link(ctx context.Context, commid int32, scope string) (string, error) {
aliases, err := c.Aliases(ctx) aliases, err := c.Aliases(ctx, commid)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -440,9 +441,9 @@ func (c *Conference) Link(ctx context.Context, scope string) (string, error) {
return fmt.Sprintf("%s.", aliases[0]), nil return fmt.Sprintf("%s.", aliases[0]), nil
} }
if scope == "global" { if scope == "global" {
comms, err := c.ContainedBy(ctx) comm, err := AmGetCommunity(ctx, commid)
if err == nil { if err == nil {
return fmt.Sprintf("%s!%s", comms[0].Alias, aliases[0]), nil return fmt.Sprintf("%s!%s", comm.Alias, aliases[0]), nil
} }
return "", err return "", err
} }
@@ -872,22 +873,22 @@ func (c *Conference) Delete(ctx context.Context, comm *Community, u *User, ipadd
// any references to conference other than this community? // any references to conference other than this community?
refCount := 0 refCount := 0
if err := tx.GetContext(ctx, &refCount, "SELECT COUNT(*) FROM commtoconf WHERE confid = ? AND commid <> ?", c.ConfId, comm.Id); err != nil { err := tx.GetContext(ctx, &refCount, "SELECT COUNT(*) FROM commtoconf WHERE confid = ? AND commid <> ?", c.ConfId, comm.Id)
if err != nil {
return err return err
} }
// break the link with the community // break the link with the community
if _, err := tx.ExecContext(ctx, "DELETE FROM commtoconf WHERE commid = ? AND confid = ?", comm.Id, c.ConfId); err != nil { if _, err = tx.ExecContext(ctx, "DELETE FROM commtoconf WHERE commid = ? AND confid = ?", comm.Id, c.ConfId); err == nil {
_, err = tx.ExecContext(ctx, "DELETE FROM confalias WHERE commid = ? AND confid = ?", comm.Id, c.ConfId)
}
if err != nil {
return err return err
} }
var err error
if refCount == 0 { if refCount == 0 {
// We have to delete all the conference core data now. // We have to delete all the conference core data now.
_, err = tx.ExecContext(ctx, "DELETE FROM confs WHERE confid = ?", c.ConfId) _, err = tx.ExecContext(ctx, "DELETE FROM confs WHERE confid = ?", c.ConfId)
if err == nil {
_, err = tx.ExecContext(ctx, "DELETE FROM confalias WHERE confid = ?", c.ConfId)
}
} }
if err != nil { if err != nil {
return err return err
@@ -901,6 +902,9 @@ func (c *Conference) Delete(ctx context.Context, comm *Community, u *User, ipadd
// kick the conference out of the cache // kick the conference out of the cache
conferenceCache.Remove(c.ConfId) conferenceCache.Remove(c.ConfId)
// simpler to just nuke the entire alias map
conferenceAliasMap.Clear()
// add an audit record // add an audit record
AmStoreAudit(AmNewCommAudit(AuditConferenceDelete, u.Uid, comm.Id, ipaddr, fmt.Sprintf("confid=%d", c.ConfId))) AmStoreAudit(AmNewCommAudit(AuditConferenceDelete, u.Uid, comm.Id, ipaddr, fmt.Sprintf("confid=%d", c.ConfId)))
@@ -955,16 +959,15 @@ func (*conferenceServiceVTable) OnDeleteCommunity(ctx context.Context, tx *sqlx.
if _, err = tx.ExecContext(ctx, "DELETE FROM commtoconf WHERE commid = ? AND confid = ?", commid, confid); err != nil { if _, err = tx.ExecContext(ctx, "DELETE FROM commtoconf WHERE commid = ? AND confid = ?", commid, confid); err != nil {
return err return err
} }
if _, err = tx.ExecContext(ctx, "DELETE FROM confalias WHERE commid = ? AND confid = ?", commid, confid); err != nil {
return err
}
if refCount > 0 { if refCount > 0 {
confids[i] = -1 confids[i] = -1
continue // done with this conference continue // done with this conference
} }
// We have to delete all the conference core data now. // We have to delete all the conference core data now.
_, err = tx.ExecContext(ctx, "DELETE FROM confs WHERE confid = ?", confid) if _, err = tx.ExecContext(ctx, "DELETE FROM confs WHERE confid = ?", confid); err != nil {
if err == nil {
_, err = tx.ExecContext(ctx, "DELETE FROM confalias WHERE confid = ?", confid)
}
if err != nil {
return err return err
} }
// kick the conference out of the cache // kick the conference out of the cache
@@ -973,7 +976,8 @@ func (*conferenceServiceVTable) OnDeleteCommunity(ctx context.Context, tx *sqlx.
getConferenceMutex.Unlock() getConferenceMutex.Unlock()
} }
// Just dump the whole conference property cache. // Just dump the whole conference alias map and property cache.
conferenceAliasMap.Clear()
getConferencePropMutex.Lock() getConferencePropMutex.Lock()
conferencePropCache.Purge() conferencePropCache.Purge()
getConferencePropMutex.Unlock() getConferencePropMutex.Unlock()
@@ -1030,27 +1034,38 @@ func AmGetConference(ctx context.Context, id int32) (*Conference, error) {
return nil, err return nil, err
} }
// confAliasMapKey creates the key for the conference alias map.
func confAliasMapKey(commid int32, alias string) string {
var b strings.Builder
b.WriteString(strconv.FormatInt(int64(commid), 10))
b.WriteByte(byte(':'))
b.WriteString(alias)
return b.String()
}
/* AmGetConferenceByAlias returns a conference given its alias. /* AmGetConferenceByAlias returns a conference given its alias.
* Parameters: * Parameters:
* ctx - Standard Go context value. * ctx - Standard Go context value.
*. commid - Community ID to look under.
* alias - The alias to look up. * alias - The alias to look up.
* Returns: * Returns:
* Pointer to the conference, or nil. * Pointer to the conference, or nil.
* Standard Go error status. * Standard Go error status.
*/ */
func AmGetConferenceByAlias(ctx context.Context, alias string) (*Conference, error) { func AmGetConferenceByAlias(ctx context.Context, commid int32, alias string) (*Conference, error) {
var confid int32 var confid int32
xconf, ok := conferenceAliasMap.Load(alias) key := confAliasMapKey(commid, alias)
xconf, ok := conferenceAliasMap.Load(key)
if ok { if ok {
confid = xconf.(int32) confid = xconf.(int32)
} else { } else {
err := amdb.GetContext(ctx, &confid, "SELECT confid FROM confalias WHERE alias = ?", alias) err := amdb.GetContext(ctx, &confid, "SELECT confid FROM confalias WHERE commid = ? AND alias = ?", commid, alias)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, fmt.Errorf("alias not found: %s", alias) return nil, fmt.Errorf("alias not found: %s", alias)
} else if err != nil { } else if err != nil {
return nil, err return nil, err
} }
conferenceAliasMap.Store(alias, confid) conferenceAliasMap.Store(key, confid)
} }
return AmGetConference(ctx, confid) return AmGetConference(ctx, confid)
} }
@@ -1074,28 +1089,6 @@ func AmGetConferenceContainingPost(ctx context.Context, postId int64) (*Conferen
return AmGetConference(ctx, confId) return AmGetConference(ctx, confId)
} }
/* AmGetConferenceByAliasInCommunity returns a conference in a community given its alias.
* Parameters:
* ctx - Standard Go context value.
* cid - The community to look inside.
* alias - The alias to look up.
* Returns:
* Pointer to the conference, or nil.
* Standard Go error status.
*/
func AmGetConferenceByAliasInCommunity(ctx context.Context, cid int32, alias string) (*Conference, error) {
var confid int32
err := amdb.GetContext(ctx, &confid, `SELECT c.confid FROM commtoconf c, confalias a WHERE c.confid = a.confid
AND c.commid = ? AND a.alias = ?`, cid, alias)
switch err {
case nil:
return AmGetConference(ctx, confid)
case sql.ErrNoRows:
return nil, errors.New("conference not found")
}
return nil, err
}
/* AmListConferences returns all conferences for a given community. /* AmListConferences returns all conferences for a given community.
* Parameters: * Parameters:
* ctx - Standard Go context value. * ctx - Standard Go context value.
@@ -1125,7 +1118,7 @@ func AmListConferences(ctx context.Context, cid int32, showHidden bool) ([]*Conf
} }
} }
for i := range rc { for i := range rc {
err := amdb.GetContext(ctx, &(rc[i].Alias), "SELECT alias FROM confalias WHERE confid = ?", rc[i].ConfId) err := amdb.GetContext(ctx, &(rc[i].Alias), "SELECT alias FROM confalias WHERE commid = ? AND confid = ?", cid, rc[i].ConfId)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1271,7 +1264,7 @@ func AmCreateConference(ctx context.Context, comm *Community, name, alias, descr
// Ensure the alias is not in use. // Ensure the alias is not in use.
var tmp int32 var tmp int32
err := tx.GetContext(ctx, &tmp, "SELECT confid FROM confalias WHERE alias = ?", alias) err := tx.GetContext(ctx, &tmp, "SELECT confid FROM confalias WHERE commid = ? AND alias = ?", comm.Id, alias)
if err == nil { if err == nil {
return nil, fmt.Errorf("the alias '%s' is already in use by a different conference", alias) return nil, fmt.Errorf("the alias '%s' is already in use by a different conference", alias)
} else if err != sql.ErrNoRows { } else if err != sql.ErrNoRows {
@@ -1295,7 +1288,8 @@ func AmCreateConference(ctx context.Context, comm *Community, name, alias, descr
} }
// Attach the alias to the conference. // Attach the alias to the conference.
if _, err = tx.ExecContext(ctx, "INSERT INTO confalias (confid, alias) VALUES (?, ?)", rc.ConfId, alias); err != nil { if _, err = tx.ExecContext(ctx, "INSERT INTO confalias (commid, confid, alias) VALUES (?, ?, ?)",
comm.Id, rc.ConfId, alias); err != nil {
return nil, err return nil, err
} }
@@ -1329,6 +1323,7 @@ func AmCreateConference(ctx context.Context, comm *Community, name, alias, descr
// Add the new conference to the cache. // Add the new conference to the cache.
conferenceCache.Add(rc.ConfId, &rc) conferenceCache.Add(rc.ConfId, &rc)
conferenceAliasMap.Store(confAliasMapKey(comm.Id, alias), rc.ConfId)
// Set the "pictures in posts" flag for the conference from the community default. // Set the "pictures in posts" flag for the conference from the community default.
fcomm, err := comm.Flags(ctx) fcomm, err := comm.Flags(ctx)
+23
View File
@@ -0,0 +1,23 @@
# Amsterdam Web Communities System
# Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
#
# SPDX-License-Identifier: MPL-2.0
#
CREATE TABLE newconfalias (
commid INT NOT NULL,
confid INT NOT NULL,
alias VARCHAR(64) NOT NULL,
PRIMARY KEY (commid, alias),
INDEX confid_x (commid, confid)
) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
INSERT INTO newconfalias (commid, confid, alias)
SELECT c.commid, c.confid, a.alias FROM commtoconf c, confalias a
WHERE c.confid = a.confid;
DROP TABLE confalias;
ALTER TABLE newconfalias RENAME TO confalias;
+48 -40
View File
@@ -253,16 +253,16 @@ 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, 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
} }
parent, err := topic.Link(ctx, scope) parent, err := topic.Link(ctx, commid, scope)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -628,47 +628,55 @@ func AmNewPost(ctx context.Context, conf *Conference, topic *Topic, user *User,
* Array of post headers, or nil. * Array of post headers, or nil.
* Standard Go error status. * Standard Go error status.
*/ */
func AmGetPublishedPosts(ctx context.Context) ([]*PostHeader, error) { func AmGetPublishedPosts(ctx context.Context) ([]*PostHeader, []*Community, error) {
// Read the globals. // Read the globals.
gv, err := AmGlobals(ctx) gv, err := AmGlobals(ctx)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
// Read the published posts. // Read the published posts.
rs, err := amdb.QueryContext(ctx, "SELECT postid FROM postpublish ORDER BY on_date DESC") rs, err := amdb.QueryContext(ctx, "SELECT commid, postid FROM postpublish ORDER BY on_date DESC")
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
// Extract post IDs to an array. // Extract post IDs to an array.
cids := make([]int32, gv.FrontPagePosts)
pids := make([]int64, gv.FrontPagePosts) pids := make([]int64, gv.FrontPagePosts)
i := 0 i := 0
for i < int(gv.FrontPagePosts) && rs.Next() { for i < int(gv.FrontPagePosts) && rs.Next() {
if err = rs.Scan(&(pids[i])); err != nil { if err = rs.Scan(&(cids[i]), &(pids[i])); err != nil {
return nil, err return nil, nil, err
} }
i++ i++
} }
if i == 0 { // no published posts, short-circuit response if i == 0 { // no published posts, short-circuit response
return make([]*PostHeader, 0), nil return make([]*PostHeader, 0), make([]*Community, 0), nil
} }
if i < int(gv.FrontPagePosts) { if i < int(gv.FrontPagePosts) {
cids = cids[:i]
pids = pids[:i] // truncate if we have fewer posts than spaces pids = pids[:i] // truncate if we have fewer posts than spaces
} }
// Build the communities return array.
comms := make([]*Community, len(cids))
for i, cid := range cids {
comms[i], _ = AmGetCommunity(ctx, cid)
}
// Use the post IDs to build a SQL statement. // Use the post IDs to build a SQL statement.
query, args, err := sqlx.In("SELECT * FROM posts WHERE postid IN (?)", pids) query, args, err := sqlx.In("SELECT * FROM posts WHERE postid IN (?)", pids)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
query = amdb.Rebind(query) query = amdb.Rebind(query)
// Use the SQL to read in all the post headers using a single database query. // Use the SQL to read in all the post headers using a single database query.
var data []PostHeader var data []PostHeader
if err = amdb.SelectContext(ctx, &data, query, args...); err != nil { if err = amdb.SelectContext(ctx, &data, query, args...); err != nil {
return nil, err return nil, nil, err
} }
if len(data) < len(pids) { if len(data) < len(pids) {
return nil, errors.New("internal error reading post headers") return nil, nil, errors.New("internal error reading post headers")
} }
// Build the return array by making sure we point to the post headers in the same order the post IDs were returned. // Build the return array by making sure we point to the post headers in the same order the post IDs were returned.
@@ -683,10 +691,10 @@ func AmGetPublishedPosts(ctx context.Context) ([]*PostHeader, error) {
} }
} }
if q < len(pids) { if q < len(pids) {
return nil, errors.New("internal error generating output") return nil, nil, errors.New("internal error generating output")
} }
return rc, nil return rc, comms, nil
} }
type PostSearchResult struct { type PostSearchResult struct {
@@ -712,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.
@@ -786,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
} }
@@ -794,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
@@ -802,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
@@ -810,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
@@ -818,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
@@ -836,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
@@ -845,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
@@ -854,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
@@ -863,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
@@ -898,13 +906,13 @@ func AmSearchPosts(ctx context.Context, searchTerms string, u *User, offset, max
if err != nil { if err != nil {
return nil, count, err return nil, count, err
} }
alias, err := conf.Aliases(ctx) alias, err := conf.Aliases(ctx, commid)
if err != nil { if err != nil {
return nil, count, err return nil, count, err
} }
// Build the post link. // Build the post link.
plink := AmCreatePostLinkContext(commAlias, alias[0], topicNum) plink := AmCreatePostLinkContext(commAlias, commid, alias[0], topicNum)
plink.FirstPost = postnum plink.FirstPost = postnum
plink.LastPost = postnum plink.LastPost = postnum
rc[i].PostLink = plink.AsString() rc[i].PostLink = plink.AsString()
+69 -45
View File
@@ -20,9 +20,29 @@ 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
CommId int32
Conference string Conference string
Topic int16 Topic int16
FirstPost int32 FirstPost int32
@@ -36,6 +56,7 @@ func (d *PostLinkData) NeedsDBVerification() bool {
// VerifyNames verifies the post link data against the database. // VerifyNames verifies the post link data against the database.
func (d *PostLinkData) VerifyNames(ctx context.Context) error { func (d *PostLinkData) VerifyNames(ctx context.Context) error {
commid := d.CommId
if d.Community != "" { if d.Community != "" {
comm, err := AmGetCommunityByAlias(ctx, d.Community) comm, err := AmGetCommunityByAlias(ctx, d.Community)
if err != nil { if err != nil {
@@ -44,9 +65,10 @@ func (d *PostLinkData) VerifyNames(ctx context.Context) error {
if comm == nil { if comm == nil {
return errors.New("community alias not found") return errors.New("community alias not found")
} }
commid = comm.Id
} }
if d.Conference != "" { if d.Conference != "" {
conf, err := AmGetConferenceByAlias(ctx, d.Conference) conf, err := AmGetConferenceByAlias(ctx, commid, d.Conference)
if err != nil { if err != nil {
return err return err
} }
@@ -118,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
} }
} }
} }
@@ -273,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")
@@ -303,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.
@@ -321,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
@@ -344,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
@@ -383,21 +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
} }
func AmCreatePostLinkContext(community string, conference string, topic int16) *PostLinkData { // AmCreatePostLinkContext creates a new empty post link context.
return &PostLinkData{ func AmCreatePostLinkContext(community string, commid int32, conference string, topic int16) *PostLinkData {
return new(PostLinkData{
Community: community, Community: community,
CommId: commid,
Conference: conference, Conference: conference,
Topic: topic, Topic: topic,
FirstPost: -1, FirstPost: -1,
LastPost: -1, LastPost: -1,
} })
} }
+23 -9
View File
@@ -25,6 +25,20 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
// The service domain names.
const (
AM_DOMAIN_COMMUNITY = "community"
)
// The service names.
const (
AM_SVC_PROFILE = "Profile"
AM_SVC_ADMIN = "Admin"
AM_SVC_SYSADMIN = "SysAdmin"
AM_SVC_CONFERENCE = "Conference"
AM_SVC_MEMBERS = "Members"
)
// ServiceVTable is a series of functions called for services on specific events. // ServiceVTable is a series of functions called for services on specific events.
type ServiceVTable interface { type ServiceVTable interface {
OnNewCommunity(context.Context, *sqlx.Tx, *Community) error OnNewCommunity(context.Context, *sqlx.Tx, *Community) error
@@ -114,13 +128,13 @@ func init() {
serviceRoot.Domains[i].seqOrder = sqo serviceRoot.Domains[i].seqOrder = sqo
serviceRoot.byName[dom.DomainName] = &(serviceRoot.Domains[i]) serviceRoot.byName[dom.DomainName] = &(serviceRoot.Domains[i])
} }
dom := serviceRoot.byName["community"] dom := serviceRoot.byName[AM_DOMAIN_COMMUNITY]
empty := emptyServiceVTable{} empty := emptyServiceVTable{}
dom.byId["Profile"].vtable = &empty dom.byId[AM_SVC_PROFILE].vtable = &empty
dom.byId["Admin"].vtable = &empty dom.byId[AM_SVC_ADMIN].vtable = &empty
dom.byId["SysAdmin"].vtable = &empty dom.byId[AM_SVC_SYSADMIN].vtable = &empty
dom.byId["Conference"].vtable = &(conferenceServiceVTable{}) dom.byId[AM_SVC_CONFERENCE].vtable = &(conferenceServiceVTable{})
dom.byId["Members"].vtable = &empty dom.byId[AM_SVC_MEMBERS].vtable = &empty
} }
// setupServicesCache sets up the services cache. // setupServicesCache sets up the services cache.
@@ -166,7 +180,7 @@ func AmGetCommunityServices(ctx context.Context, cid int32) ([]*ServiceDef, erro
if err != nil { if err != nil {
return nil, err return nil, err
} }
dom := serviceRoot.byName["community"] dom := serviceRoot.byName[AM_DOMAIN_COMMUNITY]
a := make([]*ServiceDef, 0, len(dom.Services)) a := make([]*ServiceDef, 0, len(dom.Services))
for rs.Next() { for rs.Next() {
var ndx int16 var ndx int16
@@ -198,7 +212,7 @@ func AmGetCommunityServicesTx(ctx context.Context, tx *sqlx.Tx, cid int32) ([]*S
if err != nil { if err != nil {
return nil, err return nil, err
} }
dom := serviceRoot.byName["community"] dom := serviceRoot.byName[AM_DOMAIN_COMMUNITY]
a := make([]*ServiceDef, 0, len(dom.Services)) a := make([]*ServiceDef, 0, len(dom.Services))
for rs.Next() { for rs.Next() {
var ndx int16 var ndx int16
@@ -222,7 +236,7 @@ func AmGetCommunityServicesTx(ctx context.Context, tx *sqlx.Tx, cid int32) ([]*S
* Standard Go error status. * Standard Go error status.
*/ */
func AmEstablishCommunityServices(ctx context.Context, tx *sqlx.Tx, c *Community) error { func AmEstablishCommunityServices(ctx context.Context, tx *sqlx.Tx, c *Community) error {
dom := serviceRoot.byName["community"] dom := serviceRoot.byName[AM_DOMAIN_COMMUNITY]
a := make([]*ServiceDef, 0, len(dom.Services)) a := make([]*ServiceDef, 0, len(dom.Services))
for i, svc := range dom.Services { for i, svc := range dom.Services {
if svc.Default { if svc.Default {
+4 -4
View File
@@ -43,15 +43,15 @@ 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, 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
plink, err = conf.Link(ctx, scope) plink, err = conf.Link(ctx, commid, scope)
if err == nil { if err == nil {
if strings.HasSuffix(plink, ".") { if strings.HasSuffix(plink, ".") {
return fmt.Sprintf("%s%d", plink, t.Number), nil return fmt.Sprintf("%s%d", plink, t.Number), nil
+12
View File
@@ -381,6 +381,18 @@ func (u *User) Prefs(ctx context.Context) (*UserPrefs, error) {
return u.prefs, nil return u.prefs, nil
} }
func (u *User) SetUsername(ctx context.Context, username string, setter *User, ipaddr string) error {
u.Mutex.Lock()
_, err := amdb.ExecContext(ctx, "UPDATE users SET username = ? WHERE uid = ?", username, u.Uid)
u.Mutex.Unlock()
if err == nil {
u.Username = username
AmStoreAudit(AmNewAudit(AuditAdminSetUserName, setter.Uid, ipaddr, fmt.Sprintf("uid=%d", u.Uid),
fmt.Sprintf("newname=%s", username)))
}
return err
}
/* SetProfileData sets the "profile" variables for this user. /* SetProfileData sets the "profile" variables for this user.
* Parameters: * Parameters:
* ctx - Standard Go context value. * ctx - Standard Go context value.
+17
View File
@@ -1,5 +1,22 @@
# Changelog # Changelog
## Release 0.2.0 - April 29, 2026
### Bug Fixes
* Architectural error: conference aliases now have community scope, rather than global scope.
* Fix to link for showing hidden posts.
### Enhancements
* Added database initialization, conversion from Venice, and migration scripts.
* Admin can now change the name of a user account (issue #2).
* Apple site icon added (issue #5).
### Meta-Enhancements
* Action included to automagically build binaries when a release happens.
## Release 0.1.1 - April 11, 2020 ## Release 0.1.1 - April 11, 2020
* Fixed a bug in post link resolution at post time which was causing the conference alias to be set incorrectly (issue #3). * Fixed a bug in post link resolution at post time which was causing the conference alias to be set incorrectly (issue #3).
+3
View File
@@ -234,6 +234,9 @@ func SetupMailSender() func() {
emailRenderer.AddGlobal("AmsterdamVersion", config.AMSTERDAM_VERSION) emailRenderer.AddGlobal("AmsterdamVersion", config.AMSTERDAM_VERSION)
emailRenderer.AddGlobal("AmsterdamCopyright", config.AMSTERDAM_COPYRIGHT) emailRenderer.AddGlobal("AmsterdamCopyright", config.AMSTERDAM_COPYRIGHT)
emailRenderer.AddGlobal("GlobalConfig", config.GlobalConfig) emailRenderer.AddGlobal("GlobalConfig", config.GlobalConfig)
emailRenderer.AddGlobal("PLSCOPE_COMMUNITY", database.PLSCOPE_COMMUNITY)
emailRenderer.AddGlobal("PLSCOPE_CONFERENCE", database.PLSCOPE_CONFERENCE)
emailRenderer.AddGlobal("PLSCOPE_TOPIC", database.PLSCOPE_TOPIC)
// Start the recycler. // Start the recycler.
messageRecycleBin = make(chan *amMessage, config.GlobalConfig.Tuning.Queues.EmailRecycle) messageRecycleBin = make(chan *amMessage, config.GlobalConfig.Tuning.Queues.EmailRecycle)
+1 -1
View File
@@ -88,7 +88,7 @@ func AmDeliverSubscription(ctx context.Context, comm *database.Community, conf *
vars.Set("communityName", comm.Name) vars.Set("communityName", comm.Name)
vars.Set("conferenceName", conf.Name) vars.Set("conferenceName", conf.Name)
vars.Set("topicName", topic.Name) vars.Set("topicName", topic.Name)
pl := database.AmCreatePostLinkContext(comm.Alias, confAlias, topic.Number) pl := database.AmCreatePostLinkContext(comm.Alias, comm.Id, confAlias, topic.Number)
vars.Set("topicLink", pl.AsString()) vars.Set("topicLink", pl.AsString())
vars.Set("pseud", realPseud) vars.Set("pseud", realPseud)
vars.Set("text", realText) vars.Set("text", realText)
+2 -2
View File
@@ -18,12 +18,12 @@ a Amsterdam account. Once you have completed the process, click the "Join Now"
button. You will be prompted for the "password" for this community, which is button. You will be prompted for the "password" for this community, which is
"{{ comm.JoinKey }}". You will then be able to take part in the conferences that are "{{ comm.JoinKey }}". You will then be able to take part in the conferences that are
going on in the community. going on in the community.
{{ if mode == "conference" }} {{ if mode == INVMODE_CONFERENCE }}
After you've joined the "{{ comm.Name }}" community, check out the After you've joined the "{{ comm.Name }}" community, check out the
"{{ conf.Name }}" conference. To find it, after joining the community, "{{ conf.Name }}" conference. To find it, after joining the community,
click "Conferences" on the left menu bar, then click on the click "Conferences" on the left menu bar, then click on the
"{{ conf.Name }}" conference name in the conference list. "{{ conf.Name }}" conference name in the conference list.
{{ else if mode == "topic" }} {{ else if mode == INVMODE_TOPIC }}
After you've joined the "{{ comm.Name }}" community, check out the After you've joined the "{{ comm.Name }}" community, check out the
"{{ topic.Name | raw }}" topic in the "{{ conf.Name }}" conference. To find it, "{{ topic.Name | raw }}" topic in the "{{ conf.Name }}" conference. To find it,
after joining the community, click "Conferences" on the left menu bar, then after joining the community, click "Conferences" on the left menu bar, then
+2 -2
View File
@@ -17,12 +17,12 @@ link at the top of the page, or click the "Log In" link if you already have
a Amsterdam account. Once you have completed the process, click the "Join Now" a Amsterdam account. Once you have completed the process, click the "Join Now"
button. You will then be able to take part in the conferences that are button. You will then be able to take part in the conferences that are
going on in the community. going on in the community.
{{ if mode == "conference" }} {{ if mode == INVMODE_CONFERENCE }}
After you've joined the "{{ comm.Name }}" community, check out the After you've joined the "{{ comm.Name }}" community, check out the
"{{ conf.Name }}" conference. To find it, after joining the community, "{{ conf.Name }}" conference. To find it, after joining the community,
click "Conferences" on the left menu bar, then click on the click "Conferences" on the left menu bar, then click on the
"{{ conf.Name }}" conference name in the conference list. "{{ conf.Name }}" conference name in the conference list.
{{ else if mode == "topic" }} {{ else if mode == INVMODE_TOPIC }}
After you've joined the "{{ comm.Name }}" community, check out the After you've joined the "{{ comm.Name }}" community, check out the
"{{ topic.Name | raw }}" topic in the "{{ conf.Name }}" conference. To find it, "{{ topic.Name | raw }}" topic in the "{{ conf.Name }}" conference. To find it,
after joining the community, click "Conferences" on the left menu bar, then after joining the community, click "Conferences" on the left menu bar, then
+7 -8
View File
@@ -19,10 +19,9 @@ import (
"git.erbosoft.com/amy/amsterdam/config" "git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/ui"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v5/middleware"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/time/rate"
) )
// EBUTTON is the standard error for an unknown button. // 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. * err - The error to be handled.
* c - The Echo context error is being handled on. * 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) log.Infof("-> AmErrorHandler on path %s", c.Request().URL.Path)
if c.Response().Committed { if c.Response().(*echo.Response).Committed {
return return
} }
cerr := ui.AmWithTempContext(c, func(ctxt ui.AmContext) (string, any) { 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). // 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 ui.AmWithTempContext(c, func(ctxt ui.AmContext) (string, any) {
return "error", err return "error", err
}) })
} }
// rateLimitDenyHandler is called if the rate limit is exceeded by a connection. // 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) { return ui.AmWithTempContext(c, func(ctxt ui.AmContext) (string, any) {
ctxt.VarMap().Set("identifier", identifier) ctxt.VarMap().Set("identifier", identifier)
return "ratelimit", err return "ratelimit", err
@@ -105,7 +104,7 @@ func rateLimitDenyHandler(c echo.Context, identifier string, err error) error {
// AmSetupRateLimiter sets up the rate-limiting middleware. // AmSetupRateLimiter sets up the rate-limiting middleware.
func AmSetupRateLimiter() echo.MiddlewareFunc { func AmSetupRateLimiter() echo.MiddlewareFunc {
rcfg := middleware.RateLimiterMemoryStoreConfig{ rcfg := middleware.RateLimiterMemoryStoreConfig{
Rate: rate.Limit(config.GlobalConfig.Site.RateLimit.Rate), Rate: config.GlobalConfig.Site.RateLimit.Rate,
Burst: config.GlobalConfig.Site.RateLimit.Burst, Burst: config.GlobalConfig.Site.RateLimit.Burst,
ExpiresIn: time.Duration(config.GlobalConfig.Site.RateLimit.ExpireMinutes) * time.Minute, ExpiresIn: time.Duration(config.GlobalConfig.Site.RateLimit.ExpireMinutes) * time.Minute,
} }
+14 -16
View File
@@ -330,7 +330,7 @@ func commonFindGetBackend(ctxt ui.AmContext) (string, any) {
*/ */
func FindPostsPageCommunity(ctxt ui.AmContext) (string, any) { func FindPostsPageCommunity(ctxt ui.AmContext) (string, any) {
comm := ctxt.CurrentCommunity() comm := ctxt.CurrentCommunity()
ctxt.VarMap().Set("scope", "community") ctxt.VarMap().Set("scope", database.PLSCOPE_COMMUNITY)
ctxt.VarMap().Set("entityName", comm.Name) ctxt.VarMap().Set("entityName", comm.Name)
ctxt.VarMap().Set("backlink", fmt.Sprintf("/comm/%s/conf", comm.Alias)) ctxt.VarMap().Set("backlink", fmt.Sprintf("/comm/%s/conf", comm.Alias))
ctxt.VarMap().Set("postlink", fmt.Sprintf("/comm/%s/find", comm.Alias)) ctxt.VarMap().Set("postlink", fmt.Sprintf("/comm/%s/find", comm.Alias))
@@ -345,12 +345,11 @@ func FindPostsPageCommunity(ctxt ui.AmContext) (string, any) {
* Data as a parameter for the command string. * Data as a parameter for the command string.
*/ */
func FindPostsPageConference(ctxt ui.AmContext) (string, any) { func FindPostsPageConference(ctxt ui.AmContext) (string, any) {
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", fmt.Sprintf("/comm/%s/conf/%s", comm.Alias, ctxt.GetScratch("currentAlias"))) ctxt.VarMap().Set("backlink", ctxt.GetScratch("ConferenceLink").(string))
ctxt.VarMap().Set("postlink", fmt.Sprintf("/comm/%s/conf/%s/find", comm.Alias, ctxt.GetScratch("currentAlias"))) ctxt.VarMap().Set("postlink", fmt.Sprintf("%s/find", ctxt.GetScratch("ConferenceLink")))
return commonFindGetBackend(ctxt) return commonFindGetBackend(ctxt)
} }
@@ -362,12 +361,11 @@ func FindPostsPageConference(ctxt ui.AmContext) (string, any) {
* Data as a parameter for the command string. * Data as a parameter for the command string.
*/ */
func FindPostsPageTopic(ctxt ui.AmContext) (string, any) { func FindPostsPageTopic(ctxt ui.AmContext) (string, any) {
comm := ctxt.CurrentCommunity()
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("/comm/%s/conf/%s/r/%d", comm.Alias, ctxt.GetScratch("currentAlias"), topic.Number)) ctxt.VarMap().Set("backlink", fmt.Sprintf("%s/r/%d", ctxt.GetScratch("ConferenceLink"), topic.Number))
ctxt.VarMap().Set("postlink", fmt.Sprintf("/comm/%s/conf/%s/op/%d/find", comm.Alias, ctxt.GetScratch("currentAlias"), topic.Number)) ctxt.VarMap().Set("postlink", fmt.Sprintf("%s/op/%d/find", ctxt.GetScratch("ConferenceLink"), topic.Number))
return commonFindGetBackend(ctxt) return commonFindGetBackend(ctxt)
} }
@@ -419,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))
@@ -436,10 +434,10 @@ 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", fmt.Sprintf("/comm/%s/conf/%s", comm.Alias, ctxt.GetScratch("currentAlias"))) ctxt.VarMap().Set("backlink", ctxt.GetScratch("ConferenceLink").(string))
ctxt.VarMap().Set("postlink", fmt.Sprintf("/comm/%s/conf/%s/find", comm.Alias, ctxt.GetScratch("currentAlias"))) ctxt.VarMap().Set("postlink", fmt.Sprintf("%s/find", ctxt.GetScratch("ConferenceLink")))
return commonFindPostBackend(ctxt, comm, conf, nil) return commonFindPostBackend(ctxt, comm, conf, nil)
} }
@@ -454,9 +452,9 @@ 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("/comm/%s/conf/%s/r/%d", comm.Alias, ctxt.GetScratch("currentAlias"), topic.Number)) ctxt.VarMap().Set("backlink", fmt.Sprintf("%s/r/%d", ctxt.GetScratch("ConferenceLink"), topic.Number))
ctxt.VarMap().Set("postlink", fmt.Sprintf("/comm/%s/conf/%s/op/%d/find", comm.Alias, ctxt.GetScratch("currentAlias"), topic.Number)) ctxt.VarMap().Set("postlink", fmt.Sprintf("%s/op/%d/find", ctxt.GetScratch("ConferenceLink"), topic.Number))
return commonFindPostBackend(ctxt, comm, conf, topic) return commonFindPostBackend(ctxt, comm, conf, topic)
} }
+7 -8
View File
@@ -14,13 +14,12 @@ require (
github.com/hashicorp/golang-lru v1.0.2 github.com/hashicorp/golang-lru v1.0.2
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/klauspost/lctime v0.1.0 github.com/klauspost/lctime v0.1.0
github.com/labstack/echo/v4 v4.15.1 github.com/labstack/echo/v5 v5.1.1
github.com/labstack/gommon v0.4.2 github.com/labstack/gommon v0.5.0
github.com/sirupsen/logrus v1.9.4 github.com/sirupsen/logrus v1.9.4
github.com/tkuchiki/go-timezone v0.2.3 github.com/tkuchiki/go-timezone v0.2.3
golang.org/x/net v0.52.0 golang.org/x/net v0.53.0
golang.org/x/text v0.35.0 golang.org/x/text v0.36.0
golang.org/x/time v0.15.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@@ -29,10 +28,10 @@ require (
github.com/CloudyKit/fastprinter v0.0.0-20251202014920-1725d2651bd4 // indirect github.com/CloudyKit/fastprinter v0.0.0-20251202014920-1725d2651bd4 // indirect
github.com/alexflint/go-scalar v1.2.0 // indirect github.com/alexflint/go-scalar v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.14 // 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/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // 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/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
) )
+13 -55
View File
@@ -1,28 +1,19 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c=
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
github.com/CloudyKit/fastprinter v0.0.0-20251202014920-1725d2651bd4 h1:DQ1+lDdBve+u+aovjh4wV6sYnvZKH0Hx8GaQOi4vYl8= github.com/CloudyKit/fastprinter v0.0.0-20251202014920-1725d2651bd4 h1:DQ1+lDdBve+u+aovjh4wV6sYnvZKH0Hx8GaQOi4vYl8=
github.com/CloudyKit/fastprinter v0.0.0-20251202014920-1725d2651bd4/go.mod h1:eauGmjfZG874MOAEPVeqg21mZCbTOLW+tFe8F7NpfnY= github.com/CloudyKit/fastprinter v0.0.0-20251202014920-1725d2651bd4/go.mod h1:eauGmjfZG874MOAEPVeqg21mZCbTOLW+tFe8F7NpfnY=
github.com/CloudyKit/jet/v6 v6.3.1 h1:6IAo5Cx21xrHVaR8zzXN5gJatKV/wO7Nf6bfCnCSbUw=
github.com/CloudyKit/jet/v6 v6.3.1/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw=
github.com/CloudyKit/jet/v6 v6.3.2 h1:BPaX0lnXTZ9TniICiiK/0iJqzeGJ2ibvB4DjAqLMBSM= github.com/CloudyKit/jet/v6 v6.3.2 h1:BPaX0lnXTZ9TniICiiK/0iJqzeGJ2ibvB4DjAqLMBSM=
github.com/CloudyKit/jet/v6 v6.3.2/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw= github.com/CloudyKit/jet/v6 v6.3.2/go.mod h1:lf8ksdNsxZt7/yH/3n4vJQWA9RUq4wpaHtArHhGVMOw=
github.com/alexflint/go-arg v1.6.0 h1:wPP9TwTPO54fUVQl4nZoxbFfKCcy5E6HBCumj1XVRSo=
github.com/alexflint/go-arg v1.6.0/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=
github.com/alexflint/go-arg v1.6.1 h1:uZogJ6VDBjcuosydKgvYYRhh9sRCusjOvoOLZopBlnA= github.com/alexflint/go-arg v1.6.1 h1:uZogJ6VDBjcuosydKgvYYRhh9sRCusjOvoOLZopBlnA=
github.com/alexflint/go-arg v1.6.1/go.mod h1:nQ0LFYftLJ6njcaee0sU+G0iS2+2XJQfA8I062D0LGc= github.com/alexflint/go-arg v1.6.1/go.mod h1:nQ0LFYftLJ6njcaee0sU+G0iS2+2XJQfA8I062D0LGc=
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
github.com/biter777/countries v1.7.5 h1:MJ+n3+rSxWQdqVJU8eBy9RqcdH6ePPn4PJHocVWUa+Q= github.com/biter777/countries v1.7.5 h1:MJ+n3+rSxWQdqVJU8eBy9RqcdH6ePPn4PJHocVWUa+Q=
github.com/biter777/countries v1.7.5/go.mod h1:1HSpZ526mYqKJcpT5Ti1kcGQ0L0SrXWIaptUWjFfv2E= github.com/biter777/countries v1.7.5/go.mod h1:1HSpZ526mYqKJcpT5Ti1kcGQ0L0SrXWIaptUWjFfv2E=
github.com/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJizujEZvmAM=
github.com/bits-and-blooms/bitset v1.24.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/derekparker/trie v0.0.0-20230829180723-39f4de51ef7d h1:hUWoLdw5kvo2xCsqlsIBMvWUc1QCSsCYD2J2+Fg6YoU= github.com/derekparker/trie v0.0.0-20230829180723-39f4de51ef7d h1:hUWoLdw5kvo2xCsqlsIBMvWUc1QCSsCYD2J2+Fg6YoU=
@@ -40,77 +31,44 @@ github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 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 h1:nINsuFc860M9cyYhT6vfg6U1USh7kiVBj/s/2b04U70=
github.com/klauspost/lctime v0.1.0/go.mod h1:OwdMhr8tbQvusAsnilqkkgDQqivWlqyg0w5cfXkLiDk= github.com/klauspost/lctime v0.1.0/go.mod h1:OwdMhr8tbQvusAsnilqkkgDQqivWlqyg0w5cfXkLiDk=
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= github.com/labstack/echo/v5 v5.1.1 h1:4QkvKoS8ps5ch49t8b72QS9Z581ytgxhTzxuB/CBA2I=
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= github.com/labstack/echo/v5 v5.1.1/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo=
github.com/labstack/echo/v4 v4.15.1 h1:S9keusg26gZpjMmPqB5hOEvNKnmd1lNmcHrbbH2lnFs= github.com/labstack/gommon v0.5.0 h1:6VSQ2NOzsnEJ5W6+84E0RbcaDDmgB6NIAzWCczTEe6c=
github.com/labstack/echo/v4 v4.15.1/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c= github.com/labstack/gommon v0.5.0/go.mod h1:Rzlg7HHy1maLfzBYGg9NZcVuz1sA68HHhLjhcEllYE0=
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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 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 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 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.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 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= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tkuchiki/go-timezone v0.2.3 h1:D3TVdIPrFsu9lxGxqNX2wsZwn1MZtTqTW0mdevMozHc= github.com/tkuchiki/go-timezone v0.2.3 h1:D3TVdIPrFsu9lxGxqNX2wsZwn1MZtTqTW0mdevMozHc=
github.com/tkuchiki/go-timezone v0.2.3/go.mod h1:oFweWxYl35C/s7HMVZXiA19Jr9Y0qJHMaG/J2TES4LY= github.com/tkuchiki/go-timezone v0.2.3/go.mod h1:oFweWxYl35C/s7HMVZXiA19Jr9Y0qJHMaG/J2TES4LY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 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 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
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 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 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 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA=
golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
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/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+6 -10
View File
@@ -1,6 +1,6 @@
/* /*
* Amsterdam Web Communities System * Amsterdam Web Communities System
* Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved
* *
* This Source Code Form is subject to the terms of the Mozilla Public * This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -44,17 +44,17 @@ func (d *TrieDictionary) Size() int {
// CheckWord returns true if a word is in the dictionary, false if not. // CheckWord returns true if a word is in the dictionary, false if not.
func (d *TrieDictionary) CheckWord(word string) bool { func (d *TrieDictionary) CheckWord(word string) bool {
d.mutex.Lock() d.mutex.Lock()
defer d.mutex.Unlock()
_, rc := d.trie.Find(strings.ToLower(word)) _, rc := d.trie.Find(strings.ToLower(word))
d.mutex.Unlock()
return rc return rc
} }
// AddWord adds a new word to the dictionary. // AddWord adds a new word to the dictionary.
func (d *TrieDictionary) AddWord(word string) { func (d *TrieDictionary) AddWord(word string) {
d.mutex.Lock() d.mutex.Lock()
defer d.mutex.Unlock()
d.trie.Add(strings.ToLower(word), true) d.trie.Add(strings.ToLower(word), true)
d.count++ d.count++
d.mutex.Unlock()
} }
// DelWord deletes a word from the dictionary. // DelWord deletes a word from the dictionary.
@@ -89,12 +89,8 @@ func loadDict(d *TrieDictionary, words []byte) {
// LoadTrieDict creates a TrieDictionary from a byte array that represents a word list (one word per line). // LoadTrieDict creates a TrieDictionary from a byte array that represents a word list (one word per line).
func LoadTrieDict(words []byte) *TrieDictionary { func LoadTrieDict(words []byte) *TrieDictionary {
rc := TrieDictionary{ rc := new(TrieDictionary{loaded: atomic.Bool{}, trie: trie.New(), count: 0})
loaded: atomic.Bool{},
trie: trie.New(),
count: 0,
}
rc.loaded.Store(false) rc.loaded.Store(false)
go loadDict(&rc, words) go loadDict(rc, words)
return &rc return rc
} }
+4 -6
View File
@@ -55,10 +55,8 @@ func SetupDicts() {
log.Errorf("failed to load external dictionary %s: %v", config.GlobalConfig.Posting.ExternalDictionary, err) log.Errorf("failed to load external dictionary %s: %v", config.GlobalConfig.Posting.ExternalDictionary, err)
} }
} }
rw := spellingRewriter{ rw := new(spellingRewriter{dict: NewCompositeDict(dicts)})
dict: NewCompositeDict(dicts), rewriterRegistry[rw.Name()] = rw
}
rewriterRegistry[rw.Name()] = &rw
} }
// spellingRewriter is a rewriter that flags spelling errors. // spellingRewriter is a rewriter that flags spelling errors.
@@ -89,10 +87,10 @@ func (rw *spellingRewriter) Rewrite(ctx context.Context, data string, svc rewrit
if rw.dict.CheckWord(data) { if rw.dict.CheckWord(data) {
return nil return nil
} }
return &markupData{ return new(markupData{
beginMarkup: defaultBeginError, beginMarkup: defaultBeginError,
text: data, text: data,
endMarkup: defaultEndError, endMarkup: defaultEndError,
rescan: false, rescan: false,
} })
} }
+2
View File
@@ -127,6 +127,7 @@ func buildPostLink(decoded, context *database.PostLinkData) string {
b.WriteString(context.Conference) b.WriteString(context.Conference)
} else { } else {
b.WriteString(decoded.Conference) b.WriteString(decoded.Conference)
started = true
} }
b.WriteString(".") b.WriteString(".")
if decoded.Topic == -1 { if decoded.Topic == -1 {
@@ -168,6 +169,7 @@ func (rw *postLinkRewriter) Rewrite(ctx context.Context, data string, svc rewrit
if err != nil { if err != nil {
return nil return nil
} }
mydata.CommId = ctxt.CommId
err = mydata.VerifyNames(ctx) err = mydata.VerifyNames(ctx)
if err != nil { if err != nil {
return nil return nil
+15 -5
View File
@@ -21,6 +21,13 @@ import (
"git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/ui"
) )
// Invitation modes.
const (
INVMODE_COMMUNITY = "community"
INVMODE_CONFERENCE = "conference"
INVMODE_TOPIC = "topic"
)
/* InviteToCommunity displays the community invitation form. /* InviteToCommunity displays the community invitation form.
* Parameters: * Parameters:
* ctxt - The AmContext for the request. * ctxt - The AmContext for the request.
@@ -59,7 +66,7 @@ func InviteToConference(ctxt ui.AmContext) (string, any) {
ctxt.SetFrameTitle("Send Invitation") ctxt.SetFrameTitle("Send Invitation")
ctxt.VarMap().Set("title", "Send Conference Invitation") ctxt.VarMap().Set("title", "Send Conference Invitation")
ctxt.VarMap().Set("subtitle", conf.Name) ctxt.VarMap().Set("subtitle", conf.Name)
ctxt.VarMap().Set("backlink", fmt.Sprintf("/comm/%s/conf/%s/manage", comm.Alias, ctxt.GetScratch("currentAlias"))) ctxt.VarMap().Set("backlink", fmt.Sprintf("%s/manage", ctxt.GetScratch("ConferenceLink")))
ctxt.VarMap().Set("cid", fmt.Sprintf("%d", comm.Id)) ctxt.VarMap().Set("cid", fmt.Sprintf("%d", comm.Id))
ctxt.VarMap().Set("confid", fmt.Sprintf("%d", conf.ConfId)) ctxt.VarMap().Set("confid", fmt.Sprintf("%d", conf.ConfId))
return "framed", "invite.jet" return "framed", "invite.jet"
@@ -83,7 +90,7 @@ func InviteToTopic(ctxt ui.AmContext) (string, any) {
ctxt.SetFrameTitle("Send Invitation") ctxt.SetFrameTitle("Send Invitation")
ctxt.VarMap().Set("title", "Send Topic Invitation") ctxt.VarMap().Set("title", "Send Topic Invitation")
ctxt.VarMap().Set("subtitle", topic.Name) ctxt.VarMap().Set("subtitle", topic.Name)
ctxt.VarMap().Set("backlink", fmt.Sprintf("/comm/%s/conf/%s/op/%d/manage", comm.Alias, ctxt.GetScratch("currentAlias"), topic.Number)) ctxt.VarMap().Set("backlink", fmt.Sprintf("%s/op/%d/manage", ctxt.GetScratch("ConferenceLink"), topic.Number))
ctxt.VarMap().Set("cid", fmt.Sprintf("%d", comm.Id)) ctxt.VarMap().Set("cid", fmt.Sprintf("%d", comm.Id))
ctxt.VarMap().Set("confid", fmt.Sprintf("%d", conf.ConfId)) ctxt.VarMap().Set("confid", fmt.Sprintf("%d", conf.ConfId))
ctxt.VarMap().Set("topicid", fmt.Sprintf("%d", topic.TopicId)) ctxt.VarMap().Set("topicid", fmt.Sprintf("%d", topic.TopicId))
@@ -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)
+74 -93
View File
@@ -13,104 +13,90 @@
package main package main
import ( import (
"bufio"
"bytes"
"compress/gzip" "compress/gzip"
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log/slog"
"maps"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"sync" "sync"
"time" "time"
"git.erbosoft.com/amy/amsterdam/config" "git.erbosoft.com/amy/amsterdam/config"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
glog "github.com/labstack/gommon/log"
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
/*---------------------------------------------------------------------------- /*----------------------------------------------------------------------------
* Gommon-log to logrus adapter * slog handler that outputs to Logrus
*---------------------------------------------------------------------------- *----------------------------------------------------------------------------
*/ */
/* toglog converts a Logrus logging level to a glog one. // slog2logrus converts slog levels to Logrus levels.
* Parameters: var slog2logrus = map[slog.Level]log.Level{
* l - The Logrus log level to be converted. slog.LevelDebug: log.DebugLevel,
* Returns: slog.LevelInfo: log.InfoLevel,
* The equivalent glog log level. slog.LevelWarn: log.WarnLevel,
*/ slog.LevelError: log.ErrorLevel,
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
}
} }
/* fromglog converts a glog logging level to a Logrus one. // SlogLogrusHandler implements slog.Handler and routes to Logrus.
* Parameters: type SlogLogrusHandler struct {
* l - The glog log level to be converted. fields log.Fields // fields defined in this handler
* Returns: groupPrefix string // group prefix
* 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
}
} }
// EchoLogrusAdapter implements echo.Logger using logrus. // NewSlogLogrusHandler creates a SlogLogrusHandler with base information.
type EchoLogrusAdapter struct{} func NewSlogLogrusHandler() *SlogLogrusHandler {
return new(SlogLogrusHandler{fields: make(log.Fields), groupPrefix: ""})
}
func (l *EchoLogrusAdapter) Output() io.Writer { return log.StandardLogger().Out } // Enabled returns true if the specified log level is handled.
func (l *EchoLogrusAdapter) SetOutput(w io.Writer) { log.SetOutput(w) } func (h *SlogLogrusHandler) Enabled(ctx context.Context, lvl slog.Level) bool {
func (l *EchoLogrusAdapter) Prefix() string { return "" } return log.IsLevelEnabled(slog2logrus[lvl])
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)) } // Handle sends a slog.Record to the log output.
func (l *EchoLogrusAdapter) Print(i ...any) { log.Print(i...) } func (h *SlogLogrusHandler) Handle(ctx context.Context, r slog.Record) error {
func (l *EchoLogrusAdapter) Printf(format string, args ...any) { log.Printf(format, args...) } flds := make(log.Fields)
func (l *EchoLogrusAdapter) Printj(j glog.JSON) { log.WithFields(log.Fields(j)).Print() } for k, v := range h.fields {
func (l *EchoLogrusAdapter) Debug(i ...any) { log.Debug(i...) } flds[h.groupPrefix+k] = v
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() } r.Attrs(func(a slog.Attr) bool {
func (l *EchoLogrusAdapter) Info(i ...any) { log.Info(i...) } flds[h.groupPrefix+a.Key] = a.Value.Any()
func (l *EchoLogrusAdapter) Infof(format string, args ...any) { log.Infof(format, args...) } return true
func (l *EchoLogrusAdapter) Infoj(j glog.JSON) { log.WithFields(log.Fields(j)).Info() } })
func (l *EchoLogrusAdapter) Warn(i ...any) { log.Warn(i...) } ntry := log.NewEntry(log.StandardLogger()).WithTime(r.Time).WithFields(flds)
func (l *EchoLogrusAdapter) Warnf(format string, args ...any) { log.Warnf(format, args...) } ntry.Log(slog2logrus[r.Level], r.Message)
func (l *EchoLogrusAdapter) Warnj(j glog.JSON) { log.WithFields(log.Fields(j)).Warn() } return nil
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() } // WithAttrs creates a new Handler from this one, with extra attributes.
func (l *EchoLogrusAdapter) Fatal(i ...any) { log.Fatal(i...) } func (h *SlogLogrusHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
func (l *EchoLogrusAdapter) Fatalf(format string, args ...any) { log.Fatalf(format, args...) } newh := new(SlogLogrusHandler{fields: make(log.Fields), groupPrefix: h.groupPrefix})
func (l *EchoLogrusAdapter) Fatalj(j glog.JSON) { log.WithFields(log.Fields(j)).Fatal() } maps.Copy(newh.fields, h.fields)
func (l *EchoLogrusAdapter) Panic(i ...any) { log.Panic(i...) } for _, a := range attrs {
func (l *EchoLogrusAdapter) Panicf(format string, args ...any) { log.Panicf(format, args...) } newh.fields[a.Key] = a.Value.Any()
func (l *EchoLogrusAdapter) Panicj(j glog.JSON) { log.WithFields(log.Fields(j)).Panic() } }
func (l *EchoLogrusAdapter) SetHeader(h string) {} 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 * Echo middleware adapters
@@ -119,13 +105,13 @@ func (l *EchoLogrusAdapter) SetHeader(h string) {}
// LogrusMiddleware installs Logrus logging into the Echo middleware chain. // LogrusMiddleware installs Logrus logging into the Echo middleware chain.
func LogrusMiddleware(next echo.HandlerFunc) echo.HandlerFunc { func LogrusMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c *echo.Context) error {
start := time.Now() start := time.Now()
err := next(c) err := next(c)
stop := time.Now() stop := time.Now()
req := c.Request() req := c.Request()
res := c.Response() res := c.Response().(*echo.Response)
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"remote_ip": c.RealIP(), "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 * 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. // 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
@@ -287,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()
@@ -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. // 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():
@@ -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. // 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
@@ -358,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
@@ -369,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)
+53 -22
View File
@@ -17,7 +17,10 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log/slog"
"net"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
@@ -30,24 +33,40 @@ import (
"git.erbosoft.com/amy/amsterdam/htmlcheck" "git.erbosoft.com/amy/amsterdam/htmlcheck"
"git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/ui"
"git.erbosoft.com/amy/amsterdam/util" "git.erbosoft.com/amy/amsterdam/util"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v5/middleware"
log "github.com/sirupsen/logrus" 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. // 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()
e.HideBanner = true e.Logger = slog.New(NewSlogLogrusHandler())
e.Logger = &EchoLogrusAdapter{}
e.Renderer = &ui.TemplateRenderer{} e.Renderer = &ui.TemplateRenderer{}
e.HTTPErrorHandler = AmErrorHandler e.HTTPErrorHandler = AmErrorHandler
if !config.CommandLine.DebugPanic { if !config.CommandLine.DebugPanic {
e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{
LogErrorFunc: LogrusPanicLogging, StackSize: int(config.GlobalComputedConfig.PanicRecoveryStack),
})) }))
} else { } else {
log.Warn("WARNING: --debug-panic in effect - DO NOT use this in production!") 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. // ampool is the worker pool for one-shot background tasks.
var ampool *util.WorkerPool 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 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))
} }
@@ -236,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()
@@ -258,27 +275,41 @@ 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()))
}() }()
// 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) stime := time.Since(SystemStartTime)
log.Infof("Amsterdam %s startup sequence completed in %v", config.AMSTERDAM_VERSION, stime) log.Infof("Amsterdam %s startup sequence completed in %v", config.AMSTERDAM_VERSION, stime)
// Start server // Start server
go func() { go func() {
if err := e.Start(config.GlobalComputedConfig.Listen); err != nil && err != http.ErrServerClosed { if err := sconf.Start(ctx, e); err != nil && !errors.Is(err, http.ErrServerClosed) {
e.Logger.Fatalf("shutting down the server: %v", err) 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.Done()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) log.Infof("Amsterdam shut down")
defer cancel()
if err := e.Shutdown(ctx); err != nil {
e.Logger.Fatal(err)
}
} }
+10
View File
@@ -249,6 +249,7 @@ func UserManagementForm(ctxt ui.AmContext) (string, any) {
var prefs *database.UserPrefs var prefs *database.UserPrefs
prefs, err = user.Prefs(ctxt.Ctx()) prefs, err = user.Prefs(ctxt.Ctx())
if err == nil { if err == nil {
dlg.Field("user").Value = user.Username
dlg.Field("remind").Value = user.PassReminder dlg.Field("remind").Value = user.PassReminder
dlg.Field("base_lvl").SetLevel(user.BaseLevel) dlg.Field("base_lvl").SetLevel(user.BaseLevel)
dlg.Field("verify_email").SetChecked(user.VerifyEMail) dlg.Field("verify_email").SetChecked(user.VerifyEMail)
@@ -327,6 +328,12 @@ func UserManagementSave(ctxt ui.AmContext) (string, any) {
if err == nil { if err == nil {
var prefs *database.UserPrefs var prefs *database.UserPrefs
prefs, err = user.Prefs(ctxt.Ctx()) prefs, err = user.Prefs(ctxt.Ctx())
if err == nil && user.Username != dlg.Field("user").Value {
u2, e := database.AmGetUserByName(ctxt.Ctx(), dlg.Field("user").Value, nil)
if e == nil && u2 != nil {
err = errors.New("user name is already in use")
}
}
if err == nil && !(dlg.Field("pass1").IsEmpty() && dlg.Field("pass2").IsEmpty()) { if err == nil && !(dlg.Field("pass1").IsEmpty() && dlg.Field("pass2").IsEmpty()) {
p1 := dlg.Field("pass1").Value p1 := dlg.Field("pass1").Value
if p1 == dlg.Field("pass2").Value { if p1 == dlg.Field("pass2").Value {
@@ -378,6 +385,9 @@ func UserManagementSave(ctxt ui.AmContext) (string, any) {
err = user.SaveFlags(ctxt.Ctx(), nf) err = user.SaveFlags(ctxt.Ctx(), nf)
} }
} }
if err == nil && user.Username != dlg.Field("user").Value {
err = user.SetUsername(ctxt.Ctx(), dlg.Field("user").Value, ctxt.CurrentUser(), ctxt.RemoteIP())
}
if err == nil { if err == nil {
err = user.SetProfileData(ctxt.Ctx(), dlg.Field("remind").Value, dlg.Field("dob").AsDate(), dlg.Field("descr").ValPtr(), err = user.SetProfileData(ctxt.Ctx(), dlg.Field("remind").Value, dlg.Field("dob").AsDate(), dlg.Field("descr").ValPtr(),
ctxt.CurrentUser(), ctxt.RemoteIP()) ctxt.CurrentUser(), ctxt.RemoteIP())
+15 -13
View File
@@ -24,7 +24,7 @@ import (
"git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/ui"
"github.com/CloudyKit/jet/v6" "github.com/CloudyKit/jet/v6"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -93,7 +93,7 @@ func renderSBConferences(ctx context.Context, u *database.User, sb *DisplaySideb
return err return err
} }
var a []string var a []string
if a, err = conf[i].Aliases(ctx); err != nil { if a, err = conf[i].Aliases(ctx, comm[i].Id); err != nil {
return err return err
} }
alias[i] = a[0] alias[i] = a[0]
@@ -176,9 +176,10 @@ func templateGetTopic(args jet.Arguments) reflect.Value {
// templateTopicLink returns the link string for the given topic. // templateTopicLink returns the link string for the given topic.
func templateTopicLink(args jet.Arguments) reflect.Value { func templateTopicLink(args jet.Arguments) reflect.Value {
topic := args.Get(0).Convert(reflect.TypeFor[*database.Topic]()).Interface().(*database.Topic) comm := args.Get(0).Convert(reflect.TypeFor[*database.Community]()).Interface().(*database.Community)
ctxt := args.Get(1).Convert(reflect.TypeFor[ui.AmContext]()).Interface().(ui.AmContext) topic := args.Get(1).Convert(reflect.TypeFor[*database.Topic]()).Interface().(*database.Topic)
link, _ := topic.Link(ctxt.Ctx(), "global") ctxt := args.Get(2).Convert(reflect.TypeFor[ui.AmContext]()).Interface().(ui.AmContext)
link, _ := topic.Link(ctxt.Ctx(), comm.Id, "global")
return reflect.ValueOf(link) return reflect.ValueOf(link)
} }
@@ -194,12 +195,13 @@ func TopPage(ctxt ui.AmContext) (string, any) {
ctxt.SetFrameTitle("My Front Page") ctxt.SetFrameTitle("My Front Page")
// Retrieve the published posts. // Retrieve the published posts.
hdrs, err := database.AmGetPublishedPosts(ctxt.Ctx()) hdrs, comms, err := database.AmGetPublishedPosts(ctxt.Ctx())
if err != nil { if err != nil {
return "error", err return "error", err
} }
ctxt.VarMap().Set("posts", hdrs) ctxt.VarMap().Set("posts", hdrs)
ctxt.VarMap().Set("comms", comms)
ctxt.VarMap().SetFunc("post_getText", templatePostText) ctxt.VarMap().SetFunc("post_getText", templatePostText)
ctxt.VarMap().SetFunc("post_getUserName", templateExtractUserName) ctxt.VarMap().SetFunc("post_getUserName", templateExtractUserName)
ctxt.VarMap().SetFunc("post_topic", templateGetTopic) ctxt.VarMap().SetFunc("post_topic", templateGetTopic)
@@ -283,24 +285,24 @@ func PolicyPage(ctxt ui.AmContext) (string, any) {
func JumpToShortcut(ctxt ui.AmContext) (string, any) { func JumpToShortcut(ctxt ui.AmContext) (string, any) {
link, err := database.AmDecodePostLink(ctxt.URLParam("postlink")) link, err := database.AmDecodePostLink(ctxt.URLParam("postlink"))
if err != nil { 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() 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 {
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 := "" 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"))
+6 -6
View File
@@ -27,7 +27,7 @@ import (
"git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/util" "git.erbosoft.com/amy/amsterdam/util"
"github.com/CloudyKit/jet/v6" "github.com/CloudyKit/jet/v6"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -98,7 +98,7 @@ type AmContext interface {
// amContext is the internal structure that implements AmContext. // amContext is the internal structure that implements AmContext.
type amContext struct { type amContext struct {
echoContext echo.Context echoContext *echo.Context
rendervars jet.VarMap rendervars jet.VarMap
frameTitle string frameTitle string
frameMeta map[int]map[string]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. // FormFieldValues returns all values for a specified parameter name.
func (c *amContext) FormFieldValues(name string) ([]string, error) { func (c *amContext) FormFieldValues(name string) ([]string, error) {
vals, err := c.echoContext.FormParams() vals, err := c.echoContext.FormValues()
if err != nil { if err != nil {
return make([]string, 0), err return make([]string, 0), err
} }
@@ -525,7 +525,7 @@ var amContextRecycleBin chan *amContext
* Internal Amsterdam context structure pointer, or nil. * Internal Amsterdam context structure pointer, or nil.
* Standard Go error status. * Standard Go error status.
*/ */
func newContext(ctxt echo.Context) (*amContext, error) { func newContext(ctxt *echo.Context) (*amContext, error) {
var rc *amContext var rc *amContext
tmp := freeContext.Get() tmp := freeContext.Get()
if tmp == nil { if tmp == nil {
@@ -593,7 +593,7 @@ func newContext(ctxt echo.Context) (*amContext, error) {
* Returns: * Returns:
* The associated AmContext. * The associated AmContext.
*/ */
func AmContextFromEchoContext(ctxt echo.Context) AmContext { func AmContextFromEchoContext(ctxt *echo.Context) AmContext {
myctxt := ctxt.Get("__amsterdam_context") myctxt := ctxt.Get("__amsterdam_context")
if myctxt != nil { if myctxt != nil {
rc, ok := myctxt.(*amContext) rc, ok := myctxt.(*amContext)
@@ -641,7 +641,7 @@ func setupContext() func() {
// ContextCreator is middleware that creates and recycles the AmContext. // ContextCreator is middleware that creates and recycles the AmContext.
func ContextCreator(next echo.HandlerFunc) echo.HandlerFunc { func ContextCreator(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c *echo.Context) error {
myctxt, err := newContext(c) myctxt, err := newContext(c)
if err == nil { if err == nil {
err = next(c) err = next(c)
+24 -26
View File
@@ -18,12 +18,11 @@ 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"
"git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/database"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" 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. 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,10 +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 {
log.Infof("session sweep running") 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))
@@ -353,7 +361,9 @@ func (st *amSessionStore) sweep(tick <-chan time.Time, done chan bool) {
} }
} }
st.mutex.RUnlock() st.mutex.RUnlock()
if len(zap) > 0 {
log.Infof("identified %d sessions to zap", len(zap)) log.Infof("identified %d sessions to zap", len(zap))
}
// phase 2 - get rid of the expired sessions // phase 2 - get rid of the expired sessions
for _, k := range zap { for _, k := range zap {
@@ -365,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.
@@ -380,36 +387,27 @@ 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()
} }
} }
// SessionStoreInjector is middleware that injects the session store into the context variables. // SessionStoreInjector is middleware that injects the session store into the context variables.
func SessionStoreInjector(next echo.HandlerFunc) echo.HandlerFunc { func SessionStoreInjector(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c *echo.Context) error {
c.Set("AmSessionStore", sessionStore) c.Set("AmSessionStore", sessionStore)
return next(c) return next(c)
} }
+2 -2
View File
@@ -14,6 +14,7 @@ package ui
import ( import (
"embed" "embed"
"errors"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
@@ -120,8 +121,7 @@ func AmLoadDialog(name string) (*Dialog, error) {
f, err = extDialogs.Open(fmt.Sprintf("%s.yaml", name)) f, err = extDialogs.Open(fmt.Sprintf("%s.yaml", name))
if err != nil { if err != nil {
f = nil f = nil
pe := err.(*fs.PathError) if errors.Is(err, os.ErrInvalid) || errors.Is(err, os.ErrNotExist) {
if pe.Err == os.ErrInvalid || pe.Err == os.ErrNotExist {
err = nil err = nil
} }
} }
+9
View File
@@ -15,6 +15,15 @@ title: "Modify User Account"
subtitle: "User: [USERNAME]" subtitle: "User: [USERNAME]"
action: "/sysadmin/users/[USERNAME]" action: "/sysadmin/users/[USERNAME]"
fields: fields:
- type: "header"
name: "header0"
caption: "User Information"
- type: "ams_id"
name: "user"
caption: "User Name"
required: true
size: 32
maxlength: 64
- type: "header" - type: "header"
name: "header1" name: "header1"
caption: "Security Information" caption: "Security Information"
+3 -3
View File
@@ -29,7 +29,7 @@ import (
"git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/database"
"github.com/disintegration/imaging" "github.com/disintegration/imaging"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
) )
//go:embed static_images/* //go:embed static_images/*
@@ -64,7 +64,7 @@ func mimeTypeFromFilename(filename string) string {
* Returns: * Returns:
* Standard Go error return. * Standard Go error return.
*/ */
func AmServeImage(c echo.Context) error { func AmServeImage(c *echo.Context) error {
components := strings.SplitAfter(c.Request().URL.Path, "/") components := strings.SplitAfter(c.Request().URL.Path, "/")
var err error = nil var err error = nil
if len(components) == 4 { if len(components) == 4 {
@@ -105,7 +105,7 @@ func AmServeImage(c echo.Context) error {
* Returns: * Returns:
* Standard Go error return. * Standard Go error return.
*/ */
func AmServeVeniceCompatibleImage(c echo.Context) error { func AmServeVeniceCompatibleImage(c *echo.Context) error {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err == nil { if err == nil {
var img *database.ImageStore var img *database.ImageStore
+21 -11
View File
@@ -17,20 +17,21 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings"
"git.erbosoft.com/amy/amsterdam/config" "git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/database"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// IPBanTest is middleware that handles the IP banning. // IPBanTest is middleware that handles the IP banning.
func IPBanTest(next echo.HandlerFunc) echo.HandlerFunc { func IPBanTest(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c *echo.Context) error {
// Check IP banning. // Check IP banning.
banmsg, banerr := database.AmTestIPBan(c.Request().Context(), c.RealIP()) banmsg, banerr := database.AmTestIPBan(c.Request().Context(), c.RealIP())
if banerr != nil { 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 // but let the request pass anyway
} else if banmsg != "" { } else if banmsg != "" {
amctxt := AmContextFromEchoContext(c) amctxt := AmContextFromEchoContext(c)
@@ -42,7 +43,7 @@ func IPBanTest(next echo.HandlerFunc) echo.HandlerFunc {
// CookieLoginTest is middleware that handles cookie logins. // CookieLoginTest is middleware that handles cookie logins.
func CookieLoginTest(next echo.HandlerFunc) echo.HandlerFunc { func CookieLoginTest(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c *echo.Context) error {
amctxt := AmContextFromEchoContext(c) amctxt := AmContextFromEchoContext(c)
// Check for cookie login. // Check for cookie login.
if amctxt.CurrentUser().IsAnon { if amctxt.CurrentUser().IsAnon {
@@ -76,12 +77,16 @@ func CookieLoginTest(next echo.HandlerFunc) echo.HandlerFunc {
// SetCommunity is middleware that sets the community context based on the URL. // SetCommunity is middleware that sets the community context based on the URL.
func SetCommunity(next echo.HandlerFunc) echo.HandlerFunc { func SetCommunity(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c *echo.Context) error {
ctxt := AmContextFromEchoContext(c) ctxt := AmContextFromEchoContext(c)
err := ctxt.SetCommunityContext(ctxt.URLParam("cid")) err := ctxt.SetCommunityContext(ctxt.URLParam("cid"))
if err != nil { 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/")
b.WriteString(ctxt.CurrentCommunity().Alias)
ctxt.SetScratch("CommunityLink", b.String())
ctxt.SetLeftMenu("community") ctxt.SetLeftMenu("community")
return next(c) return next(c)
} }
@@ -89,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. // ValidateConference is middleware that validates the user has access to the community's conference facility.
func ValidateConference(next echo.HandlerFunc) echo.HandlerFunc { 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)
} }
@@ -111,9 +116,9 @@ func ValidateConference(next echo.HandlerFunc) echo.HandlerFunc {
// SetConference is middleware that sets the conference context based on the URL. // SetConference is middleware that sets the conference context based on the URL.
func SetConference(next echo.HandlerFunc) echo.HandlerFunc { func SetConference(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c *echo.Context) error {
ctxt := AmContextFromEchoContext(c) ctxt := AmContextFromEchoContext(c)
conf, err := database.AmGetConferenceByAliasInCommunity(ctxt.Ctx(), ctxt.CurrentCommunity().Id, ctxt.URLParam("confid")) conf, err := database.AmGetConferenceByAlias(ctxt.Ctx(), ctxt.CurrentCommunity().Id, ctxt.URLParam("confid"))
if err != nil { if err != nil {
return AmSendPageData(c, ctxt, "error", err) return AmSendPageData(c, ctxt, "error", err)
} }
@@ -128,13 +133,18 @@ func SetConference(next echo.HandlerFunc) echo.HandlerFunc {
ctxt.SetScratch("currentConference", conf) ctxt.SetScratch("currentConference", conf)
ctxt.SetScratch("currentAlias", ctxt.URLParam("confid")) ctxt.SetScratch("currentAlias", ctxt.URLParam("confid"))
ctxt.SetScratch("levelInConference", myLevel) ctxt.SetScratch("levelInConference", myLevel)
var b strings.Builder
b.WriteString(ctxt.GetScratch("CommunityLink").(string))
b.WriteString("/conf/")
b.WriteString(ctxt.URLParam("confid"))
ctxt.SetScratch("ConferenceLink", b.String())
return next(c) return next(c)
} }
} }
// SetTopic is middleware that sets the topic context based on the URL. // SetTopic is middleware that sets the topic context based on the URL.
func SetTopic(next echo.HandlerFunc) echo.HandlerFunc { func SetTopic(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c *echo.Context) error {
ctxt := AmContextFromEchoContext(c) ctxt := AmContextFromEchoContext(c)
conf := ctxt.GetScratch("currentConference").(*database.Conference) conf := ctxt.GetScratch("currentConference").(*database.Conference)
+133 -60
View File
@@ -1,6 +1,6 @@
/* /*
* Amsterdam Web Communities System * Amsterdam Web Communities System
* Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved
* *
* This Source Code Form is subject to the terms of the Mozilla Public * This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -13,18 +13,82 @@
package ui package ui
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"runtime"
"time" "time"
"git.erbosoft.com/amy/amsterdam/config" "git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/database"
"github.com/klauspost/lctime" "github.com/klauspost/lctime"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" 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. /* AmSendPageData sends page data to the output based on the command string.
* Parameters: * Parameters:
* ctxt - The Echo context from the request. * ctxt - The Echo context from the request.
@@ -44,22 +108,37 @@ import (
* Returns: * Returns:
* Standard Go error status. * 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. // Preprocess certain commands into different ones.
httprc := http.StatusOK httprc := http.StatusOK
switch command { switch command {
case "error": case "error":
message := "" message := fmt.Sprintf("Unspecified error in %s", ctxt.Request().URL.String())
if data == nil { if data != nil {
message = fmt.Sprintf("Unspecified error in %s", ctxt.Request().URL.String()) if he, ok := data.(*echo.HTTPError); ok {
} else if he, ok := data.(*echo.HTTPError); ok {
httprc = he.Code httprc = he.Code
m1 := he.Message m1 := he.Message
e1 := he.Unwrap() e1 := he.Unwrap()
if m1 == nil || m1 == "" { if m1 == "" {
if e1 == nil { if e1 != nil {
message = fmt.Sprintf("Unspecified error in %s", ctxt.Request().URL.String())
} else {
message = e1.Error() message = e1.Error()
} }
} else { } else {
@@ -74,10 +153,11 @@ func AmSendPageData(ctxt echo.Context, amctxt AmContext, command string, data an
} else { } else {
message = fmt.Sprintf("%v", data) message = fmt.Sprintf("%v", data)
} }
}
if httprc < 400 { if httprc < 400 {
httprc = http.StatusInternalServerError httprc = http.StatusInternalServerError
} }
amctxt.SetFrameTitle("Internal Server Error") amctxt.SetFrameTitle(http.StatusText(httprc))
amctxt.VarMap().Set("error", message) amctxt.VarMap().Set("error", message)
if tmp := amctxt.GetSession("lastKnownGood"); tmp != nil { if tmp := amctxt.GetSession("lastKnownGood"); tmp != nil {
amctxt.VarMap().Set("recovery", tmp) amctxt.VarMap().Set("recovery", tmp)
@@ -98,6 +178,11 @@ func AmSendPageData(ctxt echo.Context, amctxt AmContext, command string, data an
} }
// Process commands. // 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 var err error
switch command { switch command {
case "bytes": case "bytes":
@@ -113,47 +198,7 @@ func AmSendPageData(ctxt echo.Context, amctxt AmContext, command string, data an
case "template": case "template":
err = ctxt.Render(httprc, data.(string), amctxt) err = ctxt.Render(httprc, data.(string), amctxt)
case "framed": case "framed":
if amctxt.FrameTitle() == "" { err = doFrameRender(ctxt, amctxt, httprc, data.(string))
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)
default: default:
err = fmt.Errorf("AmSendPageData(): unknown rendering type: %s", command) 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. // 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) 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 /* AmWrap wraps the Amsterdam handler function in a wrapper that implements the spec for
* Echo handler functions. * Echo handler functions.
* Parameters: * Parameters:
@@ -177,7 +250,7 @@ type AmPageFunc func(AmContext) (string, any)
* The wrapped function. * The wrapped function.
*/ */
func AmWrap(myfunc AmPageFunc) echo.HandlerFunc { func AmWrap(myfunc AmPageFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c *echo.Context) error {
ctxt := AmContextFromEchoContext(c) ctxt := AmContextFromEchoContext(c)
// Add the dynamic headers. // Add the dynamic headers.
@@ -186,16 +259,16 @@ func AmWrap(myfunc AmPageFunc) echo.HandlerFunc {
c.Response().Header().Set("Expires", expireTime) c.Response().Header().Set("Expires", expireTime)
// Exec the wrapped function. // Exec the wrapped function.
command, arg := myfunc(ctxt) command, arg := callWrappedPageFunc(myfunc, c, ctxt)
if command != "error" && command != "ipban" { if command != "error" && command != "ipban" {
ctxt.SetSession("lastKnownGood", ctxt.Locator()) ctxt.SetSession("lastKnownGood", ctxt.Locator())
} }
if err := ctxt.SaveSession(); err != nil { if err := ctxt.SaveSession(); err != nil {
c.Logger().Errorf("Session save error: %v", err) log.Errorf("Session save error: %v", err)
return err return err
} }
if err := AmSendPageData(c, ctxt, command, arg); err != nil { 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 err
} }
return nil 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. // 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 var ctxt AmContext = nil
myctxt := c.Get("__amsterdam_context") myctxt := c.Get("__amsterdam_context")
if myctxt != nil { if myctxt != nil {
@@ -225,7 +298,7 @@ func AmWithTempContext(c echo.Context, fn AmPageFunc) error {
} }
// Call the function // Call the function
command, arg := fn(ctxt) command, arg := callWrappedPageFunc(fn, c, ctxt)
// Add the dynamic headers. // Add the dynamic headers.
c.Response().Header().Set("Pragma", "No-cache") 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) c.Response().Header().Set("Expires", expireTime)
if err := AmSendPageData(c, ctxt, command, arg); err != nil { 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 err
} }
return nil return nil
+1 -1
View File
@@ -23,7 +23,7 @@ import (
"strings" "strings"
"git.erbosoft.com/amy/amsterdam/config" "git.erbosoft.com/amy/amsterdam/config"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/net/html" "golang.org/x/net/html"
) )
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

+5 -2
View File
@@ -31,7 +31,7 @@ import (
"github.com/CloudyKit/jet/v6" "github.com/CloudyKit/jet/v6"
"github.com/CloudyKit/jet/v6/loaders/embedfs" "github.com/CloudyKit/jet/v6/loaders/embedfs"
"github.com/CloudyKit/jet/v6/loaders/multi" "github.com/CloudyKit/jet/v6/loaders/multi"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -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)
@@ -363,7 +366,7 @@ type TemplateRenderer struct{}
* Returns: * Returns:
* Standard Go error status. * 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))() defer util.MeasureTime(fmt.Sprintf("ui.Render(%s)", name))()
view, err := views.GetTemplate(name) view, err := views.GetTemplate(name)
+6 -6
View File
@@ -13,11 +13,11 @@
<div class="mb-2"> <div class="mb-2">
<div class=" flex items-baseline gap-2"> <div class=" flex items-baseline gap-2">
<h1 class="text-blue-800 text-4xl font-bold">Find Posts</h1> <h1 class="text-blue-800 text-4xl font-bold">Find Posts</h1>
{{ if scope == "community" }} {{ if scope == PLSCOPE_COMMUNITY }}
<span class="text-blue-800 text-xl font-bold ml-2">in Community: {{ entityName }}</span> <span class="text-blue-800 text-xl font-bold ml-2">in Community: {{ entityName }}</span>
{{ else if scope == "conference" }} {{ else if scope == PLSCOPE_CONFERENCE }}
<span class="text-blue-800 text-xl font-bold ml-2">in Conference: {{ entityName }}</span> <span class="text-blue-800 text-xl font-bold ml-2">in Conference: {{ entityName }}</span>
{{ else if scope == "topic" }} {{ else if scope == PLSCOPE_TOPIC }}
<span class="text-blue-800 text-xl font-bold ml-2">in Topic: {{ entityName | raw }}</span> <span class="text-blue-800 text-xl font-bold ml-2">in Topic: {{ entityName | raw }}</span>
{{ end }} {{ end }}
</div> </div>
@@ -26,11 +26,11 @@
<!-- Backlink --> <!-- Backlink -->
<div class="mb-4"> <div class="mb-4">
{{ if scope == "community" }} {{ if scope == PLSCOPE_COMMUNITY }}
<a class="text-blue-700 hover:text-blue-900 text-sm font-medium" href="{{ backlink }}">Return to Conference List</a> <a class="text-blue-700 hover:text-blue-900 text-sm font-medium" href="{{ backlink }}">Return to Conference List</a>
{{ else if scope == "conference" }} {{ else if scope == PLSCOPE_CONFERENCE }}
<a class="text-blue-700 hover:text-blue-900 text-sm font-medium" href="{{ backlink }}">Return to Topic List</a> <a class="text-blue-700 hover:text-blue-900 text-sm font-medium" href="{{ backlink }}">Return to Topic List</a>
{{ else if scope == "topic" }} {{ else if scope == PLSCOPE_TOPIC }}
<a class="text-blue-700 hover:text-blue-900 text-sm font-medium" href="{{ backlink }}">Return to Topic</a> <a class="text-blue-700 hover:text-blue-900 text-sm font-medium" href="{{ backlink }}">Return to Topic</a>
{{ end }} {{ end }}
</div> </div>
+1
View File
@@ -17,6 +17,7 @@
<title>{{ .FrameTitle() | raw }} - {{ GlobalConfig.Site.Title }}</title> <title>{{ .FrameTitle() | raw }} - {{ GlobalConfig.Site.Title }}</title>
<link rel="icon" href="{{ GlobalConfig.Site.SiteIcon.Path }}" type="{{ GlobalConfig.Site.SiteIcon.Type }}" /> <link rel="icon" href="{{ GlobalConfig.Site.SiteIcon.Path }}" type="{{ GlobalConfig.Site.SiteIcon.Type }}" />
<link rel="shortcut icon" href="{{ GlobalConfig.Site.SiteShortcutIcon }}" /> <link rel="shortcut icon" href="{{ GlobalConfig.Site.SiteShortcutIcon }}" />
<link rel="apple-touch-icon" href="{{ GlobalConfig.Site.SiteAppleIcon }}" />
{{ range k, v := .FrameMetadata(0) }} {{ range k, v := .FrameMetadata(0) }}
<meta http-equiv="{{ k }}" content="{{ v }}"> <meta http-equiv="{{ k }}" content="{{ v }}">
{{ end }} {{ end }}
+2 -2
View File
@@ -117,8 +117,8 @@
{{ post_cur = p }} {{ post_cur = p }}
{{ post_userName = post_getUserName(p, .) }} {{ post_userName = post_getUserName(p, .) }}
{{ post_text = post_getText(p, .) }} {{ post_text = post_getText(p, .) }}
{{ post_overrideLine = post_getOverrideLine(p, .) }} {{ post_overrideLine = post_getOverrideLine(p, advancedControls, .) }}
{{ post_overrideLink = post_getOverrideLink(p, post_topicPermalink) }} {{ post_overrideLink = post_getOverrideLink(p, advancedControls, post_topicLink) }}
{{ post_attach = post_getAttachmentInfo(p, .) }} {{ post_attach = post_getAttachmentInfo(p, .) }}
{{ post_bozo = post_isBozo(p, post_topic, .) }} {{ post_bozo = post_isBozo(p, post_topic, .) }}
{{ if showPics }} {{ if showPics }}
+2 -2
View File
@@ -32,8 +32,8 @@
{{ post_cur = p }} {{ post_cur = p }}
{{ post_userName = post_getUserName(p, .) }} {{ post_userName = post_getUserName(p, .) }}
{{ post_text = post_getText(p, .) }} {{ post_text = post_getText(p, .) }}
{{ post_overrideLine = post_getOverrideLine(p, .) }} {{ post_overrideLine = post_getOverrideLine(p, true, .) }}
{{ post_overrideLink = post_getOverrideLink(p, post_topicPermalink) }} {{ post_overrideLink = post_getOverrideLink(p, true, post_topicLink) }}
{{ post_attach = post_getAttachmentInfo(p, .) }} {{ post_attach = post_getAttachmentInfo(p, .) }}
{{ post_bozo = post_isBozo(p, post_topic, .) }} {{ post_bozo = post_isBozo(p, post_topic, .) }}
{{ include "singlepost.jet" }} {{ include "singlepost.jet" }}
+1 -1
View File
@@ -33,7 +33,7 @@
{{ user = post_getUserName(p, .) }} {{ user = post_getUserName(p, .) }}
{{ text = post_getText(p, .) }} {{ text = post_getText(p, .) }}
{{ topic = post_topic(p, .) }} {{ topic = post_topic(p, .) }}
{{ link = post_topicLink(topic, .) }} {{ link = post_topicLink(comms[i], topic, .) }}
<div class="text-black text-sm"> <div class="text-black text-sm">
<div class="mb-2"> <div class="mb-2">
<strong>{{ p.Pseud | raw }}</strong> <strong>{{ p.Pseud | raw }}</strong>
+2 -2
View File
@@ -26,7 +26,7 @@ import (
"git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/ui"
"git.erbosoft.com/amy/amsterdam/util" "git.erbosoft.com/amy/amsterdam/util"
"github.com/biter777/countries" "github.com/biter777/countries"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v5"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -382,7 +382,7 @@ func ShowProfile(ctxt ui.AmContext) (string, any) {
// Gather the info on the current user. // Gather the info on the current user.
user, err := database.AmGetUserByName(ctxt.Ctx(), ctxt.URLParam("uname"), nil) user, err := database.AmGetUserByName(ctxt.Ctx(), ctxt.URLParam("uname"), nil)
if err != 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()) ci, err := user.ContactInfo(ctxt.Ctx())
if err != nil { if err != nil {
-12
View File
@@ -13,7 +13,6 @@
package util package util
import ( import (
"net"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@@ -172,17 +171,6 @@ func Map[A, B any](in []A, fn func(A) B) []B {
return rc return rc
} }
// MyIPAddress returns the local IP address of this machine.
func MyIPAddress() (net.IP, error) {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
return nil, err
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP, nil
}
// IIF is an "immediate-if" function returning its second argument if the first one is true, the third one if not. // IIF is an "immediate-if" function returning its second argument if the first one is true, the third one if not.
func IIF[A any](expr bool, v1, v2 A) A { func IIF[A any](expr bool, v1, v2 A) A {
if expr { if expr {