github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/common/topic_list.go (about)

     1  package common
     2  
     3  import (
     4  	"database/sql"
     5  	"fmt"
     6  	"strconv"
     7  	"sync"
     8  	"time"
     9  
    10  	qgen "github.com/Azareal/Gosora/query_gen"
    11  )
    12  
    13  var TopicList TopicListInt
    14  
    15  const (
    16  	TopicListDefault = iota
    17  	TopicListMostViewed
    18  	TopicListWeekViews
    19  )
    20  
    21  type TopicListHolder struct {
    22  	List      []*TopicsRow
    23  	ForumList []Forum
    24  	Paginator Paginator
    25  }
    26  
    27  type ForumTopicListHolder struct {
    28  	List      []*TopicsRow
    29  	Paginator Paginator
    30  }
    31  
    32  // TODO: Should we return no rows errors on empty pages? Is this likely to break something?
    33  type TopicListInt interface {
    34  	GetListByCanSee(canSee []int, page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error)
    35  	GetListByGroup(g *Group, page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error)
    36  	GetListByForum(f *Forum, page, orderby int) (topicList []*TopicsRow, pagi Paginator, err error)
    37  	GetList(page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error)
    38  }
    39  
    40  type TopicListIntTest interface {
    41  	RawGetListByForum(f *Forum, page, orderby int) (topicList []*TopicsRow, pagi Paginator, err error)
    42  	Tick() error
    43  }
    44  
    45  type DefaultTopicList struct {
    46  	// TODO: Rewrite this to put permTree as the primary and put canSeeStr on each group?
    47  	oddGroups  map[int][2]*TopicListHolder
    48  	evenGroups map[int][2]*TopicListHolder
    49  	oddLock    sync.RWMutex
    50  	evenLock   sync.RWMutex
    51  
    52  	forums    map[int]*ForumTopicListHolder
    53  	forumLock sync.RWMutex
    54  
    55  	qcounts  map[int]*sql.Stmt
    56  	qcounts2 map[int]*sql.Stmt
    57  	qLock    sync.RWMutex
    58  	qLock2   sync.RWMutex
    59  
    60  	//permTree atomic.Value // [string(canSee)]canSee
    61  	//permTree map[string][]int // [string(canSee)]canSee
    62  
    63  	getTopicsByForum *sql.Stmt
    64  	//getTidsByForum *sql.Stmt
    65  }
    66  
    67  // 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.
    68  // 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
    69  // 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.
    70  func NewDefaultTopicList(acc *qgen.Accumulator) (*DefaultTopicList, error) {
    71  	tList := &DefaultTopicList{
    72  		oddGroups:        make(map[int][2]*TopicListHolder),
    73  		evenGroups:       make(map[int][2]*TopicListHolder),
    74  		forums:           make(map[int]*ForumTopicListHolder),
    75  		qcounts:          make(map[int]*sql.Stmt),
    76  		qcounts2:         make(map[int]*sql.Stmt),
    77  		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(),
    78  		//getTidsByForum: acc.Select("topics").Columns("tid").Where("parentID=?").Orderby("sticky DESC,lastReplyAt DESC,createdBy DESC").Limit("?,?").Prepare(),
    79  	}
    80  	if e := acc.FirstError(); e != nil {
    81  		return nil, e
    82  	}
    83  	if e := tList.Tick(); e != nil {
    84  		return nil, e
    85  	}
    86  
    87  	Tasks.HalfSec.Add(tList.Tick)
    88  	//Tasks.Sec.Add(tList.GroupCountTick) // TODO: Dynamically change the groups in the short list to be optimised every second
    89  	return tList, nil
    90  }
    91  
    92  func (tList *DefaultTopicList) Tick() error {
    93  	//fmt.Println("TopicList.Tick")
    94  	if !TopicListThaw.Thawed() {
    95  		return nil
    96  	}
    97  	//fmt.Println("building topic list")
    98  
    99  	oddLists := make(map[int][2]*TopicListHolder)
   100  	evenLists := make(map[int][2]*TopicListHolder)
   101  	addList := func(gid int, h [2]*TopicListHolder) {
   102  		if gid%2 == 0 {
   103  			evenLists[gid] = h
   104  		} else {
   105  			oddLists[gid] = h
   106  		}
   107  	}
   108  
   109  	allGroups, err := Groups.GetAll()
   110  	if err != nil {
   111  		return err
   112  	}
   113  
   114  	gidToCanSee := make(map[int]string)
   115  	permTree := make(map[string][]int) // [string(canSee)]canSee
   116  	for _, g := range allGroups {
   117  		// ? - Move the user count check to instance initialisation? Might require more book-keeping, particularly when a user moves into a zero user group
   118  		if g.UserCount == 0 && g.ID != GuestUser.Group {
   119  			continue
   120  		}
   121  
   122  		canSee := make([]byte, len(g.CanSee))
   123  		for i, item := range g.CanSee {
   124  			canSee[i] = byte(item)
   125  		}
   126  
   127  		canSeeInt := make([]int, len(canSee))
   128  		copy(canSeeInt, g.CanSee)
   129  		sCanSee := string(canSee)
   130  		permTree[sCanSee] = canSeeInt
   131  		gidToCanSee[g.ID] = sCanSee
   132  	}
   133  
   134  	canSeeHolders := make(map[string][2]*TopicListHolder)
   135  	forumCounts := make(map[int]int)
   136  	for name, canSee := range permTree {
   137  		topicList, forumList, pagi, err := tList.GetListByCanSee(canSee, 1, 0, nil)
   138  		if err != nil {
   139  			return err
   140  		}
   141  		topicList2, forumList2, pagi2, err := tList.GetListByCanSee(canSee, 2, 0, nil)
   142  		if err != nil {
   143  			return err
   144  		}
   145  		canSeeHolders[name] = [2]*TopicListHolder{
   146  			{topicList, forumList, pagi},
   147  			{topicList2, forumList2, pagi2},
   148  		}
   149  		if len(canSee) > 1 {
   150  			forumCounts[len(canSee)] += 1
   151  		}
   152  	}
   153  	for gid, canSee := range gidToCanSee {
   154  		addList(gid, canSeeHolders[canSee])
   155  	}
   156  
   157  	tList.oddLock.Lock()
   158  	tList.oddGroups = oddLists
   159  	tList.oddLock.Unlock()
   160  
   161  	tList.evenLock.Lock()
   162  	tList.evenGroups = evenLists
   163  	tList.evenLock.Unlock()
   164  
   165  	topc := []int{0, 0, 0, 0, 0, 0}
   166  	addC := func(c int) {
   167  		lowI, low := 0, topc[0]
   168  		for i, top := range topc {
   169  			if top < low {
   170  				lowI = i
   171  				low = top
   172  			}
   173  		}
   174  		if c > low {
   175  			topc[lowI] = c
   176  		}
   177  	}
   178  	for forumCount := range forumCounts {
   179  		addC(forumCount)
   180  	}
   181  
   182  	qcounts := make(map[int]*sql.Stmt)
   183  	qcounts2 := make(map[int]*sql.Stmt)
   184  	for _, top := range topc {
   185  		if top == 0 {
   186  			continue
   187  		}
   188  
   189  		qlist := inqbuild2(top - 1)
   190  		cols := "tid,title,content,createdBy,is_closed,sticky,createdAt,lastReplyAt,lastReplyBy,lastReplyID,parentID,views,postCount,likeCount,attachCount,poll,data"
   191  
   192  		stmt, err := qgen.Builder.SimpleSelect("topics", cols, "parentID IN("+qlist+")", "views DESC,lastReplyAt DESC,createdBy DESC", "?,?")
   193  		if err != nil {
   194  			return err
   195  		}
   196  		qcounts[top] = stmt
   197  
   198  		stmt, err = qgen.Builder.SimpleSelect("topics", cols, "parentID IN("+qlist+")", "sticky DESC,lastReplyAt DESC,createdBy DESC", "?,?")
   199  		if err != nil {
   200  			return err
   201  		}
   202  		qcounts2[top] = stmt
   203  	}
   204  
   205  	tList.qLock.Lock()
   206  	tList.qcounts = qcounts
   207  	tList.qLock.Unlock()
   208  
   209  	tList.qLock2.Lock()
   210  	tList.qcounts2 = qcounts2
   211  	tList.qLock2.Unlock()
   212  
   213  	fmt.Printf("Forums: %+v\n", Forums)
   214  	forums, err := Forums.GetAll()
   215  	if err != nil {
   216  		return err
   217  	}
   218  
   219  	top8 := []*Forum{nil, nil, nil, nil, nil, nil, nil, nil}
   220  	z := true
   221  	addScore2 := func(f *Forum) {
   222  		for i, top := range top8 {
   223  			if top.TopicCount < f.TopicCount {
   224  				top8[i] = f
   225  				return
   226  			}
   227  		}
   228  	}
   229  	addScore := func(f *Forum) {
   230  		if z {
   231  			for i, top := range top8 {
   232  				if top == nil {
   233  					top8[i] = f
   234  					return
   235  				}
   236  			}
   237  			z = false
   238  			addScore2(f)
   239  		}
   240  		addScore2(f)
   241  	}
   242  
   243  	var fshort []*Forum
   244  	for _, f := range forums {
   245  		if f.Name == "" || !f.Active || (f.ParentType != "" && f.ParentType != "forum") {
   246  			continue
   247  		}
   248  		if f.TopicCount == 0 {
   249  			fshort = append(fshort, f)
   250  			continue
   251  		}
   252  		addScore(f)
   253  	}
   254  	for _, f := range top8 {
   255  		if f != nil {
   256  			fshort = append(fshort, f)
   257  		}
   258  	}
   259  
   260  	// TODO: Avoid rebuilding the entire list on every tick
   261  	fList := make(map[int]*ForumTopicListHolder)
   262  	for _, f := range fshort {
   263  		topicList, pagi := []*TopicsRow{}, tList.defaultPagi()
   264  		if f.TopicCount != 0 {
   265  			topicList, pagi, err = tList.RawGetListByForum(f, 1, 0)
   266  			if err != nil {
   267  				return err
   268  			}
   269  		}
   270  		fList[f.ID] = &ForumTopicListHolder{topicList, pagi}
   271  
   272  		/*topicList, pagi, err := tList.GetListByForum(f, 1, 0)
   273  		if err != nil {
   274  			return err
   275  		}
   276  		fList[f.ID] = &ForumTopicListHolder{topicList, pagi}*/
   277  	}
   278  
   279  	//fmt.Printf("fList: %+v\n", fList)
   280  	tList.setForumList(fList)
   281  
   282  	hTbl := GetHookTable()
   283  	_, _ = hTbl.VhookSkippable("tasks_tick_topic_list", tList)
   284  
   285  	return nil
   286  }
   287  
   288  func (tList *DefaultTopicList) defaultPagi() Paginator {
   289  	/*_, page, lastPage := PageOffset(f.TopicCount, page, Config.ItemsPerPage)
   290  	pageList := Paginate(page, lastPage, 5)
   291  	return topicList, Paginator{pageList, page, lastPage}, nil*/
   292  	return Paginator{[]int{}, 1, 1}
   293  }
   294  
   295  func (tList *DefaultTopicList) setForumList(forums map[int]*ForumTopicListHolder) {
   296  	tList.forumLock.Lock()
   297  	tList.forums = forums
   298  	tList.forumLock.Unlock()
   299  }
   300  
   301  /*var reloadForumMutex sync.Mutex
   302  
   303  // TODO: Avoid firing this multiple times per sec tick
   304  // TODO: Shard the forum topic list map
   305  func (tList *DefaultTopicList) ReloadForum(id int) error {
   306  	reloadForumMutex.Lock()
   307  	defer reloadForumMutex.Unlock()
   308  
   309  	forum, err := Forums.Get(id)
   310  	if err != nil {
   311  		return err
   312  	}
   313  
   314  	ofList := make(map[int]*ForumTopicListHolder)
   315  	fList := make(map[int]*ForumTopicListHolder)
   316  	tList.forumLock.Lock()
   317  	ofList = tList.forums
   318  	for id, f := range ofList {
   319  		fList[id] = f
   320  	}
   321  	tList.forumLock.Unlock()
   322  
   323  	topicList, pagi := []*TopicsRow{}, tList.defaultPagi()
   324  	if forum.TopicCount != 0 {
   325  		topicList, pagi, err = tList.getListByForum(forum, 1, 0)
   326  		if err != nil {
   327  			return err
   328  		}
   329  	}
   330  	fList[forum.ID] = &ForumTopicListHolder{topicList, pagi}
   331  
   332  	tList.setForumList(fList)
   333  	return nil
   334  }*/
   335  
   336  // TODO: Add Topics() method to *Forum?
   337  // TODO: Implement orderby
   338  func (tList *DefaultTopicList) GetListByForum(f *Forum, page, orderby int) (topicList []*TopicsRow, pagi Paginator, err error) {
   339  	if page == 0 {
   340  		page = 1
   341  	}
   342  	if f.TopicCount == 0 {
   343  		return topicList, tList.defaultPagi(), nil
   344  	}
   345  	if page == 1 && orderby == 0 {
   346  		var h *ForumTopicListHolder
   347  		var ok bool
   348  		tList.forumLock.RLock()
   349  		h, ok = tList.forums[f.ID]
   350  		tList.forumLock.RUnlock()
   351  		if ok {
   352  			return h.List, h.Paginator, nil
   353  		}
   354  	}
   355  	return tList.RawGetListByForum(f, page, orderby)
   356  }
   357  
   358  func (tList *DefaultTopicList) RawGetListByForum(f *Forum, page, orderby int) (topicList []*TopicsRow, pagi Paginator, err error) {
   359  	// TODO: Does forum.TopicCount take the deleted items into consideration for guests? We don't have soft-delete yet, only hard-delete
   360  	offset, page, lastPage := PageOffset(f.TopicCount, page, Config.ItemsPerPage)
   361  
   362  	rows, err := tList.getTopicsByForum.Query(f.ID, offset, Config.ItemsPerPage)
   363  	if err != nil {
   364  		return nil, tList.defaultPagi(), err
   365  	}
   366  	defer rows.Close()
   367  
   368  	// TODO: Use something other than TopicsRow as we don't need to store the forum name and link on each and every topic item?
   369  	reqUserList := make(map[int]bool)
   370  	for rows.Next() {
   371  		t := TopicsRow{Topic: Topic{ParentID: f.ID}}
   372  		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)
   373  		if err != nil {
   374  			return nil, tList.defaultPagi(), err
   375  		}
   376  
   377  		t.Link = BuildTopicURL(NameToSlug(t.Title), t.ID)
   378  		// TODO: Create a specialised function with a bit less overhead for getting the last page for a post count
   379  		_, _, lastPage := PageOffset(t.PostCount, 1, Config.ItemsPerPage)
   380  		t.LastPage = lastPage
   381  
   382  		//header.Hooks.VhookNoRet("forum_trow_assign", &t, &forum)
   383  		topicList = append(topicList, &t)
   384  		reqUserList[t.CreatedBy] = true
   385  		reqUserList[t.LastReplyBy] = true
   386  	}
   387  	if err = rows.Err(); err != nil {
   388  		return nil, tList.defaultPagi(), err
   389  	}
   390  
   391  	// Convert the user ID map to a slice, then bulk load the users
   392  	idSlice := make([]int, len(reqUserList))
   393  	var i int
   394  	for userID := range reqUserList {
   395  		idSlice[i] = userID
   396  		i++
   397  	}
   398  
   399  	// TODO: What if a user is deleted via the Control Panel?
   400  	userList, err := Users.BulkGetMap(idSlice)
   401  	if err != nil {
   402  		return nil, tList.defaultPagi(), err
   403  	}
   404  
   405  	// Second pass to the add the user data
   406  	// TODO: Use a pointer to TopicsRow instead of TopicsRow itself?
   407  	for _, t := range topicList {
   408  		t.Creator = userList[t.CreatedBy]
   409  		t.LastUser = userList[t.LastReplyBy]
   410  	}
   411  
   412  	if len(topicList) == 0 {
   413  		return topicList, tList.defaultPagi(), nil
   414  	}
   415  	pageList := Paginate(page, lastPage, 5)
   416  	return topicList, Paginator{pageList, page, lastPage}, nil
   417  }
   418  
   419  func (tList *DefaultTopicList) GetListByGroup(g *Group, page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error) {
   420  	if page == 0 {
   421  		page = 1
   422  	}
   423  	// TODO: Cache the first three pages not just the first along with all the topics on this beaten track
   424  	// TODO: Move this into CanSee to reduce redundancy
   425  	if (page == 1 || page == 2) && orderby == 0 && len(filterIDs) == 0 {
   426  		var h [2]*TopicListHolder
   427  		var ok bool
   428  		if g.ID%2 == 0 {
   429  			tList.evenLock.RLock()
   430  			h, ok = tList.evenGroups[g.ID]
   431  			tList.evenLock.RUnlock()
   432  		} else {
   433  			tList.oddLock.RLock()
   434  			h, ok = tList.oddGroups[g.ID]
   435  			tList.oddLock.RUnlock()
   436  		}
   437  		if ok {
   438  			return h[page-1].List, h[page-1].ForumList, h[page-1].Paginator, nil
   439  		}
   440  	}
   441  
   442  	// TODO: Make CanSee a method on *Group with a canSee field? Have a CanSee method on *User to cover the case of superadmins?
   443  	//log.Printf("deoptimising for %d on page %d\n", g.ID, page)
   444  	return tList.GetListByCanSee(g.CanSee, page, orderby, filterIDs)
   445  }
   446  
   447  func (tList *DefaultTopicList) GetListByCanSee(canSee []int, page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error) {
   448  	// TODO: Optimise this by filtering canSee and then fetching the forums?
   449  	// We need a list of the visible forums for Quick Topic
   450  	// ? - Would it be useful, if we could post in social groups from /topics/?
   451  	for _, fid := range canSee {
   452  		f := Forums.DirtyGet(fid)
   453  		if f.Name != "" && f.Active && (f.ParentType == "" || f.ParentType == "forum") /*&& f.TopicCount != 0*/ {
   454  			fcopy := f.Copy()
   455  			// TODO: Add a hook here for plugin_guilds !!
   456  			forumList = append(forumList, fcopy)
   457  		}
   458  	}
   459  
   460  	inSlice := func(haystack []int, needle int) bool {
   461  		for _, it := range haystack {
   462  			if needle == it {
   463  				return true
   464  			}
   465  		}
   466  		return false
   467  	}
   468  
   469  	var filteredForums []Forum
   470  	if len(filterIDs) > 0 {
   471  		for _, f := range forumList {
   472  			if inSlice(filterIDs, f.ID) {
   473  				filteredForums = append(filteredForums, f)
   474  			}
   475  		}
   476  	} else {
   477  		filteredForums = forumList
   478  	}
   479  	if len(filteredForums) == 1 && orderby == 0 {
   480  		topicList, pagi, err = tList.GetListByForum(&filteredForums[0], page, orderby)
   481  		return topicList, forumList, pagi, err
   482  	}
   483  
   484  	var topicCount int
   485  	for _, f := range filteredForums {
   486  		topicCount += f.TopicCount
   487  	}
   488  
   489  	// ? - Should we be showing plugin_guilds posts on /topics/?
   490  	argList, qlist := ForumListToArgQ(filteredForums)
   491  	if qlist == "" {
   492  		// We don't want to kill the page, so pass an empty slice and nil error
   493  		return topicList, filteredForums, tList.defaultPagi(), nil
   494  	}
   495  
   496  	topicList, pagi, err = tList.getList(page, orderby, topicCount, argList, qlist)
   497  	return topicList, filteredForums, pagi, err
   498  }
   499  
   500  // TODO: Reduce the number of returns
   501  func (tList *DefaultTopicList) GetList(page, orderby int, filterIDs []int) (topicList []*TopicsRow, forumList []Forum, pagi Paginator, err error) {
   502  	// TODO: Make CanSee a method on *Group with a canSee field? Have a CanSee method on *User to cover the case of superadmins?
   503  	cCanSee, err := Forums.GetAllVisibleIDs()
   504  	if err != nil {
   505  		return nil, nil, tList.defaultPagi(), err
   506  	}
   507  	//log.Printf("cCanSee: %+v\n", cCanSee)
   508  	inSlice := func(haystack []int, needle int) bool {
   509  		for _, it := range haystack {
   510  			if needle == it {
   511  				return true
   512  			}
   513  		}
   514  		return false
   515  	}
   516  
   517  	var canSee []int
   518  	if len(filterIDs) > 0 {
   519  		for _, fid := range cCanSee {
   520  			if inSlice(filterIDs, fid) {
   521  				canSee = append(canSee, fid)
   522  			}
   523  		}
   524  	} else {
   525  		canSee = cCanSee
   526  	}
   527  	//log.Printf("canSee: %+v\n", canSee)
   528  
   529  	// We need a list of the visible forums for Quick Topic
   530  	// ? - Would it be useful, if we could post in social groups from /topics/?
   531  	var topicCount int
   532  	for _, fid := range canSee {
   533  		f := Forums.DirtyGet(fid)
   534  		if f.Name != "" && f.Active && (f.ParentType == "" || f.ParentType == "forum") /*&& f.TopicCount != 0*/ {
   535  			fcopy := f.Copy()
   536  			// TODO: Add a hook here for plugin_guilds
   537  			forumList = append(forumList, fcopy)
   538  			topicCount += fcopy.TopicCount
   539  		}
   540  	}
   541  	if len(forumList) == 1 && orderby == 0 {
   542  		topicList, pagi, err = tList.GetListByForum(&forumList[0], page, orderby)
   543  		return topicList, forumList, pagi, err
   544  	}
   545  
   546  	// ? - Should we be showing plugin_guilds posts on /topics/?
   547  	argList, qlist := ForumListToArgQ(forumList)
   548  	if qlist == "" {
   549  		// If the super admin can't see anything, then things have gone terribly wrong
   550  		return topicList, forumList, tList.defaultPagi(), err
   551  	}
   552  
   553  	topicList, pagi, err = tList.getList(page, orderby, topicCount, argList, qlist)
   554  	return topicList, forumList, pagi, err
   555  }
   556  
   557  // 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
   558  // TODO: Make orderby an enum of sorts
   559  func (tList *DefaultTopicList) getList(page, orderby, topicCount int, argList []interface{}, qlist string) (topicList []*TopicsRow, paginator Paginator, err error) {
   560  	if topicCount == 0 {
   561  		return nil, tList.defaultPagi(), err
   562  	}
   563  	//log.Printf("argList: %+v\n",argList)
   564  	//log.Printf("qlist: %+v\n",qlist)
   565  	var cols, orderq string
   566  	var stmt *sql.Stmt
   567  	switch orderby {
   568  	case TopicListWeekViews:
   569  		tList.qLock.RLock()
   570  		stmt = tList.qcounts[len(argList)-2]
   571  		tList.qLock.RUnlock()
   572  		if stmt == nil {
   573  			orderq = "weekViews DESC,lastReplyAt DESC,createdBy DESC"
   574  			now := time.Now()
   575  			_, week := now.ISOWeek()
   576  			day := int(now.Weekday()) + 1
   577  			if week%2 == 0 { // is even?
   578  				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"
   579  			} else {
   580  				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"
   581  			}
   582  			topicCount, err = ArgQToWeekViewTopicCount(argList, qlist)
   583  			if err != nil {
   584  				return nil, tList.defaultPagi(), err
   585  			}
   586  			acc := qgen.NewAcc()
   587  			stmt = acc.Select("topics").Columns(cols).Where("parentID IN(" + qlist + ") AND (weekEvenViews!=0 OR weekOddViews!=0)").Orderby(orderq).Limit("?,?").ComplexPrepare()
   588  			if e := acc.FirstError(); e != nil {
   589  				return nil, tList.defaultPagi(), e
   590  			}
   591  			defer stmt.Close()
   592  		}
   593  	case TopicListMostViewed:
   594  		tList.qLock.RLock()
   595  		stmt = tList.qcounts[len(argList)-2]
   596  		tList.qLock.RUnlock()
   597  		if stmt == nil {
   598  			orderq = "views DESC,lastReplyAt DESC,createdBy DESC"
   599  			cols = "tid,title,content,createdBy,is_closed,sticky,createdAt,lastReplyAt,lastReplyBy,lastReplyID,parentID,views,postCount,likeCount,attachCount,poll,data,weekEvenViews"
   600  		}
   601  	default:
   602  		tList.qLock2.RLock()
   603  		stmt = tList.qcounts2[len(argList)-2]
   604  		tList.qLock2.RUnlock()
   605  		if stmt == nil {
   606  			orderq = "sticky DESC,lastReplyAt DESC,createdBy DESC"
   607  			cols = "tid,title,content,createdBy,is_closed,sticky,createdAt,lastReplyAt,lastReplyBy,lastReplyID,parentID,views,postCount,likeCount,attachCount,poll,data,weekEvenViews"
   608  		}
   609  	}
   610  	offset, page, lastPage := PageOffset(topicCount, page, Config.ItemsPerPage)
   611  
   612  	// 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
   613  	if stmt == nil {
   614  		stmt, err = qgen.Builder.SimpleSelect("topics", cols, "parentID IN("+qlist+")", orderq, "?,?")
   615  		if err != nil {
   616  			return nil, tList.defaultPagi(), err
   617  		}
   618  		defer stmt.Close()
   619  	}
   620  
   621  	argList = append(argList, offset)
   622  	argList = append(argList, Config.ItemsPerPage)
   623  
   624  	rows, err := stmt.Query(argList...)
   625  	if err != nil {
   626  		return nil, tList.defaultPagi(), err
   627  	}
   628  	defer rows.Close()
   629  
   630  	rc, tc := Rstore.GetCache(), Topics.GetCache()
   631  	rcap := rc.GetCapacity()
   632  	rlen := rc.Length()
   633  	reqUserList := make(map[int]bool)
   634  	for rows.Next() {
   635  		// TODO: Embed Topic structs in TopicsRow to make it easier for us to reuse this work in the topic cache
   636  		t := TopicsRow{}
   637  		//var weekViews []uint8
   638  		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)
   639  		if err != nil {
   640  			return nil, tList.defaultPagi(), err
   641  		}
   642  		//t.WeekViews = int(weekViews[0])
   643  		//log.Printf("t: %+v\n", t)
   644  		//log.Printf("weekViews: %+v\n", weekViews)
   645  
   646  		t.Link = BuildTopicURL(NameToSlug(t.Title), t.ID)
   647  		// TODO: Pass forum to something like topicItem.Forum and use that instead of these two properties? Could be more flexible.
   648  		forum := Forums.DirtyGet(t.ParentID)
   649  		t.ForumName = forum.Name
   650  		t.ForumLink = forum.Link
   651  
   652  		// TODO: Create a specialised function with a bit less overhead for getting the last page for a post count
   653  		_, _, lastPage := PageOffset(t.PostCount, 1, Config.ItemsPerPage)
   654  		t.LastPage = lastPage
   655  
   656  		// TODO: Rename this Vhook to better reflect moving the topic list from /routes/ to /common/
   657  		GetHookTable().Vhook("topics_topic_row_assign", &t, &forum)
   658  		topicList = append(topicList, &t)
   659  		reqUserList[t.CreatedBy] = true
   660  		reqUserList[t.LastReplyBy] = true
   661  
   662  		//log.Print("rlen: ", rlen)
   663  		//log.Print("rcap: ", rcap)
   664  		//log.Print("t.PostCount: ", t.PostCount)
   665  		//log.Print("t.PostCount == 2 && rlen < rcap: ", t.PostCount == 2 && rlen < rcap)
   666  
   667  		// Avoid the extra queries on topic list pages, if we already have what we want...
   668  		hRids := false
   669  		if tc != nil {
   670  			if t, e := tc.Get(t.ID); e == nil {
   671  				hRids = len(t.Rids) != 0
   672  			}
   673  		}
   674  
   675  		if t.PostCount == 2 && rlen < rcap && !hRids && page < 5 {
   676  			rids, err := GetRidsForTopic(t.ID, 0)
   677  			if err != nil {
   678  				return nil, tList.defaultPagi(), err
   679  			}
   680  
   681  			//log.Print("rids: ", rids)
   682  			if len(rids) == 0 {
   683  				continue
   684  			}
   685  			_, _ = Rstore.Get(rids[0])
   686  			rlen++
   687  			t.Rids = []int{rids[0]}
   688  		}
   689  
   690  		if tc != nil {
   691  			if _, e := tc.Get(t.ID); e == sql.ErrNoRows {
   692  				//_ = tc.Set(t.Topic())
   693  				_ = tc.Set(&t.Topic)
   694  			}
   695  		}
   696  	}
   697  	if err = rows.Err(); err != nil {
   698  		return nil, tList.defaultPagi(), err
   699  	}
   700  
   701  	// TODO: specialcase for when reqUserList only has one or two items to avoid map alloc
   702  	if len(reqUserList) == 1 {
   703  		var u *User
   704  		for uid, _ := range reqUserList {
   705  			u, err = Users.Get(uid)
   706  			if err != nil {
   707  				return nil, tList.defaultPagi(), err
   708  			}
   709  		}
   710  		for _, t := range topicList {
   711  			t.Creator = u
   712  			t.LastUser = u
   713  		}
   714  	} else if len(reqUserList) > 0 {
   715  		// Convert the user ID map to a slice, then bulk load the users
   716  		idSlice := make([]int, len(reqUserList))
   717  		var i int
   718  		for userID := range reqUserList {
   719  			idSlice[i] = userID
   720  			i++
   721  		}
   722  
   723  		// TODO: What if a user is deleted via the Control Panel?
   724  		userList, err := Users.BulkGetMap(idSlice)
   725  		if err != nil {
   726  			return nil, tList.defaultPagi(), err
   727  		}
   728  
   729  		// Second pass to the add the user data
   730  		// TODO: Use a pointer to TopicsRow instead of TopicsRow itself?
   731  		for _, t := range topicList {
   732  			t.Creator = userList[t.CreatedBy]
   733  			t.LastUser = userList[t.LastReplyBy]
   734  		}
   735  	}
   736  
   737  	pageList := Paginate(page, lastPage, 5)
   738  	return topicList, Paginator{pageList, page, lastPage}, nil
   739  }
   740  
   741  // Internal. Don't rely on it.
   742  func ForumListToArgQ(forums []Forum) (argList []interface{}, qlist string) {
   743  	for _, forum := range forums {
   744  		argList = append(argList, strconv.Itoa(forum.ID))
   745  		qlist += "?,"
   746  	}
   747  	if qlist != "" {
   748  		qlist = qlist[0 : len(qlist)-1]
   749  	}
   750  	return argList, qlist
   751  }
   752  
   753  // Internal. Don't rely on it.
   754  // TODO: Check the TopicCount field on the forums instead? Make sure it's in sync first.
   755  func ArgQToTopicCount(argList []interface{}, qlist string) (topicCount int, err error) {
   756  	topicCountStmt, err := qgen.Builder.SimpleCount("topics", "parentID IN("+qlist+")", "")
   757  	if err != nil {
   758  		return 0, err
   759  	}
   760  	defer topicCountStmt.Close()
   761  
   762  	err = topicCountStmt.QueryRow(argList...).Scan(&topicCount)
   763  	if err != nil && err != ErrNoRows {
   764  		return 0, err
   765  	}
   766  	return topicCount, err
   767  }
   768  
   769  // Internal. Don't rely on it.
   770  func ArgQToWeekViewTopicCount(argList []interface{}, qlist string) (topicCount int, err error) {
   771  	topicCountStmt, err := qgen.Builder.SimpleCount("topics", "parentID IN("+qlist+") AND (weekEvenViews!=0 OR weekOddViews!=0)", "")
   772  	if err != nil {
   773  		return 0, err
   774  	}
   775  	defer topicCountStmt.Close()
   776  
   777  	err = topicCountStmt.QueryRow(argList...).Scan(&topicCount)
   778  	if err != nil && err != ErrNoRows {
   779  		return 0, err
   780  	}
   781  	return topicCount, err
   782  }
   783  
   784  func TopicCountInForums(forums []Forum) (topicCount int, err error) {
   785  	for _, f := range forums {
   786  		topicCount += f.TopicCount
   787  	}
   788  	return topicCount, nil
   789  }