From 46a87defeec477b742f09296c7a862e7c6b9d7bc Mon Sep 17 00:00:00 2001 From: Azareal Date: Mon, 27 Apr 2020 22:41:55 +1000 Subject: [PATCH] optimise topic pages optimise profiles simple nil ptr handling in template boolean expressions --- common/null_user_cache.go | 4 + common/pages.go | 2 +- common/reply.go | 13 +- common/reply_store.go | 1 - common/template_init.go | 34 +-- common/templates/context.go | 38 +++- common/templates/templates.go | 234 ++++++++++++++++--- common/topic.go | 392 +++++++++++++++++++++++++++----- common/user_cache.go | 16 +- common/user_store.go | 48 +++- routes/profile.go | 22 +- routes/reply.go | 66 +++--- routes/topic.go | 12 +- templates/panel_group_menu.html | 2 +- templates/panel_users.html | 4 +- templates/topic_alt_inner.html | 33 +-- templates/topic_alt_poll.html | 25 ++ templates/topic_alt_posts.html | 32 +-- templates/topic_inner.html | 16 +- templates/topic_poll.html | 12 +- 20 files changed, 756 insertions(+), 250 deletions(-) create mode 100644 templates/topic_alt_poll.html diff --git a/common/null_user_cache.go b/common/null_user_cache.go index 47dd68c0..7cb60ac1 100644 --- a/common/null_user_cache.go +++ b/common/null_user_cache.go @@ -16,6 +16,10 @@ func (c *NullUserCache) DeallocOverflow(evictPriority bool) (evicted int) { func (c *NullUserCache) Get(id int) (*User, error) { return nil, ErrNoRows } + +func (c *NullUserCache) Getn(id int) *User { + return nil +} func (c *NullUserCache) BulkGet(ids []int) (list []*User) { return make([]*User, len(ids)) } diff --git a/common/pages.go b/common/pages.go index 2774bbf5..48ed3049 100644 --- a/common/pages.go +++ b/common/pages.go @@ -192,7 +192,7 @@ type TopicPage struct { ItemList []*ReplyUser Topic TopicUser Forum *Forum - Poll Poll + Poll *Poll Paginator } diff --git a/common/reply.go b/common/reply.go index a06ea4ae..fbaf6cf5 100644 --- a/common/reply.go +++ b/common/reply.go @@ -29,6 +29,7 @@ type ReplyUser struct { URL string //URLPrefix string //URLName string + Group int Level int ActionIcon string @@ -37,11 +38,11 @@ type ReplyUser struct { } type Reply struct { - ID int - ParentID int - Content string - CreatedBy int - Group int + ID int + ParentID int + Content string + CreatedBy int + //Group int CreatedAt time.Time LastEdit int LastEditBy int @@ -49,7 +50,7 @@ type Reply struct { IP string Liked bool LikeCount int - AttachCount int + AttachCount uint16 ActionType string } diff --git a/common/reply_store.go b/common/reply_store.go index 169d4c1f..819eee90 100644 --- a/common/reply_store.go +++ b/common/reply_store.go @@ -105,7 +105,6 @@ func (s *SQLReplyStore) Create(t *Topic, content, ip string, uid int) (id int, e if err != nil { return 0, err } - lastID, err := res.LastInsertId() if err != nil { return 0, err diff --git a/common/template_init.go b/common/template_init.go index d6c11f15..afc5916a 100644 --- a/common/template_init.go +++ b/common/template_init.go @@ -227,7 +227,7 @@ func compileCommons(c *tmpl.CTemplateSet, head, head2 *Header, forumList []Forum }*/ var topicsList []*TopicsRow - topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, user3.ID, 1, 1, "", "127.0.0.1", 1, 0, 1, 1, 0, "classname", 0, "", user2, "", 0, user3, "General", "/forum/general.2", nil}) + topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, user3.ID, 1, 1, "", "::1", 1, 0, 1, 1, 0, "classname", 0, "", user2, "", 0, user3, "General", "/forum/general.2", nil}) topicListPage := TopicListPage{htitle("Topic List"), topicsList, forumList, Config.DefaultForum, TopicListSort{"lastupdated", false}, Paginator{[]int{1}, 1, 1}} o.Add("topics", "c.TopicListPage", topicListPage) o.Add("topics_mini", "c.TopicListPage", topicListPage) @@ -246,11 +246,11 @@ func compileCommons(c *tmpl.CTemplateSet, head, head2 *Header, forumList []Forum topic := TopicUser{1, "blah", "Blah", "Hey there!", 0, false, false, now, now, 1, 1, 0, "", "127.0.0.1", 1, 0, 1, 0, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", 58, false, miniAttach, nil, false} var replyList []*ReplyUser - reply := Reply{1, 1, "Yo!", 1, Config.DefaultGroup, now, 0, 0, 1, "::1", true, 1, 1, ""} - ru := &ReplyUser{ClassName: "", Reply: reply, CreatedByName: "Alice", Avatar: avatar, Level: 0, Attachments: miniAttach} - ru.Init() + reply := Reply{1, 1, "Yo!", 1 /*, Config.DefaultGroup*/, now, 0, 0, 1, "::1", true, 1, 1, ""} + ru := &ReplyUser{ClassName: "", Reply: reply, CreatedByName: "Alice", Avatar: avatar, Group: Config.DefaultGroup, Level: 0, Attachments: miniAttach} + ru.Init(user2) replyList = append(replyList, ru) - tpage := TopicPage{htitle("Topic Name"), replyList, topic, &Forum{ID: 1, Name: "Hahaha"}, poll, Paginator{[]int{1}, 1, 1}} + tpage := TopicPage{htitle("Topic Name"), replyList, topic, &Forum{ID: 1, Name: "Hahaha"}, &poll, Paginator{[]int{1}, 1, 1}} tpage.Forum.Link = BuildForumURL(NameToSlug(tpage.Forum.Name), tpage.Forum.ID) o.Add("topic", "c.TopicPage", tpage) o.Add("topic_mini", "c.TopicPage", tpage) @@ -276,20 +276,24 @@ func compileTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName string //topic := TopicUser{1, "blah", "Blah", "Hey there!", 0, false, false, now, now, 1, 1, 0, "", "127.0.0.1", 1, 0, 1, 0, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", "", "", 58, false, miniAttach, nil} // TODO: Do we want the UID on this to be 0? //avatar, microAvatar = BuildAvatar(0, "") - reply := Reply{1, 1, "Yo!", 1, Config.DefaultGroup, now, 0, 0, 1, "::1", true, 1, 1, ""} - ru := &ReplyUser{ClassName: "", Reply: reply, CreatedByName: "Alice", Avatar: "", Level: 0, Attachments: miniAttach} - ru.Init() + reply := Reply{1, 1, "Yo!", 1 /*, Config.DefaultGroup*/, now, 0, 0, 1, "::1", true, 1, 1, ""} + ru := &ReplyUser{ClassName: "", Reply: reply, CreatedByName: "Alice", Avatar: "", Group: Config.DefaultGroup, Level: 0, Attachments: miniAttach} + ru.Init(user) replyList = append(replyList, ru) // TODO: Use a dummy forum list to avoid o(n) problems - var forumList []Forum + /*var forumList []Forum forums, err := Forums.GetAll() if err != nil { return err } for _, forum := range forums { forumList = append(forumList, *forum) - } + }*/ + forum := BlankForum(1, "/forum/d.1", "d", "d desc", true, "", 0, "", 1) + forum.LastTopic = BlankTopic() + forum.LastReplyer = BlankUser() + forumList := []Forum{*forum} // Convienience function to save a line here and there htitle := func(name string) *Header { @@ -297,7 +301,7 @@ func compileTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName string return header } t := TItemHold(make(map[string]TItem)) - err = compileCommons(c, header, header2, forumList, t) + err := compileCommons(c, header, header2, forumList, t) if err != nil { return err } @@ -547,14 +551,14 @@ func compileJSTemplates(wg *sync.WaitGroup, c *tmpl.CTemplateSet, themeName stri var replyList []*ReplyUser // TODO: Do we really want the UID here to be zero? avatar, microAvatar = BuildAvatar(0, "") - reply := Reply{1, 1, "Yo!", 1, Config.DefaultGroup, now, 0, 0, 1, "::1", true, 1, 1, ""} - ru := &ReplyUser{ClassName: "", Reply: reply, CreatedByName: "Alice", Avatar: avatar, Level: 0, Attachments: miniAttach} - ru.Init() + reply := Reply{1, 1, "Yo!", 1 /*, Config.DefaultGroup*/, now, 0, 0, 1, "::1", true, 1, 1, ""} + ru := &ReplyUser{ClassName: "", Reply: reply, CreatedByName: "Alice", Avatar: avatar, Group: Config.DefaultGroup, Level: 0, Attachments: miniAttach} + ru.Init(user) replyList = append(replyList, ru) varList = make(map[string]tmpl.VarItem) header.Title = "Topic Name" - tpage := TopicPage{header, replyList, topic, &Forum{ID: 1, Name: "Hahaha"}, poll, Paginator{[]int{1}, 1, 1}} + tpage := TopicPage{header, replyList, topic, &Forum{ID: 1, Name: "Hahaha"}, &poll, Paginator{[]int{1}, 1, 1}} tpage.Forum.Link = BuildForumURL(NameToSlug(tpage.Forum.Name), tpage.Forum.ID) t.AddStd("topic_posts", "c.TopicPage", tpage) t.AddStd("topic_alt_posts", "c.TopicPage", tpage) diff --git a/common/templates/context.go b/common/templates/context.go index 6842fa55..5e7bc365 100644 --- a/common/templates/context.go +++ b/common/templates/context.go @@ -25,13 +25,13 @@ type OutBufferFrame struct { } type CContext struct { - RootHolder string - VarHolder string - HoldReflect reflect.Value + RootHolder string + VarHolder string + HoldReflect reflect.Value RootTemplateName string - TemplateName string - LoopDepth int - OutBuf *[]OutBufferFrame + TemplateName string + LoopDepth int + OutBuf *[]OutBufferFrame } func (con *CContext) Push(nType string, body string) (index int) { @@ -54,6 +54,20 @@ func (con *CContext) PushPhrasef(langIndex int, args string) (index int) { return con.LastBufIndex() } +func (con *CContext) StartIf(body string) (index int) { + *con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, "startif", con.TemplateName, false, nil}) + return con.LastBufIndex() +} +func (con *CContext) StartIfPtr(body string) (index int) { + *con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, "startif", con.TemplateName, true, nil}) + return con.LastBufIndex() +} + +func (con *CContext) EndIf(startIndex int, body string) (index int) { + *con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, "endif", con.TemplateName, startIndex, nil}) + return con.LastBufIndex() +} + func (con *CContext) StartLoop(body string) (index int) { con.LoopDepth++ return con.Push("startloop", body) @@ -73,15 +87,15 @@ func (con *CContext) EndTemplate(body string) (index int) { func (con *CContext) AttachVars(vars string, index int) { outBuf := *con.OutBuf - node := outBuf[index] - if node.Type != "starttemplate" && node.Type != "startloop" { - panic("not a starttemplate node") + n := outBuf[index] + if n.Type != "starttemplate" && n.Type != "startloop" && n.Type != "startif" { + panic("not a starttemplate, startloop or startif node") } - node.Body += vars - outBuf[index] = node + n.Body += vars + outBuf[index] = n } -func (con *CContext) addFrame(body string, ftype string, extra1 interface{}, extra2 interface{}) (index int) { +func (con *CContext) addFrame(body, ftype string, extra1 interface{}, extra2 interface{}) (index int) { *con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, ftype, con.TemplateName, extra1, extra2}) return con.LastBufIndex() } diff --git a/common/templates/templates.go b/common/templates/templates.go index 35b26048..220dfe5f 100644 --- a/common/templates/templates.go +++ b/common/templates/templates.go @@ -513,8 +513,7 @@ if !ok { } `*/ fout += `var iw http.ResponseWriter - gzw, ok := w.(c.GzipResponseWriter) - if ok { + if gzw, ok := w.(c.GzipResponseWriter); ok { iw = gzw.ResponseWriter } _ = iw @@ -711,14 +710,26 @@ func (c *CTemplateSet) compileSwitch(con CContext, node parse.Node) { return } - con.Push("startif", "if "+expr+" {\n") + var startIf int + var nilIf = strings.HasPrefix(expr, con.RootHolder) && strings.HasSuffix(expr, "!=nil") + if nilIf { + startIf = con.StartIfPtr("if " + expr + " {\n") + } else { + startIf = con.StartIf("if " + expr + " {\n") + } c.compileSwitch(con, node.List) if node.ElseList == nil { c.detail("Selected Branch 1") - con.Push("endif", "}\n") + con.EndIf(startIf, "}\n") + if nilIf { + c.afterTemplate(con, startIf) + } } else { c.detail("Selected Branch 2") - con.Push("endif", "}") + con.EndIf(startIf, "}") + if nilIf { + c.afterTemplate(con, startIf) + } con.Push("startelse", " else {\n") c.compileSwitch(con, node.ElseList) con.Push("endelse", "}\n") @@ -769,7 +780,7 @@ func (c *CTemplateSet) compileRangeNode(con CContext, node *parse.RangeNode) { c.detail("Range Kind Switch!") startIf := func(item reflect.Value, useCopy bool) { - con.Push("startif", "if len("+expr+") != 0 {\n") + sIndex := con.StartIf("if len(" + expr + ")!=0 {\n") startIndex := con.StartLoop("for _, item := range " + expr + " {\n") ccon := con var depth string @@ -793,7 +804,7 @@ func (c *CTemplateSet) compileRangeNode(con CContext, node *parse.RangeNode) { con.EndLoop("}\n") c.afterTemplate(con, startIndex) if node.ElseList != nil { - con.Push("endif", "}") + con.EndIf(sIndex, "}") con.Push("startelse", " else {\n") if !useCopy { ccon = con @@ -801,7 +812,7 @@ func (c *CTemplateSet) compileRangeNode(con CContext, node *parse.RangeNode) { c.compileSwitch(ccon, node.ElseList) con.Push("endelse", "}\n") } else { - con.Push("endif", "}\n") + con.EndIf(sIndex, "}\n") } } @@ -1512,12 +1523,15 @@ func (c *CTemplateSet) compileBoolSub(con CContext, varname string) string { // TODO: What if it's a pointer or an interface? I *think* we've got pointers handled somewhere, but not interfaces which we don't know the types of at compile time switch val.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64: - out += " > 0" + out += ">0" case reflect.Bool: // Do nothing case reflect.String: - out += " != \"\"" + out += "!=\"\"" case reflect.Slice, reflect.Map: - out = "len(" + out + ") != 0" + out = "len(" + out + ")!=0" + // TODO: Follow the pointer and evaluate it? + case reflect.Ptr: + out += "!=nil" default: c.logger.Println("Variable Name:", varname) c.logger.Println("Variable Holder:", con.VarHolder) @@ -1675,9 +1689,9 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V return } } - con.Push("startif", "if "+varname+" {\n") + startIf := con.StartIf("if " + varname + " {\n") c.addText(con, []byte("true")) - con.Push("endif", "} ") + con.EndIf(startIf, "} ") con.Push("startelse", "else {\n") c.addText(con, []byte("false")) con.Push("endelse", "}\n") @@ -1945,42 +1959,66 @@ func (c *CTemplateSet) loadTemplate(fileDir, name string) (content string, err e return content, nil } -func (c *CTemplateSet) afterTemplate(con CContext, startIndex int) { +func (c *CTemplateSet) afterTemplate(con CContext, startIndex int /*, svmap map[string]int*/) { c.dumpCall("afterTemplate", con, startIndex) defer c.retCall("afterTemplate") loopDepth := 0 + ifNilDepth := 0 var outBuf = *con.OutBuf varcounts := make(map[string]int) loopStart := startIndex - if outBuf[startIndex].Type == "startloop" && (len(outBuf) > startIndex+1) { + otype := outBuf[startIndex].Type + if otype == "startloop" && (len(outBuf) > startIndex+1) { + loopStart++ + } + if otype == "startif" && (len(outBuf) > startIndex+1) { loopStart++ } // Exclude varsubs within loops for now +OLoop: for i := loopStart; i < len(outBuf); i++ { item := outBuf[i] c.detail("item:", item) - if item.Type == "startloop" { + switch item.Type { + case "startloop": loopDepth++ c.detail("loopDepth:", loopDepth) - } else if item.Type == "endloop" { + case "endloop": loopDepth-- c.detail("loopDepth:", loopDepth) if loopDepth == -1 { - break + break OLoop + } + case "startif": + if item.Extra.(bool) == true { + ifNilDepth++ + } + case "endif": + item2 := outBuf[item.Extra.(int)] + if item2.Extra.(bool) == true { + ifNilDepth-- + } + if ifNilDepth == -1 { + break OLoop + } + case "varsub": + if loopDepth == 0 && ifNilDepth == 0 { + count := varcounts[item.Body] + varcounts[item.Body] = count + 1 + c.detail("count " + strconv.Itoa(count) + " for " + item.Body) + c.detail("loopDepth:", loopDepth) } - } else if item.Type == "varsub" && loopDepth == 0 { - count := varcounts[item.Body] - varcounts[item.Body] = count + 1 - c.detail("count " + strconv.Itoa(count) + " for " + item.Body) - c.detail("loopDepth:", loopDepth) } } var varstr string var i int varmap := make(map[string]int) + /*for svkey, sventry := range svmap { + varmap[svkey] = sventry + }*/ for name, count := range varcounts { if count > 1 { varstr += "var c_v_" + strconv.Itoa(i) + "=" + name + "\n" @@ -1991,21 +2029,153 @@ func (c *CTemplateSet) afterTemplate(con CContext, startIndex int) { // Exclude varsubs within loops for now loopDepth = 0 + ifNilDepth = 0 +OOLoop: for i := loopStart; i < len(outBuf); i++ { item := outBuf[i] - if item.Type == "startloop" { + switch item.Type { + case "startloop": loopDepth++ - } else if item.Type == "endloop" { + case "endloop": loopDepth-- if loopDepth == -1 { - break + break OOLoop + } //con.Push("startif", "if "+varname+" {\n") + case "startif": + if item.Extra.(bool) == true { + ifNilDepth++ } - } else if item.Type == "varsub" && loopDepth == 0 { - index, ok := varmap[item.Body] - if ok { - item.Body = "c_v_" + strconv.Itoa(index) - item.Type = "cvarsub" - outBuf[i] = item + case "endif": + item2 := outBuf[item.Extra.(int)] + if item2.Extra.(bool) == true { + ifNilDepth-- + } + if ifNilDepth == -1 { + break OOLoop + } + case "varsub": + if loopDepth == 0 && ifNilDepth == 0 { + index, ok := varmap[item.Body] + if ok { + item.Body = "c_v_" + strconv.Itoa(index) + item.Type = "cvarsub" + outBuf[i] = item + } + } + } + } + + con.AttachVars(varstr, startIndex) +} + +const ( + ATTmpl = iota + ATLoop + ATIfPtr +) + +func (c *CTemplateSet) afterTemplateV2(con CContext, startIndex int /*, typ int*/, svmap map[string]int) { + c.dumpCall("afterTemplateV2", con, startIndex) + defer c.retCall("afterTemplateV2") + + loopDepth := 0 + ifNilDepth := 0 + var outBuf = *con.OutBuf + varcounts := make(map[string]int) + loopStart := startIndex + otype := outBuf[startIndex].Type + if otype == "startloop" && (len(outBuf) > startIndex+1) { + loopStart++ + } + if otype == "startif" && (len(outBuf) > startIndex+1) { + loopStart++ + } + + // Exclude varsubs within loops for now +OLoop: + for i := loopStart; i < len(outBuf); i++ { + item := outBuf[i] + c.detail("item:", item) + switch item.Type { + case "startloop": + loopDepth++ + c.detail("loopDepth:", loopDepth) + case "endloop": + loopDepth-- + c.detail("loopDepth:", loopDepth) + if loopDepth == -1 { + break OLoop + } + case "startif": + if item.Extra.(bool) == true { + ifNilDepth++ + } + case "endif": + item2 := outBuf[item.Extra.(int)] + if item2.Extra.(bool) == true { + ifNilDepth-- + } + if ifNilDepth == -1 { + break OLoop + } + case "varsub": + if loopDepth == 0 && ifNilDepth == 0 { + count := varcounts[item.Body] + varcounts[item.Body] = count + 1 + c.detail("count " + strconv.Itoa(count) + " for " + item.Body) + c.detail("loopDepth:", loopDepth) + } + } + } + + var varstr string + var i int + varmap := make(map[string]int) + /*for svkey, sventry := range svmap { + varmap[svkey] = sventry + }*/ + for name, count := range varcounts { + if count > 1 { + varstr += "var c_v_" + strconv.Itoa(i) + "=" + name + "\n" + varmap[name] = i + i++ + } + } + + // Exclude varsubs within loops for now + loopDepth = 0 + ifNilDepth = 0 +OOLoop: + for i := loopStart; i < len(outBuf); i++ { + item := outBuf[i] + switch item.Type { + case "startloop": + loopDepth++ + case "endloop": + loopDepth-- + if loopDepth == -1 { + break OOLoop + } //con.Push("startif", "if "+varname+" {\n") + case "startif": + if item.Extra.(bool) == true { + ifNilDepth++ + } + case "endif": + item2 := outBuf[item.Extra.(int)] + if item2.Extra.(bool) == true { + ifNilDepth-- + } + if ifNilDepth == -1 { + break OOLoop + } + case "varsub": + if loopDepth == 0 && ifNilDepth == 0 { + index, ok := varmap[item.Body] + if ok { + item.Body = "c_v_" + strconv.Itoa(index) + item.Type = "cvarsub" + outBuf[i] = item + } } } } diff --git a/common/topic.go b/common/topic.go index 1b87617f..721aca87 100644 --- a/common/topic.go +++ b/common/topic.go @@ -188,6 +188,8 @@ func (t *TopicsRow) Topic() *Topic { type TopicStmts struct { getRids *sql.Stmt getReplies *sql.Stmt + getReplies2 *sql.Stmt + getReplies3 *sql.Stmt addReplies *sql.Stmt updateLastReply *sql.Stmt lock *sql.Stmt @@ -217,7 +219,9 @@ func init() { t := "topics" topicStmts = TopicStmts{ getRids: acc.Select("replies").Columns("rid").Where("tid=?").Orderby("rid ASC").Limit("?,?").Prepare(), - getReplies: acc.SimpleLeftJoin("replies AS r", "users AS u", "r.rid, r.content, r.createdBy, r.createdAt, r.lastEdit, r.lastEditBy, u.avatar, u.name, u.group, u.level, r.ip, r.likeCount, r.attachCount, r.actionType", "r.createdBy = u.uid", "r.tid = ?", "r.rid ASC", "?,?"), + getReplies: acc.SimpleLeftJoin("replies AS r", "users AS u", "r.rid, r.content, r.createdBy, r.createdAt, r.lastEdit, r.lastEditBy, u.avatar, u.name, u.group, u.level, r.ip, r.likeCount, r.attachCount, r.actionType", "r.createdBy=u.uid", "r.tid=?", "r.rid ASC", "?,?"), + getReplies2: acc.SimpleLeftJoin("replies AS r", "users AS u", "r.rid, r.content, r.createdBy, r.createdAt, r.lastEdit, r.lastEditBy, u.avatar, u.name, u.group, u.level, r.likeCount, r.attachCount, r.actionType", "r.createdBy=u.uid", "r.tid=?", "r.rid ASC", "?,?"), + getReplies3: acc.Select("replies").Columns("rid, content, createdBy, createdAt, lastEdit, lastEditBy, likeCount, attachCount, actionType").Where("tid=?").Orderby("rid ASC").Limit("?,?").Prepare(), addReplies: acc.Update(t).Set("postCount=postCount+?, lastReplyBy=?, lastReplyAt=UTC_TIMESTAMP()").Where("tid=?").Prepare(), updateLastReply: acc.Update(t).Set("lastReplyID=?").Where("lastReplyID > ? AND tid=?").Prepare(), lock: acc.Update(t).Set("is_closed=1").Where("tid=?").Prepare(), @@ -570,8 +574,7 @@ var unlockai = "🔓" var stickai = "📌" var unstickai = "📌" + aipost -func (ru *ReplyUser) Init() (group *Group, err error) { - ru.UserLink = BuildProfileURL(NameToSlug(ru.CreatedByName), ru.CreatedBy) +func (ru *ReplyUser) Init(u *User) (group *Group, err error) { ru.ContentLines = strings.Count(ru.Content, "\n") postGroup, err := Groups.Get(ru.Group) @@ -583,8 +586,117 @@ func (ru *ReplyUser) Init() (group *Group, err error) { } ru.Tag = postGroup.Tag - // TODO: Make a function for this? Build a more sophisticated noavatar handling system? Do bulk user loads and let the c.UserStore initialise this? - ru.Avatar, ru.MicroAvatar = BuildAvatar(ru.CreatedBy, ru.Avatar) + if u.ID != ru.CreatedBy { + ru.UserLink = BuildProfileURL(NameToSlug(ru.CreatedByName), ru.CreatedBy) + // TODO: Make a function for this? Build a more sophisticated noavatar handling system? Do bulk user loads and let the c.UserStore initialise this? + ru.Avatar, ru.MicroAvatar = BuildAvatar(ru.CreatedBy, ru.Avatar) + } else { + ru.UserLink = u.Link + ru.Avatar, ru.MicroAvatar = u.Avatar, u.MicroAvatar + } + + // We really shouldn't have inline HTML, we should do something about this... + if ru.ActionType != "" { + aarr := strings.Split(ru.ActionType, "-") + action := aarr[0] + switch action { + case "lock": + ru.ActionIcon = lockai + case "unlock": + ru.ActionIcon = unlockai + case "stick": + ru.ActionIcon = stickai + case "unstick": + ru.ActionIcon = unstickai + case "move": + if len(aarr) == 2 { + fid, _ := strconv.Atoi(aarr[1]) + forum, err := Forums.Get(fid) + if err == nil { + ru.ActionType = p.GetTmplPhrasef("topic.action_topic_move_dest", forum.Link, forum.Name, ru.UserLink, ru.CreatedByName) + return postGroup, nil + } + } + default: + // TODO: Only fire this off if a corresponding phrase for the ActionType doesn't exist? Or maybe have some sort of action registry? + ru.ActionType = p.GetTmplPhrasef("topic.action_topic_default", ru.ActionType) + return postGroup, nil + } + ru.ActionType = p.GetTmplPhrasef("topic.action_topic_"+action, ru.UserLink, ru.CreatedByName) + } + + return postGroup, nil +} + +func (ru *ReplyUser) Init2() (group *Group, err error) { + //ru.UserLink = BuildProfileURL(NameToSlug(ru.CreatedByName), ru.CreatedBy) + ru.ContentLines = strings.Count(ru.Content, "\n") + + postGroup, err := Groups.Get(ru.Group) + if err != nil { + return nil, err + } + if postGroup.IsMod { + ru.ClassName = Config.StaffCSS + } + ru.Tag = postGroup.Tag + + // We really shouldn't have inline HTML, we should do something about this... + if ru.ActionType != "" { + aarr := strings.Split(ru.ActionType, "-") + action := aarr[0] + switch action { + case "lock": + ru.ActionIcon = lockai + case "unlock": + ru.ActionIcon = unlockai + case "stick": + ru.ActionIcon = stickai + case "unstick": + ru.ActionIcon = unstickai + case "move": + if len(aarr) == 2 { + fid, _ := strconv.Atoi(aarr[1]) + forum, err := Forums.Get(fid) + if err == nil { + ru.ActionType = p.GetTmplPhrasef("topic.action_topic_move_dest", forum.Link, forum.Name, ru.UserLink, ru.CreatedByName) + return postGroup, nil + } + } + default: + // TODO: Only fire this off if a corresponding phrase for the ActionType doesn't exist? Or maybe have some sort of action registry? + ru.ActionType = p.GetTmplPhrasef("topic.action_topic_default", ru.ActionType) + return postGroup, nil + } + ru.ActionType = p.GetTmplPhrasef("topic.action_topic_"+action, ru.UserLink, ru.CreatedByName) + } + + return postGroup, nil +} + +func (ru *ReplyUser) Init3(u *User, tu *TopicUser) (group *Group, err error) { + ru.ContentLines = strings.Count(ru.Content, "\n") + + postGroup, err := Groups.Get(ru.Group) + if err != nil { + return nil, err + } + if postGroup.IsMod { + ru.ClassName = Config.StaffCSS + } + ru.Tag = postGroup.Tag + + if u.ID == ru.CreatedBy { + ru.UserLink = u.Link + ru.Avatar, ru.MicroAvatar = u.Avatar, u.MicroAvatar + } else if tu.CreatedBy == ru.CreatedBy { + ru.UserLink = tu.UserLink + ru.Avatar, ru.MicroAvatar = tu.Avatar, tu.MicroAvatar + } else { + ru.UserLink = BuildProfileURL(NameToSlug(ru.CreatedByName), ru.CreatedBy) + // TODO: Make a function for this? Build a more sophisticated noavatar handling system? Do bulk user loads and let the c.UserStore initialise this? + ru.Avatar, ru.MicroAvatar = BuildAvatar(ru.CreatedBy, ru.Avatar) + } // We really shouldn't have inline HTML, we should do something about this... if ru.ActionType != "" { @@ -620,18 +732,9 @@ func (ru *ReplyUser) Init() (group *Group, err error) { } // TODO: Factor TopicUser into a *Topic and *User, as this starting to become overly complicated x.x -func (t *TopicUser) Replies(offset, pFrag int, user *User) (rlist []*ReplyUser, ogdesc string, err error) { - var likedMap map[int]int - if user.Liked > 0 { - likedMap = make(map[int]int) - } - likedQueryList := []int{user.ID} - - var attachMap map[int]int - if user.Perms.EditReply { - attachMap = make(map[int]int) - } - attachQueryList := []int{} +func (t *TopicUser) Replies(offset, pFrag int, user *User) (rlist []*ReplyUser /*, ogdesc string*/, err error) { + var likedMap, attachMap map[int]int + var likedQueryList, attachQueryList []int var rid int if len(t.Rids) > 0 { @@ -641,15 +744,19 @@ func (t *TopicUser) Replies(offset, pFrag int, user *User) (rlist []*ReplyUser, re, err := Rstore.GetCache().Get(rid) ucache := Users.GetCache() var ruser *User - if err == nil && ucache != nil { + if ucache != nil { //log.Print("ucache step") - ruser, err = ucache.Get(re.CreatedBy) + if err == nil { + ruser = ucache.Getn(re.CreatedBy) + } else if t.PostCount == 2 { + ruser = ucache.Getn(t.LastReplyBy) + } } hTbl := GetHookTable() - rf := func(r *ReplyUser) error { + rf := func(r *ReplyUser) (err error) { //log.Printf("before r: %+v\n", r) - group, err := r.Init() + group, err := r.Init3(user, t) if err != nil { return err } @@ -668,6 +775,7 @@ func (t *TopicUser) Replies(offset, pFrag int, user *User) (rlist []*ReplyUser, if r.ContentHtml == r.Content { r.ContentHtml = r.Content } + r.Deletable = user.Perms.DeleteReply || r.CreatedBy == user.ID // TODO: This doesn't work properly so pick the first one instead? /*if r.ID == pFrag { @@ -677,61 +785,211 @@ func (t *TopicUser) Replies(offset, pFrag int, user *User) (rlist []*ReplyUser, } }*/ - if r.LikeCount > 0 && user.Liked > 0 { - likedMap[r.ID] = len(rlist) - likedQueryList = append(likedQueryList, r.ID) + return nil + } + + rf3 := func(r *ReplyUser) error { + //log.Printf("before r: %+v\n", r) + group, err := r.Init2() + if err != nil { + return err } - if user.Perms.EditReply && r.AttachCount > 0 { - attachMap[r.ID] = len(rlist) - attachQueryList = append(attachQueryList, r.ID) + + var parseSettings *ParseSettings + if !group.Perms.AutoEmbed && (user.ParseSettings == nil || !user.ParseSettings.NoEmbed) { + parseSettings = DefaultParseSettings.CopyPtr() + parseSettings.NoEmbed = true + } else { + parseSettings = user.ParseSettings + } + + r.ContentHtml = ParseMessage(r.Content, t.ParentID, "forums", parseSettings, user) + // TODO: Do this more efficiently by avoiding the allocations entirely in ParseMessage, if there's nothing to do. + if r.ContentHtml == r.Content { + r.ContentHtml = r.Content } r.Deletable = user.Perms.DeleteReply || r.CreatedBy == user.ID - hTbl.VhookNoRet("topic_reply_row_assign", &rlist, &r) - // TODO: Use a pointer instead to make it easier to abstract this loop? What impact would this have on escape analysis? - rlist = append(rlist, r) - //log.Printf("r: %d-%d", r.ID, len(rlist)-1) return nil } // TODO: Factor the user fields out and embed a user struct instead - if err == nil { + if err == nil && ruser != nil { //log.Print("reply cached serve") - reply := &ReplyUser{ClassName: "", Reply: *re, CreatedByName: ruser.Name, Avatar: ruser.Avatar /*URLPrefix: ruser.URLPrefix, URLName: ruser.URLName, */, Level: ruser.Level, Tag: ruser.Tag} - reply.Group = ruser.Group - err = rf(reply) - if err != nil { - return nil, "", err + r := &ReplyUser{ /*ClassName: "", */ Reply: *re, CreatedByName: ruser.Name, UserLink: ruser.Link, Avatar: ruser.Avatar, MicroAvatar: ruser.MicroAvatar /*URLPrefix: ruser.URLPrefix, URLName: ruser.URLName, */, Group: ruser.Group, Level: ruser.Level, Tag: ruser.Tag} + if err = rf3(r); err != nil { + return nil, err } + + if r.LikeCount > 0 && user.Liked > 0 { + likedMap = map[int]int{r.ID: len(rlist)} + likedQueryList = []int{r.ID} + } + if user.Perms.EditReply && r.AttachCount > 0 { + attachMap = map[int]int{r.ID: len(rlist)} + attachQueryList = []int{r.ID} + } + + hTbl.VhookNoRet("topic_reply_row_assign", &r) + // TODO: Use a pointer instead to make it easier to abstract this loop? What impact would this have on escape analysis? + rlist = []*ReplyUser{r} + //log.Printf("r: %d-%d", r.ID, len(rlist)-1) } else { //log.Print("reply query serve") - rows, err := topicStmts.getReplies.Query(t.ID, offset, Config.ItemsPerPage) - if err != nil { - return nil, "", err - } - defer rows.Close() + rf2 := func(r *ReplyUser) { + if r.LikeCount > 0 && user.Liked > 0 { + if likedMap == nil { + likedMap = map[int]int{r.ID: len(rlist)} + likedQueryList = []int{r.ID} + } else { + likedMap[r.ID] = len(rlist) + likedQueryList = append(likedQueryList, r.ID) + } + } + if user.Perms.EditReply && r.AttachCount > 0 { + if attachMap == nil { + attachMap = map[int]int{r.ID: len(rlist)} + attachQueryList = []int{r.ID} + } else { + attachMap[r.ID] = len(rlist) + attachQueryList = append(attachQueryList, r.ID) + } + } - for rows.Next() { - r := &ReplyUser{} - err := rows.Scan(&r.ID, &r.Content, &r.CreatedBy, &r.CreatedAt, &r.LastEdit, &r.LastEditBy, &r.Avatar, &r.CreatedByName, &r.Group /*&r.URLPrefix, &r.URLName,*/, &r.Level, &r.IP, &r.LikeCount, &r.AttachCount, &r.ActionType) - if err != nil { - return nil, "", err - } - err = rf(r) - if err != nil { - return nil, "", err - } + hTbl.VhookNoRet("topic_reply_row_assign", &r) + // TODO: Use a pointer instead to make it easier to abstract this loop? What impact would this have on escape analysis? + rlist = append(rlist, r) + //log.Printf("r: %d-%d", r.ID, len(rlist)-1) } - if err = rows.Err(); err != nil { - return nil, "", err + if !user.Perms.ViewIPs && ruser != nil { + rows, err := topicStmts.getReplies3.Query(t.ID, offset, Config.ItemsPerPage) + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + r := &ReplyUser{Avatar: ruser.Avatar, MicroAvatar: ruser.MicroAvatar, UserLink: ruser.Link, CreatedByName: ruser.Name, Group: ruser.Group, Level: ruser.Level} + err := rows.Scan(&r.ID, &r.Content, &r.CreatedBy, &r.CreatedAt, &r.LastEdit, &r.LastEditBy, &r.LikeCount, &r.AttachCount, &r.ActionType) + if err != nil { + return nil, err + } + if err = rf3(r); err != nil { + return nil, err + } + rf2(r) + } + if err = rows.Err(); err != nil { + return nil, err + } + } else if user.Perms.ViewIPs { + rows, err := topicStmts.getReplies.Query(t.ID, offset, Config.ItemsPerPage) + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + r := &ReplyUser{} + err := rows.Scan(&r.ID, &r.Content, &r.CreatedBy, &r.CreatedAt, &r.LastEdit, &r.LastEditBy, &r.Avatar, &r.CreatedByName, &r.Group /*&r.URLPrefix, &r.URLName,*/, &r.Level, &r.IP, &r.LikeCount, &r.AttachCount, &r.ActionType) + if err != nil { + return nil, err + } + if err = rf(r); err != nil { + return nil, err + } + rf2(r) + } + if err = rows.Err(); err != nil { + return nil, err + } + } else if t.PostCount >= 20 { + rows, err := topicStmts.getReplies3.Query(t.ID, offset, Config.ItemsPerPage) + if err != nil { + return nil, err + } + defer rows.Close() + reqUserList := make(map[int]bool) + for rows.Next() { + r := &ReplyUser{} + err := rows.Scan(&r.ID, &r.Content, &r.CreatedBy, &r.CreatedAt, &r.LastEdit, &r.LastEditBy /*&r.URLPrefix, &r.URLName,*/, &r.LikeCount, &r.AttachCount, &r.ActionType) + if err != nil { + return nil, err + } + if r.CreatedBy != t.CreatedBy && r.CreatedBy != user.ID { + reqUserList[r.CreatedBy] = true + } + } + if err = rows.Err(); err != nil { + return nil, err + } + + var userList map[int]*User + if len(reqUserList) > 0 { + // Convert the user ID map to a slice, then bulk load the users + idSlice := make([]int, len(reqUserList)) + var i int + for userID := range reqUserList { + idSlice[i] = userID + i++ + } + userList, err = Users.BulkGetMap(idSlice) + if err != nil { + return nil, nil // TODO: Implement this! + } + } + + for _, r := range rlist { + var u *User + if r.CreatedBy == t.CreatedBy { + r.CreatedByName = t.CreatedByName + r.Avatar = t.Avatar + r.MicroAvatar = t.MicroAvatar + r.Group = t.Group + r.Level = t.Level + } else { + if r.CreatedBy == user.ID { + u = user + } else { + u = userList[r.CreatedBy] + } + r.CreatedByName = u.Name + r.Avatar = u.Avatar + r.MicroAvatar = u.MicroAvatar + r.Group = u.Group + r.Level = u.Level + } + if err = rf(r); err != nil { + return nil, err + } + rf2(r) + } + } else { + rows, err := topicStmts.getReplies2.Query(t.ID, offset, Config.ItemsPerPage) + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + r := &ReplyUser{} + err := rows.Scan(&r.ID, &r.Content, &r.CreatedBy, &r.CreatedAt, &r.LastEdit, &r.LastEditBy, &r.Avatar, &r.CreatedByName, &r.Group /*&r.URLPrefix, &r.URLName,*/, &r.Level, &r.LikeCount, &r.AttachCount, &r.ActionType) + if err != nil { + return nil, err + } + if err = rf(r); err != nil { + return nil, err + } + rf2(r) + } + if err = rows.Err(); err != nil { + return nil, err + } } } // TODO: Add a config setting to disable the liked query for a burst of extra speed - if user.Liked > 0 && len(likedQueryList) > 1 /*&& user.LastLiked <= time.Now()*/ { - eids, err := Likes.BulkExists(likedQueryList[1:], user.ID, "replies") + if user.Liked > 0 && len(likedQueryList) > 0 /*&& user.LastLiked <= time.Now()*/ { + eids, err := Likes.BulkExists(likedQueryList, user.ID, "replies") if err != nil { - return nil, "", err + return nil, err } for _, eid := range eids { rlist[likedMap[eid]].Liked = true @@ -742,7 +1000,7 @@ func (t *TopicUser) Replies(offset, pFrag int, user *User) (rlist []*ReplyUser, //log.Printf("attachQueryList: %+v\n", attachQueryList) amap, err := Attachments.BulkMiniGetList("replies", attachQueryList) if err != nil && err != sql.ErrNoRows { - return nil, "", err + return nil, err } //log.Printf("amap: %+v\n", amap) //log.Printf("attachMap: %+v\n", attachMap) @@ -755,7 +1013,9 @@ func (t *TopicUser) Replies(offset, pFrag int, user *User) (rlist []*ReplyUser, } } - return rlist, ogdesc, nil + //hTbl.VhookNoRet("topic_reply_end", &rlist) + + return rlist, nil } // TODO: Test this @@ -875,6 +1135,20 @@ func BuildTopicURL(slug string, tid int) string { return "/topic/" + slug + "." + strconv.Itoa(tid) } +func BuildTopicURLSb(sb *strings.Builder, slug string, tid int) { + if slug == "" || !Config.BuildSlugs { + sb.Grow(7 + 2) + sb.WriteString("/topic/") + sb.WriteString(strconv.Itoa(tid)) + return + } + sb.Grow(7 + 3 + len(slug)) + sb.WriteString("/topic/") + sb.WriteString(slug) + sb.WriteRune('.') + sb.WriteString(strconv.Itoa(tid)) +} + // I don't care if it isn't used,, it will likely be in the future. Nolint. // nolint func getTopicURLPrefix() string { diff --git a/common/user_cache.go b/common/user_cache.go index e0d9674d..030f9190 100644 --- a/common/user_cache.go +++ b/common/user_cache.go @@ -9,6 +9,7 @@ import ( type UserCache interface { DeallocOverflow(evictPriority bool) (evicted int) // May cause thread contention, looks for items to evict Get(id int) (*User, error) + Getn(id int) *User GetUnsafe(id int) (*User, error) BulkGet(ids []int) (list []*User) Set(item *User) error @@ -95,12 +96,19 @@ func (s *MemoryUserCache) DeallocOverflow(evictPriority bool) (evicted int) { // Get fetches a user by ID. Returns ErrNoRows if not present. func (s *MemoryUserCache) Get(id int) (*User, error) { s.RLock() - item, ok := s.items[id] + item := s.items[id] s.RUnlock() - if ok { - return item, nil + if item == nil { + return item, ErrNoRows } - return item, ErrNoRows + return item, nil +} + +func (s *MemoryUserCache) Getn(id int) *User { + s.RLock() + item := s.items[id] + s.RUnlock() + return item } // BulkGet fetches multiple users by their IDs. Indices without users will be set to nil, so make sure you check for those, we might want to change this behaviour to make it less confusing. diff --git a/common/user_store.go b/common/user_store.go index 2724d022..fa6ea18e 100644 --- a/common/user_store.go +++ b/common/user_store.go @@ -18,6 +18,7 @@ var ErrLongUsername = errors.New("this username is too long") type UserStore interface { DirtyGet(id int) *User Get(id int) (*User, error) + Getn(id int) *User GetByName(name string) (*User, error) Exists(id int) bool GetOffset(offset, perPage int) ([]*User, error) @@ -93,6 +94,15 @@ func (s *DefaultUserStore) Get(id int) (*User, error) { u = &User{ID: id, Loggedin: true} var embeds int err = s.get.QueryRow(id).Scan(&u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &u.CreatedAt, &embeds) + /*if err != nil { + return nil, err + } + if embeds != -1 { + u.ParseSettings = DefaultParseSettings.CopyPtr() + u.ParseSettings.NoEmbed = embeds == 0 + } + u.Init() + s.cache.Set(u)*/ if err == nil { if embeds != -1 { u.ParseSettings = DefaultParseSettings.CopyPtr() @@ -104,21 +114,43 @@ func (s *DefaultUserStore) Get(id int) (*User, error) { return u, err } +func (s *DefaultUserStore) Getn(id int) *User { + u := s.cache.Getn(id) + if u != nil { + return u + } + + u = &User{ID: id, Loggedin: true} + var embeds int + err := s.get.QueryRow(id).Scan(&u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &u.CreatedAt, &embeds) + if err != nil { + return nil + } + if embeds != -1 { + u.ParseSettings = DefaultParseSettings.CopyPtr() + u.ParseSettings.NoEmbed = embeds == 0 + } + u.Init() + s.cache.Set(u) + return u +} + // TODO: Log weird cache errors? Not just here but in every *Cache? // ! This bypasses the cache, use frugally func (s *DefaultUserStore) GetByName(name string) (*User, error) { u := &User{Loggedin: true} var embeds int err := s.getByName.QueryRow(name).Scan(&u.ID, &u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &u.CreatedAt, &embeds) - if err == nil { - if embeds != -1 { - u.ParseSettings = DefaultParseSettings.CopyPtr() - u.ParseSettings.NoEmbed = embeds == 0 - } - u.Init() - s.cache.Set(u) + if err != nil { + return nil, err } - return u, err + if embeds != -1 { + u.ParseSettings = DefaultParseSettings.CopyPtr() + u.ParseSettings.NoEmbed = embeds == 0 + } + u.Init() + s.cache.Set(u) + return u, nil } // TODO: Optimise this, so we don't wind up hitting the database every-time for small gaps diff --git a/routes/profile.go b/routes/profile.go index 69a85ed8..cfaf93e1 100644 --- a/routes/profile.go +++ b/routes/profile.go @@ -27,7 +27,7 @@ func init() { } // TODO: Remove the View part of the name? -func ViewProfile(w http.ResponseWriter, r *http.Request, user *c.User, header *c.Header) c.RouteError { +func ViewProfile(w http.ResponseWriter, r *http.Request, user *c.User, h *c.Header) c.RouteError { var reCreatedAt time.Time var reContent, reCreatedByName, reAvatar string var rid, reCreatedBy, reLastEdit, reLastEditBy, reGroup int @@ -36,13 +36,13 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user *c.User, header *c // TODO: Do a 301 if it's the wrong username? Do a canonical too? _, pid, err := ParseSEOURL(r.URL.Path[len("/user/"):]) if err != nil { - return c.SimpleError(phrases.GetErrorPhrase("url_id_must_be_integer"), w, r, header) + return c.SimpleError(phrases.GetErrorPhrase("url_id_must_be_integer"), w, r, h) } // TODO: Preload this? - header.AddSheet(header.Theme.Name + "/profile.css") + h.AddSheet(h.Theme.Name + "/profile.css") if user.Loggedin { - header.AddScriptAsync("profile_member.js") + h.AddScriptAsync("profile_member.js") } var puser *c.User @@ -54,13 +54,13 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user *c.User, header *c // TODO: Add a shared function for checking for ErrNoRows and internal erroring if it's not that case? puser, err = c.Users.Get(pid) if err == sql.ErrNoRows { - return c.NotFound(w, r, header) + return c.NotFound(w, r, h) } else if err != nil { return c.InternalError(err, w, r) } } - header.Title = phrases.GetTitlePhrasef("profile", puser.Name) - header.Path = c.BuildProfileURL(c.NameToSlug(puser.Name), puser.ID) + h.Title = phrases.GetTitlePhrasef("profile", puser.Name) + h.Path = c.BuildProfileURL(c.NameToSlug(puser.Name), puser.ID) // Get the replies.. rows, err := profileStmts.getReplies.Query(puser.ID) @@ -77,8 +77,8 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user *c.User, header *c reLiked := false reLikeCount := 0 - ru := &c.ReplyUser{Reply: c.Reply{rid, puser.ID, reContent, reCreatedBy, reGroup, reCreatedAt, reLastEdit, reLastEditBy, 0, "", reLiked, reLikeCount, 0, ""}, ContentHtml: c.ParseMessage(reContent, 0, "", user.ParseSettings, user), CreatedByName: reCreatedByName, Avatar: reAvatar, Level: 0} - _, err = ru.Init() + ru := &c.ReplyUser{Reply: c.Reply{rid, puser.ID, reContent, reCreatedBy /*, reGroup*/, reCreatedAt, reLastEdit, reLastEditBy, 0, "", reLiked, reLikeCount, 0, ""}, ContentHtml: c.ParseMessage(reContent, 0, "", user.ParseSettings, user), CreatedByName: reCreatedByName, Avatar: reAvatar, Group: reGroup, Level: 0} + _, err = ru.Init(user) if err != nil { return c.InternalError(err, w, r) } @@ -111,6 +111,6 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user *c.User, header *c canMessage := (!blockedInv && user.Perms.UseConvos) || (!blockedInv && puser.IsSuperMod && user.Perms.UseConvosOnlyWithMod) || user.IsSuperMod canComment := !blockedInv && user.Perms.CreateProfileReply - ppage := c.ProfilePage{header, reList, *puser, currentScore, nextScore, blocked, canMessage, canComment} - return renderTemplate("profile", w, r, header, ppage) + ppage := c.ProfilePage{h, reList, *puser, currentScore, nextScore, blocked, canMessage, canComment} + return renderTemplate("profile", w, r, h, ppage) } diff --git a/routes/reply.go b/routes/reply.go index 8478f737..53cf36fb 100644 --- a/routes/reply.go +++ b/routes/reply.go @@ -209,7 +209,7 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user *c.User) c.R // TODO: Disable stat updates in posts handled by plugin_guilds // TODO: Update the stats after edits so that we don't under or over decrement stats during deletes -func ReplyEditSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid string) c.RouteError { +func ReplyEditSubmit(w http.ResponseWriter, r *http.Request, u *c.User, srid string) c.RouteError { js := r.PostFormValue("js") == "1" rid, err := strconv.Atoi(srid) if err != nil { @@ -231,15 +231,15 @@ func ReplyEditSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid } // TODO: Add hooks to make use of headerLite - lite, ferr := c.SimpleForumUserCheck(w, r, user, topic.ParentID) + lite, ferr := c.SimpleForumUserCheck(w, r, u, topic.ParentID) if ferr != nil { return ferr } - if !user.Perms.ViewTopic || !user.Perms.EditReply { - return c.NoPermissionsJSQ(w, r, user, js) + if !u.Perms.ViewTopic || !u.Perms.EditReply { + return c.NoPermissionsJSQ(w, r, u, js) } - if topic.IsClosed && !user.Perms.CloseTopic { - return c.NoPermissionsJSQ(w, r, user, js) + if topic.IsClosed && !u.Perms.CloseTopic { + return c.NoPermissionsJSQ(w, r, u, js) } err = reply.SetPost(r.PostFormValue("edit_item")) @@ -257,7 +257,7 @@ func ReplyEditSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid return c.InternalErrorJSQ(err, w, r, js) } - skip, rerr := lite.Hooks.VhookSkippable("action_end_edit_reply", reply.ID, user) + skip, rerr := lite.Hooks.VhookSkippable("action_end_edit_reply", reply.ID, u) if skip || rerr != nil { return rerr } @@ -265,7 +265,7 @@ func ReplyEditSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid if !js { http.Redirect(w, r, "/topic/"+strconv.Itoa(topic.ID)+"#reply-"+strconv.Itoa(rid), http.StatusSeeOther) } else { - outBytes, err := json.Marshal(JsonReply{c.ParseMessage(reply.Content, topic.ParentID, "forums", user.ParseSettings, user)}) + outBytes, err := json.Marshal(JsonReply{c.ParseMessage(reply.Content, topic.ParentID, "forums", u.ParseSettings, u)}) if err != nil { return c.InternalErrorJSQ(err, w, r, js) } @@ -277,7 +277,7 @@ func ReplyEditSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid // TODO: Refactor this // TODO: Disable stat updates in posts handled by plugin_guilds -func ReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid string) c.RouteError { +func ReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, u *c.User, srid string) c.RouteError { js := r.PostFormValue("js") == "1" rid, err := strconv.Atoi(srid) if err != nil { @@ -298,20 +298,20 @@ func ReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user *c.User, sri } // TODO: Add hooks to make use of headerLite - lite, ferr := c.SimpleForumUserCheck(w, r, user, topic.ParentID) + lite, ferr := c.SimpleForumUserCheck(w, r, u, topic.ParentID) if ferr != nil { return ferr } - if reply.CreatedBy != user.ID { - if !user.Perms.ViewTopic || !user.Perms.DeleteReply { - return c.NoPermissionsJSQ(w, r, user, js) + if reply.CreatedBy != u.ID { + if !u.Perms.ViewTopic || !u.Perms.DeleteReply { + return c.NoPermissionsJSQ(w, r, u, js) } } if err := reply.Delete(); err != nil { return c.InternalErrorJSQ(err, w, r, js) } - skip, rerr := lite.Hooks.VhookSkippable("action_end_delete_reply", reply.ID, user) + skip, rerr := lite.Hooks.VhookSkippable("action_end_delete_reply", reply.ID, u) if skip || rerr != nil { return rerr } @@ -334,7 +334,7 @@ func ReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user *c.User, sri return c.InternalErrorJSQ(err, w, r, js) }*/ - err = c.ModLogs.Create("delete", reply.ParentID, "reply", user.GetIP(), user.ID) + err = c.ModLogs.Create("delete", reply.ParentID, "reply", u.GetIP(), u.ID) if err != nil { return c.InternalErrorJSQ(err, w, r, js) } @@ -455,7 +455,7 @@ func RemoveAttachFromReplySubmit(w http.ResponseWriter, r *http.Request, u *c.Us return nil } -func ReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid string) c.RouteError { +func ReplyLikeSubmit(w http.ResponseWriter, r *http.Request, u *c.User, srid string) c.RouteError { js := r.PostFormValue("js") == "1" rid, err := strconv.Atoi(srid) if err != nil { @@ -477,39 +477,39 @@ func ReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid } // TODO: Add hooks to make use of headerLite - lite, ferr := c.SimpleForumUserCheck(w, r, user, topic.ParentID) + lite, ferr := c.SimpleForumUserCheck(w, r, u, topic.ParentID) if ferr != nil { return ferr } - if !user.Perms.ViewTopic || !user.Perms.LikeItem { - return c.NoPermissionsJSQ(w, r, user, js) + if !u.Perms.ViewTopic || !u.Perms.LikeItem { + return c.NoPermissionsJSQ(w, r, u, js) } - if reply.CreatedBy == user.ID { - return c.LocalErrorJSQ("You can't like your own replies", w, r, user, js) + if reply.CreatedBy == u.ID { + return c.LocalErrorJSQ("You can't like your own replies", w, r, u, js) } _, err = c.Users.Get(reply.CreatedBy) if err != nil && err != sql.ErrNoRows { - return c.LocalErrorJSQ("The target user doesn't exist", w, r, user, js) + return c.LocalErrorJSQ("The target user doesn't exist", w, r, u, js) } else if err != nil { return c.InternalErrorJSQ(err, w, r, js) } - err = reply.Like(user.ID) + err = reply.Like(u.ID) if err == c.ErrAlreadyLiked { - return c.LocalErrorJSQ("You've already liked this!", w, r, user, js) + return c.LocalErrorJSQ("You've already liked this!", w, r, u, js) } else if err != nil { return c.InternalErrorJSQ(err, w, r, js) } // ! Be careful about leaking per-route permission state with user ptr - alert := c.Alert{ActorID: user.ID, TargetUserID: reply.CreatedBy, Event: "like", ElementType: "post", ElementID: rid, Actor: user} + alert := c.Alert{ActorID: u.ID, TargetUserID: reply.CreatedBy, Event: "like", ElementType: "post", ElementID: rid, Actor: u} err = c.AddActivityAndNotifyTarget(alert) if err != nil { return c.InternalErrorJSQ(err, w, r, js) } - skip, rerr := lite.Hooks.VhookSkippable("action_end_like_reply", reply.ID, user) + skip, rerr := lite.Hooks.VhookSkippable("action_end_like_reply", reply.ID, u) if skip || rerr != nil { return rerr } @@ -522,7 +522,7 @@ func ReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid return nil } -func ReplyUnlikeSubmit(w http.ResponseWriter, r *http.Request, user *c.User, srid string) c.RouteError { +func ReplyUnlikeSubmit(w http.ResponseWriter, r *http.Request, u *c.User, srid string) c.RouteError { js := r.PostFormValue("js") == "1" rid, err := strconv.Atoi(srid) if err != nil { @@ -544,22 +544,22 @@ func ReplyUnlikeSubmit(w http.ResponseWriter, r *http.Request, user *c.User, sri } // TODO: Add hooks to make use of headerLite - lite, ferr := c.SimpleForumUserCheck(w, r, user, topic.ParentID) + lite, ferr := c.SimpleForumUserCheck(w, r, u, topic.ParentID) if ferr != nil { return ferr } - if !user.Perms.ViewTopic || !user.Perms.LikeItem { - return c.NoPermissionsJSQ(w, r, user, js) + if !u.Perms.ViewTopic || !u.Perms.LikeItem { + return c.NoPermissionsJSQ(w, r, u, js) } _, err = c.Users.Get(reply.CreatedBy) if err != nil && err != sql.ErrNoRows { - return c.LocalErrorJSQ("The target user doesn't exist", w, r, user, js) + return c.LocalErrorJSQ("The target user doesn't exist", w, r, u, js) } else if err != nil { return c.InternalErrorJSQ(err, w, r, js) } - err = reply.Unlike(user.ID) + err = reply.Unlike(u.ID) if err != nil { return c.InternalErrorJSQ(err, w, r, js) } @@ -577,7 +577,7 @@ func ReplyUnlikeSubmit(w http.ResponseWriter, r *http.Request, user *c.User, sri return c.InternalErrorJSQ(err, w, r, js) } - skip, rerr := lite.Hooks.VhookSkippable("action_end_unlike_reply", reply.ID, user) + skip, rerr := lite.Hooks.VhookSkippable("action_end_unlike_reply", reply.ID, u) if skip || rerr != nil { return rerr } diff --git a/routes/topic.go b/routes/topic.go index ba25cccc..e52d5a1e 100644 --- a/routes/topic.go +++ b/routes/topic.go @@ -109,14 +109,15 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user *c.User, header *c.H return c.InternalError(err, w, r) } - var poll c.Poll + var poll *c.Poll if topic.Poll != 0 { pPoll, err := c.Polls.Get(topic.Poll) if err != nil { log.Print("Couldn't find the attached poll for topic " + strconv.Itoa(topic.ID)) return c.InternalError(err, w, r) } - poll = pPoll.Copy() + poll = new(c.Poll) + *poll = pPoll.Copy() } if topic.LikeCount > 0 && user.Liked > 0 { @@ -149,13 +150,12 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user *c.User, header *c.H if strings.HasPrefix(r.URL.Fragment, "post-") { pFrag, _ = strconv.Atoi(strings.TrimPrefix(r.URL.Fragment, "post-")) } - rlist, ogdesc, err := topic.Replies(offset, pFrag, user) + rlist, err := topic.Replies(offset, pFrag, user) if err == sql.ErrNoRows { return c.LocalError("Bad Page. Some of the posts may have been deleted or you got here by directly typing in the page number.", w, r, user) } else if err != nil { return c.InternalError(err, w, r) } - header.OGDesc = ogdesc tpage.ItemList = rlist } @@ -166,7 +166,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user *c.User, header *c.H var rerr c.RouteError tmpl := forum.Tmpl if r.FormValue("i") == "1" { - if tpage.Poll.ID != 0 { + if tpage.Poll != nil { header.AddXRes("chartist/chartist.min.css", "chartist/chartist.min.js") } if tmpl == "" { @@ -179,7 +179,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user *c.User, header *c.H } } } else { - if tpage.Poll.ID != 0 { + if tpage.Poll != nil { header.AddSheet("chartist/chartist.min.css") header.AddScript("chartist/chartist.min.js") } diff --git a/templates/panel_group_menu.html b/templates/panel_group_menu.html index 51f9503f..e1fd32e7 100644 --- a/templates/panel_group_menu.html +++ b/templates/panel_group_menu.html @@ -1,4 +1,4 @@ -