gosora/common/topic_list.go

790 lines
27 KiB
Go
Raw Normal View History

package common
import (
2022-02-21 03:53:13 +00:00
"database/sql"
"fmt"
"strconv"
"sync"
"time"
2022-02-21 03:53:13 +00:00
qgen "git.tuxpa.in/a/gosora/query_gen"
)
var TopicList TopicListInt
const (
2022-02-21 03:32:53 +00:00
TopicListDefault = iota
TopicListMostViewed
TopicListWeekViews
)
type TopicListHolder struct {
2022-02-21 03:32:53 +00:00
List []*TopicsRow
ForumList []Forum
Paginator Paginator
}
type ForumTopicListHolder struct {
2022-02-21 03:32:53 +00:00
List []*TopicsRow
Paginator Paginator
}
// TODO: Should we return no rows errors on empty pages? Is this likely to break something?
type TopicListInt interface {
2022-02-21 03:32:53 +00:00
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)
GetListByForum(f *Forum, page, orderby int) (topicList []*TopicsRow, pagi Paginator, err error)
GetList(page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error)
}
type TopicListIntTest interface {
2022-02-21 03:32:53 +00:00
RawGetListByForum(f *Forum, page, orderby int) (topicList []*TopicsRow, pagi Paginator, err error)
Tick() error
}
type DefaultTopicList struct {
2022-02-21 03:32:53 +00:00
// TODO: Rewrite this to put permTree as the primary and put canSeeStr on each group?
oddGroups map[int][2]*TopicListHolder
evenGroups map[int][2]*TopicListHolder
oddLock sync.RWMutex
evenLock sync.RWMutex
2022-02-21 03:32:53 +00:00
forums map[int]*ForumTopicListHolder
forumLock sync.RWMutex
2022-02-21 03:32:53 +00:00
qcounts map[int]*sql.Stmt
qcounts2 map[int]*sql.Stmt
qLock sync.RWMutex
qLock2 sync.RWMutex
2022-02-21 03:32:53 +00:00
//permTree atomic.Value // [string(canSee)]canSee
//permTree map[string][]int // [string(canSee)]canSee
2022-02-21 03:32:53 +00:00
getTopicsByForum *sql.Stmt
//getTidsByForum *sql.Stmt
}
Almost finished live topic lists, you can find them at /topics/. You can disable them via config.json The topic list cache can handle more groups now, but don't go too crazy with groups (e.g. thousands of them). Make the suspicious request logs more descriptive. Added the phrases API endpoint. Split the template phrases up by prefix, more work on this coming up. Removed #dash_saved and part of #dash_username. Removed some temporary artifacts from trying to implement FA5 in Nox. Removed some commented CSS. Fixed template artifact deletion on Windows. Tweaked HTTPSRedirect to make it more compact. Fixed NullUserCache not complying with the expectations for BulkGet. Swapped out a few RunVhook calls for more appropriate RunVhookNoreturn calls. Removed a few redundant IsAdmin checks when IsMod would suffice. Commented out a few pushers. Desktop notification permission requests are no longer served to guests. Split topics.html into topics.html and topics_topic.html RunThemeTemplate should now fallback to interpreted templates properly when the transpiled variants aren't avaialb.e Changed TopicsRow.CreatedAt from a string to a time.Time Added SkipTmplPtrMap to CTemplateConfig. Added SetBuildTags to CTemplateSet. A bit more data is dumped when something goes wrong while transpiling templates now. topics_topic, topic_posts, and topic_alt_posts are now transpiled for the client, although not all of them are ready to be served to the client yet. Client rendered templates now support phrases. Client rendered templates now support loops. Fixed loadAlerts in global.js Refactored some of the template initialisation code to make it less repetitive. Split topic.html into topic.html and topic_posts.html Split topic_alt.html into topic_alt.html and topic_alt_posts.html Added comments for PollCache. Fixed a data race in the MemoryPollCache. The writer is now closed properly in WsHubImpl.broadcastMessage. Fixed a potential deadlock in WsHubImpl.broadcastMessage. Removed some old commented code in websockets.go Added the DisableLiveTopicList config setting.
2018-06-24 13:49:29 +00:00
// We've removed the topic list cache cap as admins really shouldn't be abusing groups like this with plugin_guilds around and it was extremely fiddly.
// If this becomes a problem later on, then we can revisit this with a fresh perspective, particularly with regards to what people expect a group to really be
// Also, keep in mind that as-long as the groups don't all have unique sets of forums they can see, then we can optimise a large portion of the work away.
func NewDefaultTopicList(acc *qgen.Accumulator) (*DefaultTopicList, error) {
2022-02-21 03:32:53 +00:00
tList := &DefaultTopicList{
oddGroups: make(map[int][2]*TopicListHolder),
evenGroups: make(map[int][2]*TopicListHolder),
forums: make(map[int]*ForumTopicListHolder),
qcounts: make(map[int]*sql.Stmt),
qcounts2: make(map[int]*sql.Stmt),
getTopicsByForum: acc.Select("topics").Columns("tid,title,content,createdBy,is_closed,sticky,createdAt,lastReplyAt,lastReplyBy,lastReplyID,views,postCount,likeCount").Where("parentID=?").Orderby("sticky DESC,lastReplyAt DESC,createdBy DESC").Limit("?,?").Prepare(),
//getTidsByForum: acc.Select("topics").Columns("tid").Where("parentID=?").Orderby("sticky DESC,lastReplyAt DESC,createdBy DESC").Limit("?,?").Prepare(),
}
if e := acc.FirstError(); e != nil {
return nil, e
}
if e := tList.Tick(); e != nil {
return nil, e
}
Tasks.HalfSec.Add(tList.Tick)
//Tasks.Sec.Add(tList.GroupCountTick) // TODO: Dynamically change the groups in the short list to be optimised every second
return tList, nil
}
func (tList *DefaultTopicList) Tick() error {
2022-02-21 03:32:53 +00:00
//fmt.Println("TopicList.Tick")
if !TopicListThaw.Thawed() {
return nil
}
//fmt.Println("building topic list")
oddLists := make(map[int][2]*TopicListHolder)
evenLists := make(map[int][2]*TopicListHolder)
addList := func(gid int, h [2]*TopicListHolder) {
if gid%2 == 0 {
evenLists[gid] = h
} else {
oddLists[gid] = h
}
}
allGroups, err := Groups.GetAll()
if err != nil {
return err
}
gidToCanSee := make(map[int]string)
permTree := make(map[string][]int) // [string(canSee)]canSee
for _, g := range allGroups {
// ? - Move the user count check to instance initialisation? Might require more book-keeping, particularly when a user moves into a zero user group
if g.UserCount == 0 && g.ID != GuestUser.Group {
continue
}
canSee := make([]byte, len(g.CanSee))
for i, item := range g.CanSee {
canSee[i] = byte(item)
}
canSeeInt := make([]int, len(canSee))
copy(canSeeInt, g.CanSee)
sCanSee := string(canSee)
permTree[sCanSee] = canSeeInt
gidToCanSee[g.ID] = sCanSee
}
canSeeHolders := make(map[string][2]*TopicListHolder)
forumCounts := make(map[int]int)
for name, canSee := range permTree {
topicList, forumList, pagi, err := tList.GetListByCanSee(canSee, 1, 0, nil)
if err != nil {
return err
}
topicList2, forumList2, pagi2, err := tList.GetListByCanSee(canSee, 2, 0, nil)
if err != nil {
return err
}
canSeeHolders[name] = [2]*TopicListHolder{
{topicList, forumList, pagi},
{topicList2, forumList2, pagi2},
}
if len(canSee) > 1 {
forumCounts[len(canSee)] += 1
}
}
for gid, canSee := range gidToCanSee {
addList(gid, canSeeHolders[canSee])
}
tList.oddLock.Lock()
tList.oddGroups = oddLists
tList.oddLock.Unlock()
tList.evenLock.Lock()
tList.evenGroups = evenLists
tList.evenLock.Unlock()
topc := []int{0, 0, 0, 0, 0, 0}
addC := func(c int) {
lowI, low := 0, topc[0]
for i, top := range topc {
if top < low {
lowI = i
low = top
}
}
if c > low {
topc[lowI] = c
}
}
for forumCount := range forumCounts {
addC(forumCount)
}
qcounts := make(map[int]*sql.Stmt)
qcounts2 := make(map[int]*sql.Stmt)
for _, top := range topc {
if top == 0 {
continue
}
qlist := inqbuild2(top - 1)
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", "?,?")
if err != nil {
return err
}
qcounts[top] = stmt
stmt, err = qgen.Builder.SimpleSelect("topics", cols, "parentID IN("+qlist+")", "sticky DESC,lastReplyAt DESC,createdBy DESC", "?,?")
if err != nil {
return err
}
qcounts2[top] = stmt
}
tList.qLock.Lock()
tList.qcounts = qcounts
tList.qLock.Unlock()
tList.qLock2.Lock()
tList.qcounts2 = qcounts2
tList.qLock2.Unlock()
fmt.Printf("Forums: %+v\n", Forums)
forums, err := Forums.GetAll()
if err != nil {
return err
}
top8 := []*Forum{nil, nil, nil, nil, nil, nil, nil, nil}
z := true
addScore2 := func(f *Forum) {
for i, top := range top8 {
if top.TopicCount < f.TopicCount {
top8[i] = f
return
}
}
}
addScore := func(f *Forum) {
if z {
for i, top := range top8 {
if top == nil {
top8[i] = f
return
}
}
z = false
addScore2(f)
}
addScore2(f)
}
var fshort []*Forum
for _, f := range forums {
if f.Name == "" || !f.Active || (f.ParentType != "" && f.ParentType != "forum") {
continue
}
if f.TopicCount == 0 {
fshort = append(fshort, f)
continue
}
addScore(f)
}
for _, f := range top8 {
if f != nil {
fshort = append(fshort, f)
}
}
// TODO: Avoid rebuilding the entire list on every tick
fList := make(map[int]*ForumTopicListHolder)
for _, f := range fshort {
topicList, pagi := []*TopicsRow{}, tList.defaultPagi()
if f.TopicCount != 0 {
topicList, pagi, err = tList.RawGetListByForum(f, 1, 0)
if err != nil {
return err
}
}
fList[f.ID] = &ForumTopicListHolder{topicList, pagi}
/*topicList, pagi, err := tList.GetListByForum(f, 1, 0)
if err != nil {
return err
}
fList[f.ID] = &ForumTopicListHolder{topicList, pagi}*/
}
//fmt.Printf("fList: %+v\n", fList)
tList.setForumList(fList)
hTbl := GetHookTable()
_, _ = hTbl.VhookSkippable("tasks_tick_topic_list", tList)
return nil
}
func (tList *DefaultTopicList) defaultPagi() Paginator {
2022-02-21 03:32:53 +00:00
/*_, 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) {
2022-02-21 03:32:53 +00:00
tList.forumLock.Lock()
tList.forums = forums
tList.forumLock.Unlock()
}
/*var reloadForumMutex sync.Mutex
// TODO: Avoid firing this multiple times per sec tick
// TODO: Shard the forum topic list map
func (tList *DefaultTopicList) ReloadForum(id int) error {
2022-02-21 03:32:53 +00:00
reloadForumMutex.Lock()
defer reloadForumMutex.Unlock()
forum, err := Forums.Get(id)
if err != nil {
return err
}
ofList := make(map[int]*ForumTopicListHolder)
fList := make(map[int]*ForumTopicListHolder)
tList.forumLock.Lock()
ofList = tList.forums
for id, f := range ofList {
fList[id] = f
}
tList.forumLock.Unlock()
topicList, pagi := []*TopicsRow{}, tList.defaultPagi()
if forum.TopicCount != 0 {
topicList, pagi, err = tList.getListByForum(forum, 1, 0)
if err != nil {
return err
}
}
fList[forum.ID] = &ForumTopicListHolder{topicList, pagi}
tList.setForumList(fList)
return nil
}*/
// TODO: Add Topics() method to *Forum?
// TODO: Implement orderby
func (tList *DefaultTopicList) GetListByForum(f *Forum, page, orderby int) (topicList []*TopicsRow, pagi Paginator, err error) {
2022-02-21 03:32:53 +00:00
if page == 0 {
page = 1
}
if f.TopicCount == 0 {
return topicList, tList.defaultPagi(), nil
}
if page == 1 && orderby == 0 {
var h *ForumTopicListHolder
var ok bool
tList.forumLock.RLock()
h, ok = tList.forums[f.ID]
tList.forumLock.RUnlock()
if ok {
return h.List, h.Paginator, nil
}
}
return tList.RawGetListByForum(f, page, orderby)
}
func (tList *DefaultTopicList) RawGetListByForum(f *Forum, page, orderby int) (topicList []*TopicsRow, pagi Paginator, err error) {
2022-02-21 03:32:53 +00:00
// 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)
rows, err := tList.getTopicsByForum.Query(f.ID, offset, Config.ItemsPerPage)
if err != nil {
return nil, tList.defaultPagi(), err
}
defer rows.Close()
// TODO: Use something other than TopicsRow as we don't need to store the forum name and link on each and every topic item?
reqUserList := make(map[int]bool)
for rows.Next() {
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)
if err != nil {
return nil, tList.defaultPagi(), err
}
t.Link = BuildTopicURL(NameToSlug(t.Title), t.ID)
// TODO: Create a specialised function with a bit less overhead for getting the last page for a post count
_, _, lastPage := PageOffset(t.PostCount, 1, Config.ItemsPerPage)
t.LastPage = lastPage
//header.Hooks.VhookNoRet("forum_trow_assign", &t, &forum)
topicList = append(topicList, &t)
reqUserList[t.CreatedBy] = true
reqUserList[t.LastReplyBy] = true
}
if err = rows.Err(); err != nil {
return nil, tList.defaultPagi(), err
}
// 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++
}
// TODO: What if a user is deleted via the Control Panel?
userList, err := Users.BulkGetMap(idSlice)
if err != nil {
return nil, tList.defaultPagi(), err
}
// Second pass to the add the user data
// TODO: Use a pointer to TopicsRow instead of TopicsRow itself?
for _, t := range topicList {
t.Creator = userList[t.CreatedBy]
t.LastUser = userList[t.LastReplyBy]
}
if len(topicList) == 0 {
return topicList, tList.defaultPagi(), nil
}
pageList := Paginate(page, lastPage, 5)
return topicList, Paginator{pageList, page, lastPage}, nil
}
func (tList *DefaultTopicList) GetListByGroup(g *Group, page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error) {
2022-02-21 03:32:53 +00:00
if page == 0 {
page = 1
}
// TODO: Cache the first three pages not just the first along with all the topics on this beaten track
// TODO: Move this into CanSee to reduce redundancy
if (page == 1 || page == 2) && orderby == 0 && len(filterIDs) == 0 {
var h [2]*TopicListHolder
var ok bool
if g.ID%2 == 0 {
tList.evenLock.RLock()
h, ok = tList.evenGroups[g.ID]
tList.evenLock.RUnlock()
} else {
tList.oddLock.RLock()
h, ok = tList.oddGroups[g.ID]
tList.oddLock.RUnlock()
}
if ok {
return h[page-1].List, h[page-1].ForumList, h[page-1].Paginator, nil
}
}
// TODO: Make CanSee a method on *Group with a canSee field? Have a CanSee method on *User to cover the case of superadmins?
//log.Printf("deoptimising for %d on page %d\n", g.ID, page)
return tList.GetListByCanSee(g.CanSee, page, orderby, filterIDs)
Almost finished live topic lists, you can find them at /topics/. You can disable them via config.json The topic list cache can handle more groups now, but don't go too crazy with groups (e.g. thousands of them). Make the suspicious request logs more descriptive. Added the phrases API endpoint. Split the template phrases up by prefix, more work on this coming up. Removed #dash_saved and part of #dash_username. Removed some temporary artifacts from trying to implement FA5 in Nox. Removed some commented CSS. Fixed template artifact deletion on Windows. Tweaked HTTPSRedirect to make it more compact. Fixed NullUserCache not complying with the expectations for BulkGet. Swapped out a few RunVhook calls for more appropriate RunVhookNoreturn calls. Removed a few redundant IsAdmin checks when IsMod would suffice. Commented out a few pushers. Desktop notification permission requests are no longer served to guests. Split topics.html into topics.html and topics_topic.html RunThemeTemplate should now fallback to interpreted templates properly when the transpiled variants aren't avaialb.e Changed TopicsRow.CreatedAt from a string to a time.Time Added SkipTmplPtrMap to CTemplateConfig. Added SetBuildTags to CTemplateSet. A bit more data is dumped when something goes wrong while transpiling templates now. topics_topic, topic_posts, and topic_alt_posts are now transpiled for the client, although not all of them are ready to be served to the client yet. Client rendered templates now support phrases. Client rendered templates now support loops. Fixed loadAlerts in global.js Refactored some of the template initialisation code to make it less repetitive. Split topic.html into topic.html and topic_posts.html Split topic_alt.html into topic_alt.html and topic_alt_posts.html Added comments for PollCache. Fixed a data race in the MemoryPollCache. The writer is now closed properly in WsHubImpl.broadcastMessage. Fixed a potential deadlock in WsHubImpl.broadcastMessage. Removed some old commented code in websockets.go Added the DisableLiveTopicList config setting.
2018-06-24 13:49:29 +00:00
}
func (tList *DefaultTopicList) GetListByCanSee(canSee []int, page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error) {
2022-02-21 03:32:53 +00:00
// TODO: Optimise this by filtering canSee and then fetching the forums?
// We need a list of the visible forums for Quick Topic
// ? - Would it be useful, if we could post in social groups from /topics/?
for _, fid := range canSee {
f := Forums.DirtyGet(fid)
if f.Name != "" && f.Active && (f.ParentType == "" || f.ParentType == "forum") /*&& f.TopicCount != 0*/ {
fcopy := f.Copy()
// TODO: Add a hook here for plugin_guilds !!
forumList = append(forumList, fcopy)
}
}
inSlice := func(haystack []int, needle int) bool {
for _, it := range haystack {
if needle == it {
return true
}
}
return false
}
var filteredForums []Forum
if len(filterIDs) > 0 {
for _, f := range forumList {
if inSlice(filterIDs, f.ID) {
filteredForums = append(filteredForums, f)
}
}
} else {
filteredForums = forumList
}
if len(filteredForums) == 1 && orderby == 0 {
topicList, pagi, err = tList.GetListByForum(&filteredForums[0], page, orderby)
return topicList, forumList, pagi, err
}
var topicCount int
for _, f := range filteredForums {
topicCount += f.TopicCount
}
// ? - Should we be showing plugin_guilds posts on /topics/?
argList, qlist := ForumListToArgQ(filteredForums)
if qlist == "" {
// We don't want to kill the page, so pass an empty slice and nil error
return topicList, filteredForums, tList.defaultPagi(), nil
}
topicList, pagi, err = tList.getList(page, orderby, topicCount, argList, qlist)
return topicList, filteredForums, pagi, err
}
// TODO: Reduce the number of returns
func (tList *DefaultTopicList) GetList(page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error) {
2022-02-21 03:32:53 +00:00
// 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()
if err != nil {
return nil, nil, tList.defaultPagi(), err
}
//log.Printf("cCanSee: %+v\n", cCanSee)
inSlice := func(haystack []int, needle int) bool {
for _, it := range haystack {
if needle == it {
return true
}
}
return false
}
var canSee []int
if len(filterIDs) > 0 {
for _, fid := range cCanSee {
if inSlice(filterIDs, fid) {
canSee = append(canSee, fid)
}
}
} else {
canSee = cCanSee
}
//log.Printf("canSee: %+v\n", canSee)
// We need a list of the visible forums for Quick Topic
// ? - Would it be useful, if we could post in social groups from /topics/?
var topicCount int
for _, fid := range canSee {
f := Forums.DirtyGet(fid)
if f.Name != "" && f.Active && (f.ParentType == "" || f.ParentType == "forum") /*&& f.TopicCount != 0*/ {
fcopy := f.Copy()
// TODO: Add a hook here for plugin_guilds
forumList = append(forumList, fcopy)
topicCount += fcopy.TopicCount
}
}
if len(forumList) == 1 && orderby == 0 {
topicList, pagi, err = tList.GetListByForum(&forumList[0], page, orderby)
return topicList, forumList, pagi, err
}
// ? - Should we be showing plugin_guilds posts on /topics/?
argList, qlist := ForumListToArgQ(forumList)
if qlist == "" {
// If the super admin can't see anything, then things have gone terribly wrong
return topicList, forumList, tList.defaultPagi(), err
}
topicList, pagi, err = tList.getList(page, orderby, topicCount, argList, qlist)
return topicList, forumList, pagi, err
}
// TODO: Rename this to TopicListStore and pass back a TopicList instance holding the pagination data and topic list rather than passing them back one argument at a time
// 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) {
2022-02-21 03:32:53 +00:00
if topicCount == 0 {
return nil, tList.defaultPagi(), err
}
//log.Printf("argList: %+v\n",argList)
//log.Printf("qlist: %+v\n",qlist)
var cols, orderq string
var stmt *sql.Stmt
switch orderby {
case TopicListWeekViews:
tList.qLock.RLock()
stmt = tList.qcounts[len(argList)-2]
tList.qLock.RUnlock()
if stmt == nil {
orderq = "weekViews DESC,lastReplyAt DESC,createdBy DESC"
now := time.Now()
_, week := now.ISOWeek()
day := int(now.Weekday()) + 1
if week%2 == 0 { // is even?
cols = "tid,title,content,createdBy,is_closed,sticky,createdAt,lastReplyAt,lastReplyBy,lastReplyID,parentID,views,postCount,likeCount,attachCount,poll,data,FLOOR(weekEvenViews+((weekOddViews/7)*" + strconv.Itoa(day) + ")) AS weekViews"
} else {
cols = "tid,title,content,createdBy,is_closed,sticky,createdAt,lastReplyAt,lastReplyBy,lastReplyID,parentID,views,postCount,likeCount,attachCount,poll,data,FLOOR(weekOddViews+((weekEvenViews/7)*" + strconv.Itoa(day) + ")) AS weekViews"
}
topicCount, err = ArgQToWeekViewTopicCount(argList, qlist)
if err != nil {
return nil, tList.defaultPagi(), err
}
acc := qgen.NewAcc()
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 {
return nil, tList.defaultPagi(), e
}
defer stmt.Close()
}
case TopicListMostViewed:
tList.qLock.RLock()
stmt = tList.qcounts[len(argList)-2]
tList.qLock.RUnlock()
if stmt == nil {
orderq = "views DESC,lastReplyAt DESC,createdBy DESC"
cols = "tid,title,content,createdBy,is_closed,sticky,createdAt,lastReplyAt,lastReplyBy,lastReplyID,parentID,views,postCount,likeCount,attachCount,poll,data,weekEvenViews"
}
default:
tList.qLock2.RLock()
stmt = tList.qcounts2[len(argList)-2]
tList.qLock2.RUnlock()
if stmt == nil {
orderq = "sticky DESC,lastReplyAt DESC,createdBy DESC"
cols = "tid,title,content,createdBy,is_closed,sticky,createdAt,lastReplyAt,lastReplyBy,lastReplyID,parentID,views,postCount,likeCount,attachCount,poll,data,weekEvenViews"
}
}
offset, page, lastPage := PageOffset(topicCount, page, Config.ItemsPerPage)
// TODO: Prepare common qlist lengths to speed this up in common cases, prepared statements are prepared lazily anyway, so it probably doesn't matter if we do ten or so
if stmt == nil {
stmt, err = qgen.Builder.SimpleSelect("topics", cols, "parentID IN("+qlist+")", orderq, "?,?")
if err != nil {
return nil, tList.defaultPagi(), err
}
defer stmt.Close()
}
argList = append(argList, offset)
argList = append(argList, Config.ItemsPerPage)
rows, err := stmt.Query(argList...)
if err != nil {
return nil, tList.defaultPagi(), err
}
defer rows.Close()
rc, tc := Rstore.GetCache(), Topics.GetCache()
rcap := rc.GetCapacity()
rlen := rc.Length()
reqUserList := make(map[int]bool)
for rows.Next() {
// TODO: Embed Topic structs in TopicsRow to make it easier for us to reuse this work in the topic cache
t := TopicsRow{}
//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)
if err != nil {
return nil, tList.defaultPagi(), err
}
//t.WeekViews = int(weekViews[0])
//log.Printf("t: %+v\n", t)
//log.Printf("weekViews: %+v\n", weekViews)
t.Link = BuildTopicURL(NameToSlug(t.Title), t.ID)
// TODO: Pass forum to something like topicItem.Forum and use that instead of these two properties? Could be more flexible.
forum := Forums.DirtyGet(t.ParentID)
t.ForumName = forum.Name
t.ForumLink = forum.Link
// TODO: Create a specialised function with a bit less overhead for getting the last page for a post count
_, _, lastPage := PageOffset(t.PostCount, 1, Config.ItemsPerPage)
t.LastPage = lastPage
// TODO: Rename this Vhook to better reflect moving the topic list from /routes/ to /common/
GetHookTable().Vhook("topics_topic_row_assign", &t, &forum)
topicList = append(topicList, &t)
reqUserList[t.CreatedBy] = true
reqUserList[t.LastReplyBy] = true
//log.Print("rlen: ", rlen)
//log.Print("rcap: ", rcap)
//log.Print("t.PostCount: ", t.PostCount)
//log.Print("t.PostCount == 2 && rlen < rcap: ", t.PostCount == 2 && rlen < rcap)
// Avoid the extra queries on topic list pages, if we already have what we want...
hRids := false
if tc != nil {
if t, e := tc.Get(t.ID); e == nil {
hRids = len(t.Rids) != 0
}
}
if t.PostCount == 2 && rlen < rcap && !hRids && page < 5 {
rids, err := GetRidsForTopic(t.ID, 0)
if err != nil {
return nil, tList.defaultPagi(), err
}
//log.Print("rids: ", rids)
if len(rids) == 0 {
continue
}
_, _ = Rstore.Get(rids[0])
rlen++
t.Rids = []int{rids[0]}
}
if tc != nil {
if _, e := tc.Get(t.ID); e == sql.ErrNoRows {
//_ = tc.Set(t.Topic())
_ = tc.Set(&t.Topic)
}
}
}
if err = rows.Err(); err != nil {
return nil, tList.defaultPagi(), err
}
// TODO: specialcase for when reqUserList only has one or two items to avoid map alloc
if len(reqUserList) == 1 {
var u *User
for uid, _ := range reqUserList {
u, err = Users.Get(uid)
if err != nil {
return nil, tList.defaultPagi(), err
}
}
for _, t := range topicList {
t.Creator = u
t.LastUser = u
}
} else 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++
}
// TODO: What if a user is deleted via the Control Panel?
userList, err := Users.BulkGetMap(idSlice)
if err != nil {
return nil, tList.defaultPagi(), err
}
// Second pass to the add the user data
// TODO: Use a pointer to TopicsRow instead of TopicsRow itself?
for _, t := range topicList {
t.Creator = userList[t.CreatedBy]
t.LastUser = userList[t.LastReplyBy]
}
}
pageList := Paginate(page, lastPage, 5)
return topicList, Paginator{pageList, page, lastPage}, nil
}
// Internal. Don't rely on it.
func ForumListToArgQ(forums []Forum) (argList []interface{}, qlist string) {
2022-02-21 03:32:53 +00:00
for _, forum := range forums {
argList = append(argList, strconv.Itoa(forum.ID))
qlist += "?,"
}
if qlist != "" {
qlist = qlist[0 : len(qlist)-1]
}
return argList, qlist
}
// Internal. Don't rely on it.
// TODO: Check the TopicCount field on the forums instead? Make sure it's in sync first.
func ArgQToTopicCount(argList []interface{}, qlist string) (topicCount int, err error) {
2022-02-21 03:32:53 +00:00
topicCountStmt, err := qgen.Builder.SimpleCount("topics", "parentID IN("+qlist+")", "")
if err != nil {
return 0, err
}
defer topicCountStmt.Close()
err = topicCountStmt.QueryRow(argList...).Scan(&topicCount)
if err != nil && err != ErrNoRows {
return 0, err
}
return topicCount, err
}
// Internal. Don't rely on it.
func ArgQToWeekViewTopicCount(argList []interface{}, qlist string) (topicCount int, err error) {
2022-02-21 03:32:53 +00:00
topicCountStmt, err := qgen.Builder.SimpleCount("topics", "parentID IN("+qlist+") AND (weekEvenViews!=0 OR weekOddViews!=0)", "")
if err != nil {
return 0, err
}
defer topicCountStmt.Close()
err = topicCountStmt.QueryRow(argList...).Scan(&topicCount)
if err != nil && err != ErrNoRows {
return 0, err
}
return topicCount, err
}
func TopicCountInForums(forums []Forum) (topicCount int, err error) {
2022-02-21 03:32:53 +00:00
for _, f := range forums {
topicCount += f.TopicCount
}
return topicCount, nil
}