Add TopicListIntTest interface.

Use inqbuild2 in DefaultTopicList.Tick.
Avoid doing a full paginator calculation in the topic list when the result is obvious.
Add defaultPagi and setForumList methods to DefaultTopicList.
Cache two more items in qcache.
Use a string builder in inqbuild and inqbuildstr.
Add the inqbuild2 function.
Reduce boilerplate in topic.go
This commit is contained in:
Azareal 2021-03-24 22:07:46 +10:00
parent 26e8bf32a7
commit 300defd460
3 changed files with 142 additions and 126 deletions

View File

@ -12,7 +12,7 @@ import (
"log" "log"
"os" "os"
"strconv" "strconv"
//"sync" "strings"
"sync/atomic" "sync/atomic"
"time" "time"
@ -223,10 +223,10 @@ func eachall(stmt *sql.Stmt, f func(r *sql.Rows) error) error {
return rows.Err() return rows.Err()
} }
var qcache = []string{0: "?", 1: "?,?", 2: "?,?,?", 3: "?,?,?,?", 4: "?,?,?,?,?", 5: "?,?,?,?,?,?"} var qcache = []string{0: "?", 1: "?,?", 2: "?,?,?", 3: "?,?,?,?", 4: "?,?,?,?,?", 5: "?,?,?,?,?,?", 6: "?,?,?,?,?,?,?", 7: "?,?,?,?,?,?,?,?"}
func inqbuild(ids []int) ([]interface{}, string) { func inqbuild(ids []int) ([]interface{}, string) {
if len(ids) < 5 { if len(ids) < 7 {
idList := make([]interface{}, len(ids)) idList := make([]interface{}, len(ids))
for i, id := range ids { for i, id := range ids {
idList[i] = strconv.Itoa(id) idList[i] = strconv.Itoa(id)
@ -234,21 +234,38 @@ func inqbuild(ids []int) ([]interface{}, string) {
return idList, qcache[len(ids)-1] return idList, qcache[len(ids)-1]
} }
var q string var sb strings.Builder
sb.Grow((len(ids) * 2) - 1)
idList := make([]interface{}, len(ids)) idList := make([]interface{}, len(ids))
for i, id := range ids { for i, id := range ids {
idList[i] = strconv.Itoa(id) idList[i] = strconv.Itoa(id)
if i == 0 { if i == 0 {
q = "?" sb.WriteRune('?')
} else { } else {
q += ",?" sb.WriteString(",?")
} }
} }
return idList, q return idList, sb.String()
}
func inqbuild2(count int) string {
if count <= 7 {
return qcache[count-1]
}
var sb strings.Builder
sb.Grow((count * 2) - 1)
for i := 0; i <= count; i++ {
if i == 0 {
sb.WriteRune('?')
} else {
sb.WriteString(",?")
}
}
return sb.String()
} }
func inqbuildstr(strs []string) ([]interface{}, string) { func inqbuildstr(strs []string) ([]interface{}, string) {
if len(strs) < 5 { if len(strs) < 7 {
idList := make([]interface{}, len(strs)) idList := make([]interface{}, len(strs))
for i, id := range strs { for i, id := range strs {
idList[i] = id idList[i] = id
@ -256,15 +273,16 @@ func inqbuildstr(strs []string) ([]interface{}, string) {
return idList, qcache[len(strs)-1] return idList, qcache[len(strs)-1]
} }
var q string var sb strings.Builder
sb.Grow((len(strs) * 2) - 1)
idList := make([]interface{}, len(strs)) idList := make([]interface{}, len(strs))
for i, id := range strs { for i, id := range strs {
idList[i] = id idList[i] = id
if i == 0 { if i == 0 {
q = "?" sb.WriteRune('?')
} else { } else {
q += ",?" sb.WriteString(",?")
} }
} }
return idList, q return idList, sb.String()
} }

View File

@ -28,6 +28,7 @@ type ForumTopicListHolder struct {
Paginator Paginator Paginator Paginator
} }
// TODO: Should we return no rows errors on empty pages? Is this likely to break something?
type TopicListInt interface { type TopicListInt interface {
GetListByCanSee(canSee []int, page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error) GetListByCanSee(canSee []int, page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error)
GetListByGroup(g *Group, page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error) GetListByGroup(g *Group, page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error)
@ -35,6 +36,11 @@ type TopicListInt interface {
GetList(page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error) GetList(page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error)
} }
type TopicListIntTest interface {
RawGetListByForum(f *Forum, page, orderby int) (topicList []*TopicsRow, pagi Paginator, err error)
Tick() error
}
type DefaultTopicList struct { type DefaultTopicList struct {
// TODO: Rewrite this to put permTree as the primary and put canSeeStr on each group? // TODO: Rewrite this to put permTree as the primary and put canSeeStr on each group?
oddGroups map[int][2]*TopicListHolder oddGroups map[int][2]*TopicListHolder
@ -73,9 +79,7 @@ func NewDefaultTopicList(acc *qgen.Accumulator) (*DefaultTopicList, error) {
if err := acc.FirstError(); err != nil { if err := acc.FirstError(); err != nil {
return nil, err return nil, err
} }
if err := tList.Tick(); err != nil {
err := tList.Tick()
if err != nil {
return nil, err return nil, err
} }
@ -181,13 +185,7 @@ func (tList *DefaultTopicList) Tick() error {
continue continue
} }
var qlist string qlist := inqbuild2(top - 1)
for i := 0; i < top; i++ {
if i != 0 {
qlist += ","
}
qlist += "?"
}
cols := "tid,title,content,createdBy,is_closed,sticky,createdAt,lastReplyAt,lastReplyBy,lastReplyID,parentID,views,postCount,likeCount,attachCount,poll,data" cols := "tid,title,content,createdBy,is_closed,sticky,createdAt,lastReplyAt,lastReplyBy,lastReplyID,parentID,views,postCount,likeCount,attachCount,poll,data"
stmt, err := qgen.Builder.SimpleSelect("topics", cols, "parentID IN("+qlist+")", "views DESC,lastReplyAt DESC,createdBy DESC", "?,?") stmt, err := qgen.Builder.SimpleSelect("topics", cols, "parentID IN("+qlist+")", "views DESC,lastReplyAt DESC,createdBy DESC", "?,?")
@ -260,14 +258,9 @@ func (tList *DefaultTopicList) Tick() error {
// TODO: Avoid rebuilding the entire list on every tick // TODO: Avoid rebuilding the entire list on every tick
fList := make(map[int]*ForumTopicListHolder) fList := make(map[int]*ForumTopicListHolder)
for _, f := range fshort { for _, f := range fshort {
topicList, pagi := []*TopicsRow{}, Paginator{} topicList, pagi := []*TopicsRow{}, tList.defaultPagi()
if f.TopicCount == 0 { if f.TopicCount != 0 {
page := 1 topicList, pagi, err = tList.RawGetListByForum(f, 1, 0)
_, page, lastPage := PageOffset(f.TopicCount, page, Config.ItemsPerPage)
pageList := Paginate(page, lastPage, 5)
pagi = Paginator{pageList, page, lastPage}
} else {
topicList, pagi, err = tList.getListByForum(f, 1, 0)
if err != nil { if err != nil {
return err return err
} }
@ -281,9 +274,8 @@ func (tList *DefaultTopicList) Tick() error {
fList[f.ID] = &ForumTopicListHolder{topicList, pagi}*/ fList[f.ID] = &ForumTopicListHolder{topicList, pagi}*/
} }
tList.forumLock.Lock() //fmt.Printf("fList: %+v\n", fList)
tList.forums = fList tList.setForumList(fList)
tList.forumLock.Unlock()
hTbl := GetHookTable() hTbl := GetHookTable()
_, _ = hTbl.VhookSkippable("tasks_tick_topic_list", tList) _, _ = hTbl.VhookSkippable("tasks_tick_topic_list", tList)
@ -291,6 +283,19 @@ func (tList *DefaultTopicList) Tick() error {
return nil return nil
} }
func (tList *DefaultTopicList) defaultPagi() Paginator {
/*_, page, lastPage := PageOffset(f.TopicCount, page, Config.ItemsPerPage)
pageList := Paginate(page, lastPage, 5)
return topicList, Paginator{pageList, page, lastPage}, nil*/
return Paginator{[]int{}, 1, 1}
}
func (tList *DefaultTopicList) setForumList(forums map[int]*ForumTopicListHolder) {
tList.forumLock.Lock()
tList.forums = forums
tList.forumLock.Unlock()
}
/*var reloadForumMutex sync.Mutex /*var reloadForumMutex sync.Mutex
// TODO: Avoid firing this multiple times per sec tick // TODO: Avoid firing this multiple times per sec tick
@ -313,13 +318,8 @@ func (tList *DefaultTopicList) ReloadForum(id int) error {
} }
tList.forumLock.Unlock() tList.forumLock.Unlock()
topicList, pagi := []*TopicsRow{}, Paginator{} topicList, pagi := []*TopicsRow{}, tList.defaultPagi()
if forum.TopicCount == 0 { if forum.TopicCount != 0 {
page := 1
_, page, lastPage := PageOffset(forum.TopicCount, page, Config.ItemsPerPage)
pageList := Paginate(page, lastPage, 5)
pagi = Paginator{pageList, page, lastPage}
} else {
topicList, pagi, err = tList.getListByForum(forum, 1, 0) topicList, pagi, err = tList.getListByForum(forum, 1, 0)
if err != nil { if err != nil {
return err return err
@ -327,9 +327,7 @@ func (tList *DefaultTopicList) ReloadForum(id int) error {
} }
fList[forum.ID] = &ForumTopicListHolder{topicList, pagi} fList[forum.ID] = &ForumTopicListHolder{topicList, pagi}
tList.forumLock.Lock() tList.setForumList(fList)
tList.forums = fList
tList.forumLock.Unlock()
return nil return nil
}*/ }*/
@ -340,9 +338,7 @@ func (tList *DefaultTopicList) GetListByForum(f *Forum, page, orderby int) (topi
page = 1 page = 1
} }
if f.TopicCount == 0 { if f.TopicCount == 0 {
_, page, lastPage := PageOffset(f.TopicCount, page, Config.ItemsPerPage) return topicList, tList.defaultPagi(), nil
pageList := Paginate(page, lastPage, 5)
return topicList, Paginator{pageList, page, lastPage}, nil
} }
if page == 1 && orderby == 0 { if page == 1 && orderby == 0 {
var h *ForumTopicListHolder var h *ForumTopicListHolder
@ -354,16 +350,16 @@ func (tList *DefaultTopicList) GetListByForum(f *Forum, page, orderby int) (topi
return h.List, h.Paginator, nil return h.List, h.Paginator, nil
} }
} }
return tList.getListByForum(f, page, orderby) return tList.RawGetListByForum(f, page, orderby)
} }
func (tList *DefaultTopicList) getListByForum(f *Forum, page, orderby int) (topicList []*TopicsRow, pagi Paginator, err error) { func (tList *DefaultTopicList) RawGetListByForum(f *Forum, page, orderby int) (topicList []*TopicsRow, pagi Paginator, err error) {
// TODO: Does forum.TopicCount take the deleted items into consideration for guests? We don't have soft-delete yet, only hard-delete // TODO: Does forum.TopicCount take the deleted items into consideration for guests? We don't have soft-delete yet, only hard-delete
offset, page, lastPage := PageOffset(f.TopicCount, page, Config.ItemsPerPage) offset, page, lastPage := PageOffset(f.TopicCount, page, Config.ItemsPerPage)
rows, err := tList.getTopicsByForum.Query(f.ID, offset, Config.ItemsPerPage) rows, err := tList.getTopicsByForum.Query(f.ID, offset, Config.ItemsPerPage)
if err != nil { if err != nil {
return nil, Paginator{nil, 1, 1}, err return nil, tList.defaultPagi(), err
} }
defer rows.Close() defer rows.Close()
@ -373,7 +369,7 @@ func (tList *DefaultTopicList) getListByForum(f *Forum, page, orderby int) (topi
t := TopicsRow{Topic: Topic{ParentID: f.ID}} t := TopicsRow{Topic: Topic{ParentID: f.ID}}
err := rows.Scan(&t.ID, &t.Title, &t.Content, &t.CreatedBy, &t.IsClosed, &t.Sticky, &t.CreatedAt, &t.LastReplyAt, &t.LastReplyBy, &t.LastReplyID, &t.ViewCount, &t.PostCount, &t.LikeCount) err := rows.Scan(&t.ID, &t.Title, &t.Content, &t.CreatedBy, &t.IsClosed, &t.Sticky, &t.CreatedAt, &t.LastReplyAt, &t.LastReplyBy, &t.LastReplyID, &t.ViewCount, &t.PostCount, &t.LikeCount)
if err != nil { if err != nil {
return nil, Paginator{nil, 1, 1}, err return nil, tList.defaultPagi(), err
} }
t.Link = BuildTopicURL(NameToSlug(t.Title), t.ID) t.Link = BuildTopicURL(NameToSlug(t.Title), t.ID)
@ -387,7 +383,7 @@ func (tList *DefaultTopicList) getListByForum(f *Forum, page, orderby int) (topi
reqUserList[t.LastReplyBy] = true reqUserList[t.LastReplyBy] = true
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
return nil, Paginator{nil, 1, 1}, err return nil, tList.defaultPagi(), err
} }
// Convert the user ID map to a slice, then bulk load the users // Convert the user ID map to a slice, then bulk load the users
@ -401,7 +397,7 @@ func (tList *DefaultTopicList) getListByForum(f *Forum, page, orderby int) (topi
// TODO: What if a user is deleted via the Control Panel? // TODO: What if a user is deleted via the Control Panel?
userList, err := Users.BulkGetMap(idSlice) userList, err := Users.BulkGetMap(idSlice)
if err != nil { if err != nil {
return nil, Paginator{nil, 1, 1}, err return nil, tList.defaultPagi(), err
} }
// Second pass to the add the user data // Second pass to the add the user data
@ -411,6 +407,9 @@ func (tList *DefaultTopicList) getListByForum(f *Forum, page, orderby int) (topi
t.LastUser = userList[t.LastReplyBy] t.LastUser = userList[t.LastReplyBy]
} }
if len(topicList) == 0 {
return topicList, tList.defaultPagi(), nil
}
pageList := Paginate(page, lastPage, 5) pageList := Paginate(page, lastPage, 5)
return topicList, Paginator{pageList, page, lastPage}, nil return topicList, Paginator{pageList, page, lastPage}, nil
} }
@ -489,7 +488,7 @@ func (tList *DefaultTopicList) GetListByCanSee(canSee []int, page, orderby int,
argList, qlist := ForumListToArgQ(filteredForums) argList, qlist := ForumListToArgQ(filteredForums)
if qlist == "" { if qlist == "" {
// We don't want to kill the page, so pass an empty slice and nil error // We don't want to kill the page, so pass an empty slice and nil error
return topicList, filteredForums, Paginator{[]int{}, 1, 1}, nil return topicList, filteredForums, tList.defaultPagi(), nil
} }
topicList, pagi, err = tList.getList(page, orderby, topicCount, argList, qlist) topicList, pagi, err = tList.getList(page, orderby, topicCount, argList, qlist)
@ -501,7 +500,7 @@ func (tList *DefaultTopicList) GetList(page, orderby int, filterIDs []int) (topi
// TODO: Make CanSee a method on *Group with a canSee field? Have a CanSee method on *User to cover the case of superadmins? // TODO: Make CanSee a method on *Group with a canSee field? Have a CanSee method on *User to cover the case of superadmins?
cCanSee, err := Forums.GetAllVisibleIDs() cCanSee, err := Forums.GetAllVisibleIDs()
if err != nil { if err != nil {
return nil, nil, Paginator{nil, 1, 1}, err return nil, nil, tList.defaultPagi(), err
} }
//log.Printf("cCanSee: %+v\n", cCanSee) //log.Printf("cCanSee: %+v\n", cCanSee)
inSlice := func(haystack []int, needle int) bool { inSlice := func(haystack []int, needle int) bool {
@ -546,7 +545,7 @@ func (tList *DefaultTopicList) GetList(page, orderby int, filterIDs []int) (topi
argList, qlist := ForumListToArgQ(forumList) argList, qlist := ForumListToArgQ(forumList)
if qlist == "" { if qlist == "" {
// If the super admin can't see anything, then things have gone terribly wrong // If the super admin can't see anything, then things have gone terribly wrong
return topicList, forumList, Paginator{[]int{}, 1, 1}, err return topicList, forumList, tList.defaultPagi(), err
} }
topicList, pagi, err = tList.getList(page, orderby, topicCount, argList, qlist) topicList, pagi, err = tList.getList(page, orderby, topicCount, argList, qlist)
@ -557,7 +556,7 @@ func (tList *DefaultTopicList) GetList(page, orderby int, filterIDs []int) (topi
// TODO: Make orderby an enum of sorts // TODO: Make orderby an enum of sorts
func (tList *DefaultTopicList) getList(page, orderby, topicCount int, argList []interface{}, qlist string) (topicList []*TopicsRow, paginator Paginator, err error) { func (tList *DefaultTopicList) getList(page, orderby, topicCount int, argList []interface{}, qlist string) (topicList []*TopicsRow, paginator Paginator, err error) {
if topicCount == 0 { if topicCount == 0 {
return nil, Paginator{nil, 1, 1}, err return nil, tList.defaultPagi(), err
} }
//log.Printf("argList: %+v\n",argList) //log.Printf("argList: %+v\n",argList)
//log.Printf("qlist: %+v\n",qlist) //log.Printf("qlist: %+v\n",qlist)
@ -580,12 +579,12 @@ func (tList *DefaultTopicList) getList(page, orderby, topicCount int, argList []
} }
topicCount, err = ArgQToWeekViewTopicCount(argList, qlist) topicCount, err = ArgQToWeekViewTopicCount(argList, qlist)
if err != nil { if err != nil {
return nil, Paginator{nil, 1, 1}, err return nil, tList.defaultPagi(), err
} }
acc := qgen.NewAcc() acc := qgen.NewAcc()
stmt = acc.Select("topics").Columns(cols).Where("parentID IN(" + qlist + ") AND (weekEvenViews!=0 OR weekOddViews!=0)").Orderby(orderq).Limit("?,?").ComplexPrepare() stmt = acc.Select("topics").Columns(cols).Where("parentID IN(" + qlist + ") AND (weekEvenViews!=0 OR weekOddViews!=0)").Orderby(orderq).Limit("?,?").ComplexPrepare()
if e := acc.FirstError(); e != nil { if e := acc.FirstError(); e != nil {
return nil, Paginator{nil, 1, 1}, e return nil, tList.defaultPagi(), e
} }
defer stmt.Close() defer stmt.Close()
} }
@ -612,7 +611,7 @@ func (tList *DefaultTopicList) getList(page, orderby, topicCount int, argList []
if stmt == nil { if stmt == nil {
stmt, err = qgen.Builder.SimpleSelect("topics", cols, "parentID IN("+qlist+")", orderq, "?,?") stmt, err = qgen.Builder.SimpleSelect("topics", cols, "parentID IN("+qlist+")", orderq, "?,?")
if err != nil { if err != nil {
return nil, Paginator{nil, 1, 1}, err return nil, tList.defaultPagi(), err
} }
defer stmt.Close() defer stmt.Close()
} }
@ -622,14 +621,13 @@ func (tList *DefaultTopicList) getList(page, orderby, topicCount int, argList []
rows, err := stmt.Query(argList...) rows, err := stmt.Query(argList...)
if err != nil { if err != nil {
return nil, Paginator{nil, 1, 1}, err return nil, tList.defaultPagi(), err
} }
defer rows.Close() defer rows.Close()
rc := Rstore.GetCache() rc, tc := Rstore.GetCache(), Topics.GetCache()
rcap := rc.GetCapacity() rcap := rc.GetCapacity()
rlen := rc.Length() rlen := rc.Length()
tc := Topics.GetCache()
reqUserList := make(map[int]bool) reqUserList := make(map[int]bool)
for rows.Next() { for rows.Next() {
// TODO: Embed Topic structs in TopicsRow to make it easier for us to reuse this work in the topic cache // TODO: Embed Topic structs in TopicsRow to make it easier for us to reuse this work in the topic cache
@ -637,7 +635,7 @@ func (tList *DefaultTopicList) getList(page, orderby, topicCount int, argList []
//var weekViews []uint8 //var weekViews []uint8
err := rows.Scan(&t.ID, &t.Title, &t.Content, &t.CreatedBy, &t.IsClosed, &t.Sticky, &t.CreatedAt, &t.LastReplyAt, &t.LastReplyBy, &t.LastReplyID, &t.ParentID, &t.ViewCount, &t.PostCount, &t.LikeCount, &t.AttachCount, &t.Poll, &t.Data, &t.WeekViews) err := rows.Scan(&t.ID, &t.Title, &t.Content, &t.CreatedBy, &t.IsClosed, &t.Sticky, &t.CreatedAt, &t.LastReplyAt, &t.LastReplyBy, &t.LastReplyID, &t.ParentID, &t.ViewCount, &t.PostCount, &t.LikeCount, &t.AttachCount, &t.Poll, &t.Data, &t.WeekViews)
if err != nil { if err != nil {
return nil, Paginator{nil, 1, 1}, err return nil, tList.defaultPagi(), err
} }
//t.WeekViews = int(weekViews[0]) //t.WeekViews = int(weekViews[0])
//log.Printf("t: %+v\n", t) //log.Printf("t: %+v\n", t)
@ -675,7 +673,7 @@ func (tList *DefaultTopicList) getList(page, orderby, topicCount int, argList []
if t.PostCount == 2 && rlen < rcap && !hRids && page < 5 { if t.PostCount == 2 && rlen < rcap && !hRids && page < 5 {
rids, err := GetRidsForTopic(t.ID, 0) rids, err := GetRidsForTopic(t.ID, 0)
if err != nil { if err != nil {
return nil, Paginator{nil, 1, 1}, err return nil, tList.defaultPagi(), err
} }
//log.Print("rids: ", rids) //log.Print("rids: ", rids)
@ -695,7 +693,7 @@ func (tList *DefaultTopicList) getList(page, orderby, topicCount int, argList []
} }
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
return nil, Paginator{nil, 1, 1}, err return nil, tList.defaultPagi(), err
} }
// TODO: specialcase for when reqUserList only has one or two items to avoid map alloc // TODO: specialcase for when reqUserList only has one or two items to avoid map alloc
@ -704,7 +702,7 @@ func (tList *DefaultTopicList) getList(page, orderby, topicCount int, argList []
for uid, _ := range reqUserList { for uid, _ := range reqUserList {
u, err = Users.Get(uid) u, err = Users.Get(uid)
if err != nil { if err != nil {
return nil, Paginator{nil, 1, 1}, err return nil, tList.defaultPagi(), err
} }
} }
for _, t := range topicList { for _, t := range topicList {
@ -723,7 +721,7 @@ func (tList *DefaultTopicList) getList(page, orderby, topicCount int, argList []
// TODO: What if a user is deleted via the Control Panel? // TODO: What if a user is deleted via the Control Panel?
userList, err := Users.BulkGetMap(idSlice) userList, err := Users.BulkGetMap(idSlice)
if err != nil { if err != nil {
return nil, Paginator{nil, 1, 1}, err return nil, tList.defaultPagi(), err
} }
// Second pass to the add the user data // Second pass to the add the user data

View File

@ -8,7 +8,6 @@ import (
"errors" "errors"
"io" "io"
//"fmt"
"image" "image"
"image/gif" "image/gif"
"image/jpeg" "image/jpeg"
@ -24,7 +23,7 @@ import (
c "github.com/Azareal/Gosora/common" c "github.com/Azareal/Gosora/common"
co "github.com/Azareal/Gosora/common/counters" co "github.com/Azareal/Gosora/common/counters"
"github.com/Azareal/Gosora/common/phrases" p "github.com/Azareal/Gosora/common/phrases"
qgen "github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
) )
@ -48,7 +47,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user *c.User, h *c.Header
page, _ := strconv.Atoi(r.FormValue("page")) page, _ := strconv.Atoi(r.FormValue("page"))
_, tid, err := ParseSEOURL(urlBit) _, tid, err := ParseSEOURL(urlBit)
if err != nil { if err != nil {
return c.SimpleError(phrases.GetErrorPhrase("url_id_must_be_integer"), w, r, h) return c.SimpleError(p.GetErrorPhrase("url_id_must_be_integer"), w, r, h)
} }
// Get the topic... // Get the topic...
@ -158,6 +157,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user *c.User, h *c.Header
} else if err != nil { } else if err != nil {
return c.InternalError(err, w, r) return c.InternalError(err, w, r)
} }
//fmt.Printf("rlist: %+v\n",rlist)
tpage.ItemList = rlist tpage.ItemList = rlist
if externalHead { if externalHead {
h.ExternalMedia = true h.ExternalMedia = true
@ -206,7 +206,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user *c.User, h *c.Header
func AttachTopicActCommon(w http.ResponseWriter, r *http.Request, u *c.User, stid string) (t *c.Topic, ferr c.RouteError) { func AttachTopicActCommon(w http.ResponseWriter, r *http.Request, u *c.User, stid string) (t *c.Topic, ferr c.RouteError) {
tid, err := strconv.Atoi(stid) tid, err := strconv.Atoi(stid)
if err != nil { if err != nil {
return t, c.LocalErrorJS(phrases.GetErrorPhrase("id_must_be_integer"), w, r) return t, c.LocalErrorJS(p.GetErrorPhrase("id_must_be_integer"), w, r)
} }
t, err = c.Topics.Get(tid) t, err = c.Topics.Get(tid)
if err != nil { if err != nil {
@ -268,7 +268,7 @@ func RemoveAttachFromTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.Us
for _, said := range strings.Split(r.PostFormValue("aids"), ",") { for _, said := range strings.Split(r.PostFormValue("aids"), ",") {
aid, err := strconv.Atoi(said) aid, err := strconv.Atoi(said)
if err != nil { if err != nil {
return c.LocalErrorJS(phrases.GetErrorPhrase("id_must_be_integer"), w, r) return c.LocalErrorJS(p.GetErrorPhrase("id_must_be_integer"), w, r)
} }
rerr := deleteAttachment(w, r, u, aid, true) rerr := deleteAttachment(w, r, u, aid, true)
if rerr != nil { if rerr != nil {
@ -293,7 +293,7 @@ func CreateTopic(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header,
if sfid != "" { if sfid != "" {
fid, err = strconv.Atoi(sfid) fid, err = strconv.Atoi(sfid)
if err != nil { if err != nil {
return c.LocalError(phrases.GetErrorPhrase("url_id_must_be_integer"), w, r, u) return c.LocalError(p.GetErrorPhrase("url_id_must_be_integer"), w, r, u)
} }
} }
if fid == 0 { if fid == 0 {
@ -308,7 +308,7 @@ func CreateTopic(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header,
return c.NoPermissions(w, r, u) return c.NoPermissions(w, r, u)
} }
// TODO: Add a phrase for this // TODO: Add a phrase for this
h.Title = phrases.GetTitlePhrase("create_topic") h.Title = p.GetTitlePhrase("create_topic")
h.Zone = "create_topic" h.Zone = "create_topic"
// Lock this to the forum being linked? // Lock this to the forum being linked?
@ -361,7 +361,7 @@ func CreateTopic(w http.ResponseWriter, r *http.Request, u *c.User, h *c.Header,
func CreateTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { func CreateTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError {
fid, err := strconv.Atoi(r.PostFormValue("board")) fid, err := strconv.Atoi(r.PostFormValue("board"))
if err != nil { if err != nil {
return c.LocalError(phrases.GetErrorPhrase("id_must_be_integer"), w, r, u) return c.LocalError(p.GetErrorPhrase("id_must_be_integer"), w, r, u)
} }
// TODO: Add hooks to make use of headerLite // TODO: Add hooks to make use of headerLite
lite, ferr := c.SimpleForumUserCheck(w, r, u, fid) lite, ferr := c.SimpleForumUserCheck(w, r, u, fid)
@ -562,11 +562,11 @@ func uploadFilesWithHash(w http.ResponseWriter, r *http.Request, u *c.User, dir
// TODO: Update the stats after edits so that we don't under or over decrement stats during deletes // TODO: Update the stats after edits so that we don't under or over decrement stats during deletes
// TODO: Disable stat updates in posts handled by plugin_guilds // TODO: Disable stat updates in posts handled by plugin_guilds
func EditTopicSubmit(w http.ResponseWriter, r *http.Request, user *c.User, stid string) c.RouteError { func EditTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.User, stid string) c.RouteError {
js := (r.PostFormValue("js") == "1") js := (r.PostFormValue("js") == "1")
tid, err := strconv.Atoi(stid) tid, err := strconv.Atoi(stid)
if err != nil { if err != nil {
return c.PreErrorJSQ(phrases.GetErrorPhrase("id_must_be_integer"), w, r, js) return c.PreErrorJSQ(p.GetErrorPhrase("id_must_be_integer"), w, r, js)
} }
topic, err := c.Topics.Get(tid) topic, err := c.Topics.Get(tid)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@ -576,15 +576,15 @@ func EditTopicSubmit(w http.ResponseWriter, r *http.Request, user *c.User, stid
} }
// TODO: Add hooks to make use of headerLite // 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 { if ferr != nil {
return ferr return ferr
} }
if !user.Perms.ViewTopic || !user.Perms.EditTopic { if !u.Perms.ViewTopic || !u.Perms.EditTopic {
return c.NoPermissionsJSQ(w, r, user, js) return c.NoPermissionsJSQ(w, r, u, js)
} }
if topic.IsClosed && !user.Perms.CloseTopic { if topic.IsClosed && !u.Perms.CloseTopic {
return c.NoPermissionsJSQ(w, r, user, js) return c.NoPermissionsJSQ(w, r, u, js)
} }
err = topic.Update(r.PostFormValue("name"), r.PostFormValue("content")) err = topic.Update(r.PostFormValue("name"), r.PostFormValue("content"))
@ -592,16 +592,16 @@ func EditTopicSubmit(w http.ResponseWriter, r *http.Request, user *c.User, stid
if err != nil { if err != nil {
switch err { switch err {
case c.ErrNoTitle: case c.ErrNoTitle:
return c.LocalErrorJSQ("This topic doesn't have a title", w, r, user, js) return c.LocalErrorJSQ("This topic doesn't have a title", w, r, u, js)
case c.ErrLongTitle: case c.ErrLongTitle:
return c.LocalErrorJSQ("The length of the title is too long, max: "+strconv.Itoa(c.Config.MaxTopicTitleLength), w, r, user, js) return c.LocalErrorJSQ("The length of the title is too long, max: "+strconv.Itoa(c.Config.MaxTopicTitleLength), w, r, u, js)
case c.ErrNoBody: case c.ErrNoBody:
return c.LocalErrorJSQ("This topic doesn't have a body", w, r, user, js) return c.LocalErrorJSQ("This topic doesn't have a body", w, r, u, js)
} }
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
err = c.Forums.UpdateLastTopic(topic.ID, user.ID, topic.ParentID) err = c.Forums.UpdateLastTopic(topic.ID, u.ID, topic.ParentID)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
@ -614,7 +614,7 @@ func EditTopicSubmit(w http.ResponseWriter, r *http.Request, user *c.User, stid
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
skip, rerr := lite.Hooks.VhookSkippable("action_end_edit_topic", topic.ID, user) skip, rerr := lite.Hooks.VhookSkippable("action_end_edit_topic", topic.ID, u)
if skip || rerr != nil { if skip || rerr != nil {
return rerr return rerr
} }
@ -622,7 +622,7 @@ func EditTopicSubmit(w http.ResponseWriter, r *http.Request, user *c.User, stid
if !js { if !js {
http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther) http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther)
} else { } else {
outBytes, err := json.Marshal(JsonReply{c.ParseMessage(topic.Content, topic.ParentID, "forums", user.ParseSettings, user)}) outBytes, err := json.Marshal(JsonReply{c.ParseMessage(topic.Content, topic.ParentID, "forums", u.ParseSettings, u)})
if err != nil { if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
@ -722,7 +722,7 @@ func StickTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.User, stid st
func topicActionPre(stid, action string, w http.ResponseWriter, r *http.Request, u *c.User) (*c.Topic, *c.HeaderLite, c.RouteError) { func topicActionPre(stid, action string, w http.ResponseWriter, r *http.Request, u *c.User) (*c.Topic, *c.HeaderLite, c.RouteError) {
tid, err := strconv.Atoi(stid) tid, err := strconv.Atoi(stid)
if err != nil { if err != nil {
return nil, nil, c.PreError(phrases.GetErrorPhrase("id_must_be_integer"), w, r) return nil, nil, c.PreError(p.GetErrorPhrase("id_must_be_integer"), w, r)
} }
t, err := c.Topics.Get(tid) t, err := c.Topics.Get(tid)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@ -766,7 +766,7 @@ func UnstickTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.User, stid
return topicActionPost(t.Unstick(), "unstick", w, r, lite, t, u) return topicActionPost(t.Unstick(), "unstick", w, r, lite, t, u)
} }
func LockTopicSubmit(w http.ResponseWriter, r *http.Request, user *c.User) c.RouteError { func LockTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError {
// TODO: Move this to some sort of middleware // TODO: Move this to some sort of middleware
var tids []int var tids []int
js := c.ReqIsJson(r) js := c.ReqIsJson(r)
@ -786,7 +786,7 @@ func LockTopicSubmit(w http.ResponseWriter, r *http.Request, user *c.User) c.Rou
tids = append(tids, tid) tids = append(tids, tid)
} }
if len(tids) == 0 { if len(tids) == 0 {
return c.LocalErrorJSQ("You haven't provided any IDs", w, r, user, js) return c.LocalErrorJSQ("You haven't provided any IDs", w, r, u, js)
} }
for _, tid := range tids { for _, tid := range tids {
@ -798,12 +798,12 @@ func LockTopicSubmit(w http.ResponseWriter, r *http.Request, user *c.User) c.Rou
} }
// TODO: Add hooks to make use of headerLite // 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 { if ferr != nil {
return ferr return ferr
} }
if !user.Perms.ViewTopic || !user.Perms.CloseTopic { if !u.Perms.ViewTopic || !u.Perms.CloseTopic {
return c.NoPermissionsJSQ(w, r, user, js) return c.NoPermissionsJSQ(w, r, u, js)
} }
err = topic.Lock() err = topic.Lock()
@ -811,13 +811,13 @@ func LockTopicSubmit(w http.ResponseWriter, r *http.Request, user *c.User) c.Rou
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
err = addTopicAction("lock", topic, user) err = addTopicAction("lock", topic, u)
if err != nil { if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
// TODO: Do a bulk lock action hook? // TODO: Do a bulk lock action hook?
skip, rerr := lite.Hooks.VhookSkippable("action_end_lock_topic", topic.ID, user) skip, rerr := lite.Hooks.VhookSkippable("action_end_lock_topic", topic.ID, u)
if skip || rerr != nil { if skip || rerr != nil {
return rerr return rerr
} }
@ -842,10 +842,10 @@ func UnlockTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.User, stid s
// ! JS only route // ! JS only route
// TODO: Figure a way to get this route to work without JS // TODO: Figure a way to get this route to work without JS
func MoveTopicSubmit(w http.ResponseWriter, r *http.Request, user *c.User, sfid string) c.RouteError { func MoveTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.User, sfid string) c.RouteError {
fid, err := strconv.Atoi(sfid) fid, err := strconv.Atoi(sfid)
if err != nil { if err != nil {
return c.PreErrorJS(phrases.GetErrorPhrase("id_must_be_integer"), w, r) return c.PreErrorJS(p.GetErrorPhrase("id_must_be_integer"), w, r)
} }
// TODO: Move this to some sort of middleware // TODO: Move this to some sort of middleware
var tids []int var tids []int
@ -869,19 +869,19 @@ func MoveTopicSubmit(w http.ResponseWriter, r *http.Request, user *c.User, sfid
} }
// TODO: Add hooks to make use of headerLite // TODO: Add hooks to make use of headerLite
_, ferr := c.SimpleForumUserCheck(w, r, user, topic.ParentID) _, ferr := c.SimpleForumUserCheck(w, r, u, topic.ParentID)
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
if !user.Perms.ViewTopic || !user.Perms.MoveTopic { if !u.Perms.ViewTopic || !u.Perms.MoveTopic {
return c.NoPermissionsJS(w, r, user) return c.NoPermissionsJS(w, r, u)
} }
lite, ferr := c.SimpleForumUserCheck(w, r, user, fid) lite, ferr := c.SimpleForumUserCheck(w, r, u, fid)
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
if !user.Perms.ViewTopic || !user.Perms.MoveTopic { if !u.Perms.ViewTopic || !u.Perms.MoveTopic {
return c.NoPermissionsJS(w, r, user) return c.NoPermissionsJS(w, r, u)
} }
err = topic.MoveTo(fid) err = topic.MoveTo(fid)
@ -889,13 +889,13 @@ func MoveTopicSubmit(w http.ResponseWriter, r *http.Request, user *c.User, sfid
return c.InternalErrorJS(err, w, r) return c.InternalErrorJS(err, w, r)
} }
// ? - Is there a better way of doing this? // ? - Is there a better way of doing this?
err = addTopicAction("move-"+strconv.Itoa(fid), topic, user) err = addTopicAction("move-"+strconv.Itoa(fid), topic, u)
if err != nil { if err != nil {
return c.InternalErrorJS(err, w, r) return c.InternalErrorJS(err, w, r)
} }
// TODO: Do a bulk move action hook? // TODO: Do a bulk move action hook?
skip, rerr := lite.Hooks.VhookSkippable("action_end_move_topic", topic.ID, user) skip, rerr := lite.Hooks.VhookSkippable("action_end_move_topic", topic.ID, u)
if skip || rerr != nil { if skip || rerr != nil {
return rerr return rerr
} }
@ -916,11 +916,11 @@ func addTopicAction(action string, t *c.Topic, u *c.User) error {
} }
// TODO: Refactor this // TODO: Refactor this
func LikeTopicSubmit(w http.ResponseWriter, r *http.Request, user *c.User, stid string) c.RouteError { func LikeTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.User, stid string) c.RouteError {
js := r.PostFormValue("js") == "1" js := r.PostFormValue("js") == "1"
tid, err := strconv.Atoi(stid) tid, err := strconv.Atoi(stid)
if err != nil { if err != nil {
return c.PreErrorJSQ(phrases.GetErrorPhrase("id_must_be_integer"), w, r, js) return c.PreErrorJSQ(p.GetErrorPhrase("id_must_be_integer"), w, r, js)
} }
topic, err := c.Topics.Get(tid) topic, err := c.Topics.Get(tid)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@ -930,50 +930,50 @@ func LikeTopicSubmit(w http.ResponseWriter, r *http.Request, user *c.User, stid
} }
// TODO: Add hooks to make use of headerLite // 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 { if ferr != nil {
return ferr return ferr
} }
if !user.Perms.ViewTopic || !user.Perms.LikeItem { if !u.Perms.ViewTopic || !u.Perms.LikeItem {
return c.NoPermissionsJSQ(w, r, user, js) return c.NoPermissionsJSQ(w, r, u, js)
} }
if topic.CreatedBy == user.ID { if topic.CreatedBy == u.ID {
return c.LocalErrorJSQ("You can't like your own topics", w, r, user, js) return c.LocalErrorJSQ("You can't like your own topics", w, r, u, js)
} }
_, err = c.Users.Get(topic.CreatedBy) _, err = c.Users.Get(topic.CreatedBy)
if err != nil && err == sql.ErrNoRows { 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 { } else if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
score := 1 score := 1
err = topic.Like(score, user.ID) err = topic.Like(score, u.ID)
if err == c.ErrAlreadyLiked { if err == c.ErrAlreadyLiked {
return c.LocalErrorJSQ("You already liked this", w, r, user, js) return c.LocalErrorJSQ("You already liked this", w, r, u, js)
} else if err != nil { } else if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
// ! Be careful about leaking per-route permission state with user ptr // ! Be careful about leaking per-route permission state with user ptr
alert := c.Alert{ActorID: user.ID, TargetUserID: topic.CreatedBy, Event: "like", ElementType: "topic", ElementID: tid, Actor: user} alert := c.Alert{ActorID: u.ID, TargetUserID: topic.CreatedBy, Event: "like", ElementType: "topic", ElementID: tid, Actor: u}
err = c.AddActivityAndNotifyTarget(alert) err = c.AddActivityAndNotifyTarget(alert)
if err != nil { if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
skip, rerr := lite.Hooks.VhookSkippable("action_end_like_topic", topic.ID, user) skip, rerr := lite.Hooks.VhookSkippable("action_end_like_topic", topic.ID, u)
if skip || rerr != nil { if skip || rerr != nil {
return rerr return rerr
} }
return actionSuccess(w, r, "/topic/"+strconv.Itoa(tid), js) return actionSuccess(w, r, "/topic/"+strconv.Itoa(tid), js)
} }
func UnlikeTopicSubmit(w http.ResponseWriter, r *http.Request, user *c.User, stid string) c.RouteError { func UnlikeTopicSubmit(w http.ResponseWriter, r *http.Request, u *c.User, stid string) c.RouteError {
js := r.PostFormValue("js") == "1" js := r.PostFormValue("js") == "1"
tid, err := strconv.Atoi(stid) tid, err := strconv.Atoi(stid)
if err != nil { if err != nil {
return c.PreErrorJSQ(phrases.GetErrorPhrase("id_must_be_integer"), w, r, js) return c.PreErrorJSQ(p.GetErrorPhrase("id_must_be_integer"), w, r, js)
} }
topic, err := c.Topics.Get(tid) topic, err := c.Topics.Get(tid)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@ -983,21 +983,21 @@ func UnlikeTopicSubmit(w http.ResponseWriter, r *http.Request, user *c.User, sti
} }
// TODO: Add hooks to make use of headerLite // 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 { if ferr != nil {
return ferr return ferr
} }
if !user.Perms.ViewTopic || !user.Perms.LikeItem { if !u.Perms.ViewTopic || !u.Perms.LikeItem {
return c.NoPermissionsJSQ(w, r, user, js) return c.NoPermissionsJSQ(w, r, u, js)
} }
_, err = c.Users.Get(topic.CreatedBy) _, err = c.Users.Get(topic.CreatedBy)
if err != nil && err == sql.ErrNoRows { 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 { } else if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
err = topic.Unlike(user.ID) err = topic.Unlike(u.ID)
if err != nil { if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
@ -1015,7 +1015,7 @@ func UnlikeTopicSubmit(w http.ResponseWriter, r *http.Request, user *c.User, sti
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
skip, rerr := lite.Hooks.VhookSkippable("action_end_unlike_topic", topic.ID, user) skip, rerr := lite.Hooks.VhookSkippable("action_end_unlike_topic", topic.ID, u)
if skip || rerr != nil { if skip || rerr != nil {
return rerr return rerr
} }