diff --git a/communityadmin.go b/communityadmin.go index c52597f..4d3f962 100644 --- a/communityadmin.go +++ b/communityadmin.go @@ -11,6 +11,7 @@ package main import ( + "context" "errors" "fmt" "net/http" @@ -334,12 +335,12 @@ func EditCommunityLogo(ctxt ui.AmContext) (string, any, error) { } defer func() { if happy { - go func() { + ampool.Submit(func(context.Context) { err := database.AmDeleteImage(int32(id)) if err != nil { log.Errorf("unable to delete image ID %d: %v", id, err) } - }() + }) } }() } diff --git a/conference.go b/conference.go index 6bfb0db..7a6869b 100644 --- a/conference.go +++ b/conference.go @@ -11,6 +11,7 @@ package main import ( + "context" "errors" "fmt" "io" @@ -324,24 +325,61 @@ func AttachmentUpload(ctxt ui.AmContext) (string, any, error) { return ui.ErrorPage(ctxt, errors.New("invalid button clicked on form")) } +func breakRange(topic *database.Topic, into []int32, param string, sep string) error { + rstr := strings.Split(param, sep) + if len(rstr) == 0 { + return fmt.Errorf("posts not found: %s in topic %d", param, topic.Number) + } + v, err := strconv.ParseInt(rstr[0], 10, 32) + if err != nil { + return fmt.Errorf("posts not found: %s in topic %d", param, topic.Number) + } + into[0] = int32(v) + if len(rstr) > 1 { + v, err = strconv.ParseInt(rstr[1], 10, 32) + if err != nil { + return fmt.Errorf("posts not found: %s in topic %d", param, topic.Number) + } + into[1] = int32(v) + } else { + into[1] = into[0] + } + if into[1] < 0 { + into[1] = topic.TopMessage + } + if into[0] > into[1] { + t := into[0] + into[0] = into[1] + into[1] = t + } + into[0] = max(into[0], 0) + into[1] = min(into[1], topic.TopMessage) + return nil +} + func ReadPosts(ctxt ui.AmContext) (string, any, error) { - // If we need to reset a topic's last read count (as with "Next & Keep New"), spin the task off into a goroutine. + // If we need to reset a topic's last read count (as with "Next & Keep New"), spin the task off. if ctxt.HasParameter("rst") { rst := strings.Split(ctxt.Parameter("rst"), ",") if len(rst) >= 2 { - topicId, e1 := strconv.ParseInt(rst[0], 10, 32) - lastRead, e2 := strconv.ParseInt(rst[1], 10, 32) - if e1 == nil && e2 == nil { - user := ctxt.CurrentUser() - go func() { + user := ctxt.CurrentUser() + ampool.Submit(func(context.Context) { + topicId, e1 := strconv.ParseInt(rst[0], 10, 32) + lastRead, e2 := strconv.ParseInt(rst[1], 10, 32) + if e1 == nil && e2 == nil { topic, _ := database.AmGetTopic(int32(topicId)) if topic != nil { topic.SetLastRead(user, int32(lastRead)) } - }() - } + } + }) } } + // Get user prefs. + prefs, err := ctxt.CurrentUser().Prefs() + if err != nil { + return ui.ErrorPage(ctxt, err) + } // Locate community, conference, and topic. comm := ctxt.CurrentCommunity() conf := ctxt.GetScratch("currentConference").(*database.Conference) @@ -355,44 +393,25 @@ func ReadPosts(ctxt ui.AmContext) (string, any, error) { } // Determine the range of posts to display. The "pin" is the post number after which we display the horizontal line separating old and new posts. + lastRead, err := topic.GetLastRead(ctxt.CurrentUser()) + if err != nil { + ctxt.SetRC(http.StatusNotFound) + return ui.ErrorPage(ctxt, fmt.Errorf("posts not found in topic %d - %v", topic.Number, err)) + } postRange := make([]int32, 2) var pin int32 = -1 + resetLastRead := false if ctxt.HasParameter("r") { - rstr := strings.Split(ctxt.Parameter("r"), ",") - if len(rstr) == 0 { + if err := breakRange(topic, postRange, ctxt.Parameter("r"), ","); err != nil { ctxt.SetRC(http.StatusNotFound) - return ui.ErrorPage(ctxt, fmt.Errorf("posts not found: %s in topic %d", ctxt.Parameter("r"), topic.Number)) + return ui.ErrorPage(ctxt, err) } - v, err := strconv.ParseInt(rstr[0], 10, 32) - if err != nil { + } else if ctxt.HasParameter("rgo") { + if err := breakRange(topic, postRange, ctxt.Parameter("rgo"), "-"); err != nil { ctxt.SetRC(http.StatusNotFound) - return ui.ErrorPage(ctxt, fmt.Errorf("posts not found: %s in topic %d", ctxt.Parameter("r"), topic.Number)) - } - postRange[0] = int32(v) - if len(rstr) > 1 { - v, err = strconv.ParseInt(rstr[1], 10, 32) - if err != nil { - ctxt.SetRC(http.StatusNotFound) - return ui.ErrorPage(ctxt, fmt.Errorf("posts not found: %s in topic %d", ctxt.Parameter("r"), topic.Number)) - } - postRange[1] = int32(v) - } else { - postRange[1] = postRange[0] - } - if postRange[1] < 0 { - postRange[1] = topic.TopMessage - } - if postRange[0] > postRange[1] { - t := postRange[0] - postRange[0] = postRange[1] - postRange[1] = t + return ui.ErrorPage(ctxt, err) } } else { - lastRead, err := topic.GetLastRead(ctxt.CurrentUser()) - if err != nil { - ctxt.SetRC(http.StatusNotFound) - return ui.ErrorPage(ctxt, fmt.Errorf("posts not found in topic %d - %v", topic.Number, err)) - } postRange[0] = lastRead + 1 postRange[1] = topic.TopMessage count := postRange[1] - postRange[0] + 1 @@ -400,11 +419,49 @@ func ReadPosts(ctxt ui.AmContext) (string, any, error) { postRange[0] = postRange[1] - ctxt.Globals().PostsPerPage + 1 } else if count < ctxt.Globals().PostsPerPage { pin = postRange[0] - 1 - postRange[0] -= ctxt.Globals().OldPostsAtTop - postRange[0] = max(0, postRange[0]) + postRange[0] = max(0, postRange[0]-ctxt.Globals().OldPostsAtTop) if pin < postRange[0] { pin = -1 } } + resetLastRead = true } + + // Load the actual posts. + posts, err := database.AmGetPostRange(topic, postRange[0], postRange[1]) + if err != nil { + return ui.ErrorPage(ctxt, fmt.Errorf("internal error getting posts <%d:%d-%d> - %v", topic.Number, postRange[0], postRange[1], err)) + } + + // Determine other required data. + loc := prefs.Localizer() + summaryLine := fmt.Sprintf("%d Total; %d New; Last: %s", topic.TopMessage+1, topic.TopMessage-lastRead, loc.Strftime("%b %e, %Y %r", topic.LastUpdate)) + plc := database.AmCreatePostLinkContext(comm.Alias, ctxt.GetScratch("currentAlias").(string), topic.Number) + topicPostRef := plc.AsString() + plc.FirstPost = postRange[0] + plc.LastPost = postRange[1] + postsPostRef := plc.AsString() + + // Render the output. + ctxt.VarMap().Set("stem", fmt.Sprintf("/comm/%s/conf/%s", comm.Alias, ctxt.GetScratch("currentAlias").(string))) + ctxt.VarMap().Set("topicName", topic.Name) + ctxt.VarMap().Set("summaryLine", summaryLine) + ctxt.VarMap().Set("lastRead", lastRead) + ctxt.VarMap().Set("pageSize", ctxt.Globals().PostsPerPage) + ctxt.VarMap().Set("post_max", topic.TopMessage) + ctxt.VarMap().Set("posts", posts) + ctxt.VarMap().Set("postsPermalink", fmt.Sprintf("/go/%s", postsPostRef)) + ctxt.VarMap().Set("pin", pin) + ctxt.VarMap().Set("rangeEnd", postRange[1]) + ctxt.VarMap().Set("rangeStart", postRange[0]) + ctxt.VarMap().Set("topicNum", topic.Number) + ctxt.VarMap().Set("topicPermalink", fmt.Sprintf("/go/%s", topicPostRef)) + ctxt.VarMap().Set("amsterdam_pageTitle", fmt.Sprintf("%s: %s", topic.Name, summaryLine)) + if resetLastRead { + user := ctxt.CurrentUser() + ampool.Submit(func(context.Context) { + topic.SetLastRead(user, topic.TopMessage) + }) + } + return "framed_template", "posts.jet", nil } diff --git a/database/post.go b/database/post.go index 52e3ed0..0e97a93 100644 --- a/database/post.go +++ b/database/post.go @@ -49,3 +49,12 @@ func AmGetPost(postId int64) (*PostHeader, error) { } return &(dbdata[0]), nil } + +func AmGetPostRange(topic *Topic, first, last int32) ([]PostHeader, error) { + var rc []PostHeader + err := amdb.Select(&rc, "SELECT * FROM posts WHERE topicid = ? AND num >= ? AND num <= ? ORDER BY num", topic.TopicId, first, last) + if err != nil { + return nil, err + } + return rc, nil +} diff --git a/database/post_link.go b/database/post_link.go index 001c733..5fd8ba9 100644 --- a/database/post_link.go +++ b/database/post_link.go @@ -11,6 +11,7 @@ package database import ( "errors" + "fmt" "math" "strconv" "strings" @@ -53,6 +54,43 @@ func (d *PostLinkData) VerifyNames() error { return nil } +// AsString converts the post link data to a string reference. +func (d *PostLinkData) AsString() string { + var b strings.Builder + if d.Community != "" { + b.WriteString(d.Community) + b.WriteString("!") + } + wrote := false + if d.Conference != "" { + b.WriteString(d.Conference) + b.WriteString(".") + wrote = true + } + needDot := false + if d.Topic > 0 { + needDot = true + b.WriteString(fmt.Sprintf("%d", d.Topic)) + if !wrote { + b.WriteString(".") + needDot = false + } + } + if d.FirstPost >= 0 { + s := "" + if d.LastPost < 0 || d.LastPost == d.FirstPost { + s = fmt.Sprintf("%d", d.FirstPost) + } else { + s = fmt.Sprintf("%d-%d", d.FirstPost, d.LastPost) + } + if needDot { + b.WriteString(".") + } + b.WriteString(s) + } + return b.String() +} + // Maximum lengths of the components. const ( maxLinkLength = 130 diff --git a/main.go b/main.go index d6827d7..8ff8fc7 100644 --- a/main.go +++ b/main.go @@ -23,6 +23,7 @@ import ( "git.erbosoft.com/amy/amsterdam/email" "git.erbosoft.com/amy/amsterdam/htmlcheck" "git.erbosoft.com/amy/amsterdam/ui" + "git.erbosoft.com/amy/amsterdam/util" "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" @@ -102,6 +103,9 @@ func setupEcho() *echo.Echo { return e } +// ampool is the worker pool for one-shot background tasks. +var ampool *util.WorkerPool + // main is Ye Olde Main Function. func main() { // Configure the system. @@ -127,6 +131,13 @@ func main() { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) defer stop() + // Set up ampool. + ampool = util.AmNewPool(ctx, 4, 128) + go func() { + <-ctx.Done() + ampool.Shutdown() + }() + // Start server go func() { if err := e.Start(":1323"); err != nil && err != http.ErrServerClosed { diff --git a/ui/amcontext.go b/ui/amcontext.go index 12e2fe3..34f044a 100644 --- a/ui/amcontext.go +++ b/ui/amcontext.go @@ -12,6 +12,7 @@ package ui import ( "bytes" + "context" "fmt" "mime/multipart" "net/http" @@ -38,6 +39,7 @@ type AmContext interface { ClearCommunityContext() ClearLoginCookie() ClearSession() + Ctx() context.Context CurrentCommunity() *database.Community CurrentUser() *database.User CurrentUserId() int32 @@ -123,6 +125,11 @@ func (c *amContext) ClearSession() { c.effectiveLevel = 0 } +// Ctx returns the current context.Context for the request. +func (c *amContext) Ctx() context.Context { + return c.echoContext.Request().Context() +} + // CurrentCommunity returns the current community, if one's been set. func (c *amContext) CurrentCommunity() *database.Community { if c.community == nil { @@ -213,11 +220,9 @@ func (c *amContext) HasParameter(name string) bool { if s != "" { return true } - if c.echoContext.Request().Method == "POST" { - s = c.echoContext.FormValue(name) - if s != "" { - return true - } + s = c.echoContext.FormValue(name) + if s != "" { + return true } return false } @@ -255,7 +260,7 @@ func (c *amContext) OutputType() string { */ func (c *amContext) Parameter(name string) string { rc := c.echoContext.QueryParam(name) - if rc == "" && c.echoContext.Request().Method == "POST" { + if rc == "" { rc = c.echoContext.FormValue(name) } return rc diff --git a/ui/middleware.go b/ui/middleware.go index e6281dc..603434e 100644 --- a/ui/middleware.go +++ b/ui/middleware.go @@ -135,6 +135,7 @@ func SetConference(next echo.HandlerFunc) echo.HandlerFunc { myLevel = lvl } ctxt.SetScratch("currentConference", conf) + ctxt.SetScratch("currentAlias", ctxt.URLParam("confid")) ctxt.SetScratch("levelInConference", myLevel) return next(c) } diff --git a/ui/views/posts.jet b/ui/views/posts.jet index e5d628d..2a89f8a 100644 --- a/ui/views/posts.jet +++ b/ui/views/posts.jet @@ -18,7 +18,7 @@
This is a test. -This is only a test. -If this had been an actual emergency, we would all be -dead by now.-
Lorem ipsum dolor sit amet, consectetur adipiscing elit. -Nulla maximus, quam sit amet dictum tristique, mi nibh -tempor dolor, pretium finibus purus nunc nec mauris. In -hendrerit a erat at sodales. Fusce dictum metus id -augue interdum dapibus. Mauris maximus elementum arcu -eu ultricies. Nullam mollis lorem ac ipsum accumsan -tincidunt. Proin gravida nibh fringilla tellus gravida, -viverra pellentesque metus luctus. Vivamus quis pretium -magna...-
Let's not worry about all this.-
This is a test. +This is only a test. +If this had been an actual emergency, we would all be +dead by now.+