2017-12-26 07:17:26 +00:00
package common
2018-02-10 15:07:21 +00:00
import (
2019-05-19 01:01:11 +00:00
"database/sql"
2021-05-03 00:36:29 +00:00
"fmt"
2018-02-10 15:07:21 +00:00
"strconv"
"sync"
2020-12-17 22:54:44 +00:00
"time"
2018-02-10 15:07:21 +00:00
2020-02-29 05:38:30 +00:00
qgen "github.com/Azareal/Gosora/query_gen"
2018-02-10 15:07:21 +00:00
)
var TopicList TopicListInt
2020-03-03 04:25:18 +00:00
const (
TopicListDefault = iota
TopicListMostViewed
2020-12-17 22:54:44 +00:00
TopicListWeekViews
2020-03-03 04:25:18 +00:00
)
2018-02-10 15:07:21 +00:00
type TopicListHolder struct {
List [ ] * TopicsRow
ForumList [ ] Forum
2018-04-22 12:33:56 +00:00
Paginator Paginator
2018-02-10 15:07:21 +00:00
}
2020-03-08 08:04:37 +00:00
type ForumTopicListHolder struct {
List [ ] * TopicsRow
Paginator Paginator
}
2021-03-24 12:07:46 +00:00
// TODO: Should we return no rows errors on empty pages? Is this likely to break something?
2018-02-10 15:07:21 +00:00
type TopicListInt interface {
2020-03-08 08:04:37 +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 )
2018-02-10 15:07:21 +00:00
}
2021-03-24 12:07:46 +00:00
type TopicListIntTest interface {
RawGetListByForum ( f * Forum , page , orderby int ) ( topicList [ ] * TopicsRow , pagi Paginator , err error )
Tick ( ) error
}
2018-02-10 15:07:21 +00:00
type DefaultTopicList struct {
2018-06-24 13:49:29 +00:00
// TODO: Rewrite this to put permTree as the primary and put canSeeStr on each group?
2020-04-13 06:44:20 +00:00
oddGroups map [ int ] [ 2 ] * TopicListHolder
evenGroups map [ int ] [ 2 ] * TopicListHolder
2018-02-10 15:07:21 +00:00
oddLock sync . RWMutex
evenLock sync . RWMutex
2020-03-08 08:04:37 +00:00
forums map [ int ] * ForumTopicListHolder
forumLock sync . RWMutex
2020-03-09 03:51:44 +00:00
qcounts map [ int ] * sql . Stmt
qcounts2 map [ int ] * sql . Stmt
qLock sync . RWMutex
qLock2 sync . RWMutex
2018-11-17 02:36:02 +00:00
//permTree atomic.Value // [string(canSee)]canSee
2018-06-24 13:49:29 +00:00
//permTree map[string][]int // [string(canSee)]canSee
2020-03-01 06:22:43 +00:00
getTopicsByForum * sql . Stmt
2020-03-08 08:04:37 +00:00
//getTidsByForum *sql.Stmt
2018-02-10 15:07:21 +00:00
}
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.
2020-03-01 06:22:43 +00:00
func NewDefaultTopicList ( acc * qgen . Accumulator ) ( * DefaultTopicList , error ) {
2018-02-10 15:07:21 +00:00
tList := & DefaultTopicList {
2020-04-13 06:44:20 +00:00
oddGroups : make ( map [ int ] [ 2 ] * TopicListHolder ) ,
evenGroups : make ( map [ int ] [ 2 ] * TopicListHolder ) ,
2020-03-08 08:04:37 +00:00
forums : make ( map [ int ] * ForumTopicListHolder ) ,
2020-03-09 03:51:44 +00:00
qcounts : make ( map [ int ] * sql . Stmt ) ,
qcounts2 : make ( map [ int ] * sql . Stmt ) ,
2020-04-02 20:25:55 +00:00
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 ( ) ,
2020-03-08 08:04:37 +00:00
//getTidsByForum: acc.Select("topics").Columns("tid").Where("parentID=?").Orderby("sticky DESC,lastReplyAt DESC,createdBy DESC").Limit("?,?").Prepare(),
2020-03-01 06:22:43 +00:00
}
2021-05-05 07:24:16 +00:00
if e := acc . FirstError ( ) ; e != nil {
return nil , e
2018-02-10 15:07:21 +00:00
}
2021-05-05 07:24:16 +00:00
if e := tList . Tick ( ) ; e != nil {
return nil , e
2018-02-10 15:07:21 +00:00
}
2018-06-24 13:49:29 +00:00
2021-05-03 00:36:29 +00:00
Tasks . HalfSec . Add ( tList . Tick )
//Tasks.Sec.Add(tList.GroupCountTick) // TODO: Dynamically change the groups in the short list to be optimised every second
2018-02-10 15:07:21 +00:00
return tList , nil
}
func ( tList * DefaultTopicList ) Tick ( ) error {
2018-11-19 23:06:15 +00:00
//fmt.Println("TopicList.Tick")
if ! TopicListThaw . Thawed ( ) {
return nil
}
//fmt.Println("building topic list")
2020-04-13 06:44:20 +00:00
oddLists := make ( map [ int ] [ 2 ] * TopicListHolder )
evenLists := make ( map [ int ] [ 2 ] * TopicListHolder )
addList := func ( gid int , h [ 2 ] * TopicListHolder ) {
2018-02-10 15:07:21 +00:00
if gid % 2 == 0 {
2020-03-08 08:04:37 +00:00
evenLists [ gid ] = h
2018-02-10 15:07:21 +00:00
} else {
2020-03-08 08:04:37 +00:00
oddLists [ gid ] = h
2018-02-10 15:07:21 +00:00
}
}
2018-06-24 13:49:29 +00:00
allGroups , err := Groups . GetAll ( )
2018-02-10 15:07:21 +00:00
if err != nil {
return err
}
2018-11-17 02:36:02 +00:00
2019-07-26 23:29:42 +00:00
gidToCanSee := make ( map [ int ] string )
permTree := make ( map [ string ] [ ] int ) // [string(canSee)]canSee
2020-03-08 08:04:37 +00:00
for _ , g := range allGroups {
2018-06-24 13:49:29 +00:00
// ? - Move the user count check to instance initialisation? Might require more book-keeping, particularly when a user moves into a zero user group
2020-03-08 08:04:37 +00:00
if g . UserCount == 0 && g . ID != GuestUser . Group {
2018-02-15 13:15:27 +00:00
continue
}
2019-05-17 08:40:41 +00:00
2020-03-08 08:04:37 +00:00
canSee := make ( [ ] byte , len ( g . CanSee ) )
for i , item := range g . CanSee {
2018-06-24 13:49:29 +00:00
canSee [ i ] = byte ( item )
2018-02-10 15:07:21 +00:00
}
2019-05-17 08:40:41 +00:00
2019-10-27 23:13:24 +00:00
canSeeInt := make ( [ ] int , len ( canSee ) )
2020-03-08 08:04:37 +00:00
copy ( canSeeInt , g . CanSee )
2018-11-17 02:36:02 +00:00
sCanSee := string ( canSee )
permTree [ sCanSee ] = canSeeInt
2020-03-08 08:04:37 +00:00
gidToCanSee [ g . ID ] = sCanSee
2018-11-17 02:36:02 +00:00
}
2020-04-13 06:44:20 +00:00
canSeeHolders := make ( map [ string ] [ 2 ] * TopicListHolder )
2020-03-09 03:51:44 +00:00
forumCounts := make ( map [ int ] int )
2018-11-17 02:36:02 +00:00
for name , canSee := range permTree {
2020-03-08 08:04:37 +00:00
topicList , forumList , pagi , err := tList . GetListByCanSee ( canSee , 1 , 0 , nil )
2018-11-17 02:36:02 +00:00
if err != nil {
return err
}
2020-04-13 06:44:20 +00:00
topicList2 , forumList2 , pagi2 , err := tList . GetListByCanSee ( canSee , 2 , 0 , nil )
if err != nil {
return err
}
canSeeHolders [ name ] = [ 2 ] * TopicListHolder {
2021-01-06 06:41:08 +00:00
{ topicList , forumList , pagi } ,
{ topicList2 , forumList2 , pagi2 } ,
2020-04-13 06:44:20 +00:00
}
2020-03-09 03:51:44 +00:00
if len ( canSee ) > 1 {
forumCounts [ len ( canSee ) ] += 1
}
2018-11-17 02:36:02 +00:00
}
for gid , canSee := range gidToCanSee {
addList ( gid , canSeeHolders [ canSee ] )
2018-02-10 15:07:21 +00:00
}
tList . oddLock . Lock ( )
tList . oddGroups = oddLists
tList . oddLock . Unlock ( )
tList . evenLock . Lock ( )
tList . evenGroups = evenLists
tList . evenLock . Unlock ( )
2020-03-09 03:51:44 +00:00
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
}
2021-03-24 12:07:46 +00:00
qlist := inqbuild2 ( top - 1 )
2020-03-09 03:51:44 +00:00
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 ( )
2021-05-03 00:36:29 +00:00
fmt . Printf ( "Forums: %+v\n" , Forums )
2020-03-08 08:04:37 +00:00
forums , err := Forums . GetAll ( )
if err != nil {
return err
}
2020-03-09 03:51:44 +00:00
top8 := [ ] * Forum { nil , nil , nil , nil , nil , nil , nil , nil }
2020-03-08 08:04:37 +00:00
z := true
addScore2 := func ( f * Forum ) {
2020-03-09 03:51:44 +00:00
for i , top := range top8 {
2020-03-08 08:04:37 +00:00
if top . TopicCount < f . TopicCount {
2020-03-09 03:51:44 +00:00
top8 [ i ] = f
2020-03-08 08:04:37 +00:00
return
}
}
}
addScore := func ( f * Forum ) {
if z {
2020-03-09 03:51:44 +00:00
for i , top := range top8 {
2020-03-08 08:04:37 +00:00
if top == nil {
2020-03-09 03:51:44 +00:00
top8 [ i ] = f
2020-03-08 08:04:37 +00:00
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 )
}
2020-03-09 03:51:44 +00:00
for _ , f := range top8 {
2020-03-08 08:10:56 +00:00
if f != nil {
fshort = append ( fshort , f )
}
2020-03-08 08:04:37 +00:00
}
2020-12-17 22:54:44 +00:00
// TODO: Avoid rebuilding the entire list on every tick
2020-03-08 08:04:37 +00:00
fList := make ( map [ int ] * ForumTopicListHolder )
for _ , f := range fshort {
2021-03-24 12:07:46 +00:00
topicList , pagi := [ ] * TopicsRow { } , tList . defaultPagi ( )
if f . TopicCount != 0 {
topicList , pagi , err = tList . RawGetListByForum ( f , 1 , 0 )
2020-12-17 22:54:44 +00:00
if err != nil {
return err
}
}
fList [ f . ID ] = & ForumTopicListHolder { topicList , pagi }
/ * topicList , pagi , err := tList . GetListByForum ( f , 1 , 0 )
2020-03-08 08:04:37 +00:00
if err != nil {
return err
}
2020-12-17 22:54:44 +00:00
fList [ f . ID ] = & ForumTopicListHolder { topicList , pagi } * /
2020-03-08 08:04:37 +00:00
}
2021-03-24 12:07:46 +00:00
//fmt.Printf("fList: %+v\n", fList)
tList . setForumList ( fList )
2020-03-08 08:04:37 +00:00
2019-04-19 01:02:33 +00:00
hTbl := GetHookTable ( )
_ , _ = hTbl . VhookSkippable ( "tasks_tick_topic_list" , tList )
2018-02-10 15:07:21 +00:00
return nil
}
2021-03-24 12:07:46 +00:00
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 ( )
}
2020-12-17 22:54:44 +00:00
/ * 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 {
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 ( )
2021-03-24 12:07:46 +00:00
topicList , pagi := [ ] * TopicsRow { } , tList . defaultPagi ( )
if forum . TopicCount != 0 {
2020-12-17 22:54:44 +00:00
topicList , pagi , err = tList . getListByForum ( forum , 1 , 0 )
if err != nil {
return err
}
}
fList [ forum . ID ] = & ForumTopicListHolder { topicList , pagi }
2021-03-24 12:07:46 +00:00
tList . setForumList ( fList )
2020-12-17 22:54:44 +00:00
return nil
} * /
2020-03-01 06:22:43 +00:00
// TODO: Add Topics() method to *Forum?
// TODO: Implement orderby
2020-03-08 08:04:37 +00:00
func ( tList * DefaultTopicList ) GetListByForum ( f * Forum , page , orderby int ) ( topicList [ ] * TopicsRow , pagi Paginator , err error ) {
if page == 0 {
page = 1
}
2020-03-09 03:51:44 +00:00
if f . TopicCount == 0 {
2021-03-24 12:07:46 +00:00
return topicList , tList . defaultPagi ( ) , nil
2020-03-09 03:51:44 +00:00
}
2020-03-08 08:04:37 +00:00
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
}
}
2021-03-24 12:07:46 +00:00
return tList . RawGetListByForum ( f , page , orderby )
2020-12-17 22:54:44 +00:00
}
2020-03-08 08:04:37 +00:00
2021-03-24 12:07:46 +00:00
func ( tList * DefaultTopicList ) RawGetListByForum ( f * Forum , page , orderby int ) ( topicList [ ] * TopicsRow , pagi Paginator , err error ) {
2020-03-01 06:22:43 +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 {
2021-03-24 12:07:46 +00:00
return nil , tList . defaultPagi ( ) , err
2020-03-01 06:22:43 +00:00
}
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 ( ) {
2021-01-02 21:46:24 +00:00
t := TopicsRow { Topic : Topic { ParentID : f . ID } }
2020-03-01 06:22:43 +00:00
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 {
2021-03-24 12:07:46 +00:00
return nil , tList . defaultPagi ( ) , err
2020-03-01 06:22:43 +00:00
}
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 {
2021-03-24 12:07:46 +00:00
return nil , tList . defaultPagi ( ) , err
2020-03-01 06:22:43 +00:00
}
// 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 {
2021-03-24 12:07:46 +00:00
return nil , tList . defaultPagi ( ) , err
2020-03-01 06:22:43 +00:00
}
// 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 ]
}
2021-03-24 12:07:46 +00:00
if len ( topicList ) == 0 {
return topicList , tList . defaultPagi ( ) , nil
}
2020-03-01 06:22:43 +00:00
pageList := Paginate ( page , lastPage , 5 )
return topicList , Paginator { pageList , page , lastPage } , nil
}
2020-03-08 08:04:37 +00:00
func ( tList * DefaultTopicList ) GetListByGroup ( g * Group , page , orderby int , filterIDs [ ] int ) ( topicList [ ] * TopicsRow , forumList [ ] Forum , pagi Paginator , err error ) {
2018-08-27 09:06:00 +00:00
if page == 0 {
page = 1
}
2018-02-10 15:07:21 +00:00
// TODO: Cache the first three pages not just the first along with all the topics on this beaten track
2020-04-13 06:44:20 +00:00
// TODO: Move this into CanSee to reduce redundancy
if ( page == 1 || page == 2 ) && orderby == 0 && len ( filterIDs ) == 0 {
var h [ 2 ] * TopicListHolder
2018-02-10 15:07:21 +00:00
var ok bool
2020-03-08 08:04:37 +00:00
if g . ID % 2 == 0 {
2018-02-10 15:07:21 +00:00
tList . evenLock . RLock ( )
2020-03-08 08:04:37 +00:00
h , ok = tList . evenGroups [ g . ID ]
2018-02-10 15:07:21 +00:00
tList . evenLock . RUnlock ( )
} else {
tList . oddLock . RLock ( )
2020-03-08 08:04:37 +00:00
h , ok = tList . oddGroups [ g . ID ]
2018-02-10 15:07:21 +00:00
tList . oddLock . RUnlock ( )
}
if ok {
2020-04-13 06:44:20 +00:00
return h [ page - 1 ] . List , h [ page - 1 ] . ForumList , h [ page - 1 ] . Paginator , nil
2018-02-10 15:07:21 +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?
2020-03-08 08:04:37 +00:00
//log.Printf("deoptimising for %d on page %d\n", g.ID, page)
return tList . GetListByCanSee ( g . CanSee , page , orderby , filterIDs )
2018-06-24 13:49:29 +00:00
}
2018-02-10 15:07:21 +00:00
2020-03-08 08:04:37 +00:00
func ( tList * DefaultTopicList ) GetListByCanSee ( canSee [ ] int , page , orderby int , filterIDs [ ] int ) ( topicList [ ] * TopicsRow , forumList [ ] Forum , pagi Paginator , err error ) {
2019-02-23 06:29:19 +00:00
// TODO: Optimise this by filtering canSee and then fetching the forums?
2018-02-10 15:07:21 +00:00
// 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 {
2019-11-12 06:56:51 +00:00
f := Forums . DirtyGet ( fid )
2020-08-20 22:18:54 +00:00
if f . Name != "" && f . Active && ( f . ParentType == "" || f . ParentType == "forum" ) /*&& f.TopicCount != 0*/ {
2019-11-12 06:56:51 +00:00
fcopy := f . Copy ( )
2020-08-15 02:37:56 +00:00
// TODO: Add a hook here for plugin_guilds !!
2018-02-10 15:07:21 +00:00
forumList = append ( forumList , fcopy )
}
}
2019-10-27 23:13:24 +00:00
inSlice := func ( haystack [ ] int , needle int ) bool {
2020-08-20 22:18:54 +00:00
for _ , it := range haystack {
if needle == it {
2019-02-10 05:52:26 +00:00
return true
}
}
return false
}
var filteredForums [ ] Forum
if len ( filterIDs ) > 0 {
2020-02-29 05:38:30 +00:00
for _ , f := range forumList {
if inSlice ( filterIDs , f . ID ) {
filteredForums = append ( filteredForums , f )
2019-02-10 05:52:26 +00:00
}
}
} else {
filteredForums = forumList
}
2020-03-03 04:25:18 +00:00
if len ( filteredForums ) == 1 && orderby == 0 {
2020-03-01 06:22:43 +00:00
topicList , pagi , err = tList . GetListByForum ( & filteredForums [ 0 ] , page , orderby )
return topicList , forumList , pagi , err
}
2020-02-29 05:38:30 +00:00
var topicCount int
for _ , f := range filteredForums {
topicCount += f . TopicCount
}
2019-02-10 05:52:26 +00:00
2018-02-10 15:07:21 +00:00
// ? - Should we be showing plugin_guilds posts on /topics/?
2019-04-20 01:53:23 +00:00
argList , qlist := ForumListToArgQ ( filteredForums )
2018-02-10 15:07:21 +00:00
if qlist == "" {
// We don't want to kill the page, so pass an empty slice and nil error
2021-03-24 12:07:46 +00:00
return topicList , filteredForums , tList . defaultPagi ( ) , nil
2018-02-10 15:07:21 +00:00
}
2020-03-01 06:22:43 +00:00
topicList , pagi , err = tList . getList ( page , orderby , topicCount , argList , qlist )
return topicList , filteredForums , pagi , err
2018-02-10 15:07:21 +00:00
}
// TODO: Reduce the number of returns
2020-03-03 04:25:18 +00:00
func ( tList * DefaultTopicList ) GetList ( page , orderby int , filterIDs [ ] int ) ( topicList [ ] * TopicsRow , forumList [ ] Forum , pagi Paginator , err error ) {
2018-02-10 15:07:21 +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?
2019-02-10 05:52:26 +00:00
cCanSee , err := Forums . GetAllVisibleIDs ( )
2018-02-10 15:07:21 +00:00
if err != nil {
2021-03-24 12:07:46 +00:00
return nil , nil , tList . defaultPagi ( ) , err
2018-02-10 15:07:21 +00:00
}
2020-08-20 22:18:54 +00:00
//log.Printf("cCanSee: %+v\n", cCanSee)
2019-10-27 23:13:24 +00:00
inSlice := func ( haystack [ ] int , needle int ) bool {
2020-08-20 22:18:54 +00:00
for _ , it := range haystack {
if needle == it {
2019-02-10 05:52:26 +00:00
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
}
2020-08-20 22:18:54 +00:00
//log.Printf("canSee: %+v\n", canSee)
2019-02-10 05:52:26 +00:00
2018-02-10 15:07:21 +00:00
// We need a list of the visible forums for Quick Topic
// ? - Would it be useful, if we could post in social groups from /topics/?
2020-02-29 05:38:30 +00:00
var topicCount int
2018-02-10 15:07:21 +00:00
for _ , fid := range canSee {
2019-11-12 06:56:51 +00:00
f := Forums . DirtyGet ( fid )
2020-08-20 22:18:54 +00:00
if f . Name != "" && f . Active && ( f . ParentType == "" || f . ParentType == "forum" ) /*&& f.TopicCount != 0*/ {
2019-11-12 06:56:51 +00:00
fcopy := f . Copy ( )
2018-02-10 15:07:21 +00:00
// TODO: Add a hook here for plugin_guilds
forumList = append ( forumList , fcopy )
2020-02-29 05:38:30 +00:00
topicCount += fcopy . TopicCount
2018-02-10 15:07:21 +00:00
}
}
2020-03-03 04:25:18 +00:00
if len ( forumList ) == 1 && orderby == 0 {
2020-03-01 06:22:43 +00:00
topicList , pagi , err = tList . GetListByForum ( & forumList [ 0 ] , page , orderby )
return topicList , forumList , pagi , err
}
2018-02-10 15:07:21 +00:00
// ? - 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
2021-03-24 12:07:46 +00:00
return topicList , forumList , tList . defaultPagi ( ) , err
2018-02-10 15:07:21 +00:00
}
2020-03-01 06:22:43 +00:00
topicList , pagi , err = tList . getList ( page , orderby , topicCount , argList , qlist )
return topicList , forumList , pagi , err
2018-02-10 15:07:21 +00:00
}
// 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
2020-03-01 06:22:43 +00:00
// TODO: Make orderby an enum of sorts
2020-03-03 04:25:18 +00:00
func ( tList * DefaultTopicList ) getList ( page , orderby , topicCount int , argList [ ] interface { } , qlist string ) ( topicList [ ] * TopicsRow , paginator Paginator , err error ) {
2020-08-20 22:18:54 +00:00
if topicCount == 0 {
2021-03-24 12:07:46 +00:00
return nil , tList . defaultPagi ( ) , err
2020-08-20 22:18:54 +00:00
}
2019-04-20 01:53:23 +00:00
//log.Printf("argList: %+v\n",argList)
//log.Printf("qlist: %+v\n",qlist)
2020-12-17 22:54:44 +00:00
var cols , orderq string
2020-03-09 03:51:44 +00:00
var stmt * sql . Stmt
2020-12-17 22:54:44 +00:00
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?
2021-01-02 21:46:24 +00:00
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"
2020-12-17 22:54:44 +00:00
} else {
2021-01-02 21:46:24 +00:00
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"
2020-12-17 22:54:44 +00:00
}
topicCount , err = ArgQToWeekViewTopicCount ( argList , qlist )
if err != nil {
2021-03-24 12:07:46 +00:00
return nil , tList . defaultPagi ( ) , err
2020-12-17 22:54:44 +00:00
}
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 {
2021-03-24 12:07:46 +00:00
return nil , tList . defaultPagi ( ) , e
2020-12-17 22:54:44 +00:00
}
defer stmt . Close ( )
}
case TopicListMostViewed :
2020-03-09 03:51:44 +00:00
tList . qLock . RLock ( )
stmt = tList . qcounts [ len ( argList ) - 2 ]
tList . qLock . RUnlock ( )
if stmt == nil {
orderq = "views DESC,lastReplyAt DESC,createdBy DESC"
2021-01-02 21:46:24 +00:00
cols = "tid,title,content,createdBy,is_closed,sticky,createdAt,lastReplyAt,lastReplyBy,lastReplyID,parentID,views,postCount,likeCount,attachCount,poll,data,weekEvenViews"
2020-03-09 03:51:44 +00:00
}
2020-12-17 22:54:44 +00:00
default :
2020-03-09 03:51:44 +00:00
tList . qLock2 . RLock ( )
stmt = tList . qcounts2 [ len ( argList ) - 2 ]
tList . qLock2 . RUnlock ( )
if stmt == nil {
orderq = "sticky DESC,lastReplyAt DESC,createdBy DESC"
2021-01-02 21:46:24 +00:00
cols = "tid,title,content,createdBy,is_closed,sticky,createdAt,lastReplyAt,lastReplyBy,lastReplyID,parentID,views,postCount,likeCount,attachCount,poll,data,weekEvenViews"
2020-03-09 03:51:44 +00:00
}
2018-09-26 07:46:30 +00:00
}
2020-03-01 06:22:43 +00:00
offset , page , lastPage := PageOffset ( topicCount , page , Config . ItemsPerPage )
2018-09-26 07:46:30 +00:00
2018-09-13 07:41:01 +00:00
// 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
2020-03-09 03:51:44 +00:00
if stmt == nil {
2020-12-17 22:54:44 +00:00
stmt , err = qgen . Builder . SimpleSelect ( "topics" , cols , "parentID IN(" + qlist + ")" , orderq , "?,?" )
2020-03-09 03:51:44 +00:00
if err != nil {
2021-03-24 12:07:46 +00:00
return nil , tList . defaultPagi ( ) , err
2020-03-09 03:51:44 +00:00
}
defer stmt . Close ( )
2018-02-10 15:07:21 +00:00
}
argList = append ( argList , offset )
argList = append ( argList , Config . ItemsPerPage )
rows , err := stmt . Query ( argList ... )
if err != nil {
2021-03-24 12:07:46 +00:00
return nil , tList . defaultPagi ( ) , err
2018-02-10 15:07:21 +00:00
}
defer rows . Close ( )
2021-03-24 12:07:46 +00:00
rc , tc := Rstore . GetCache ( ) , Topics . GetCache ( )
2020-12-17 22:54:44 +00:00
rcap := rc . GetCapacity ( )
rlen := rc . Length ( )
2019-05-17 08:40:41 +00:00
reqUserList := make ( map [ int ] bool )
2018-02-10 15:07:21 +00:00
for rows . Next ( ) {
2018-09-13 07:41:01 +00:00
// TODO: Embed Topic structs in TopicsRow to make it easier for us to reuse this work in the topic cache
2019-10-27 23:13:24 +00:00
t := TopicsRow { }
2020-12-17 22:54:44 +00:00
//var weekViews []uint8
2021-01-02 21:46:24 +00:00
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 )
2018-02-10 15:07:21 +00:00
if err != nil {
2021-03-24 12:07:46 +00:00
return nil , tList . defaultPagi ( ) , err
2018-02-10 15:07:21 +00:00
}
2021-01-02 21:46:24 +00:00
//t.WeekViews = int(weekViews[0])
//log.Printf("t: %+v\n", t)
2020-12-17 22:54:44 +00:00
//log.Printf("weekViews: %+v\n", weekViews)
2018-02-10 15:07:21 +00:00
2019-10-27 23:13:24 +00:00
t . Link = BuildTopicURL ( NameToSlug ( t . Title ) , t . ID )
2018-06-24 13:49:29 +00:00
// TODO: Pass forum to something like topicItem.Forum and use that instead of these two properties? Could be more flexible.
2019-10-27 23:13:24 +00:00
forum := Forums . DirtyGet ( t . ParentID )
t . ForumName = forum . Name
t . ForumLink = forum . Link
2018-02-10 15:07:21 +00:00
2018-12-17 04:58:55 +00:00
// TODO: Create a specialised function with a bit less overhead for getting the last page for a post count
2019-10-27 23:13:24 +00:00
_ , _ , lastPage := PageOffset ( t . PostCount , 1 , Config . ItemsPerPage )
t . LastPage = lastPage
2018-02-10 15:07:21 +00:00
// TODO: Rename this Vhook to better reflect moving the topic list from /routes/ to /common/
2019-10-27 23:13:24 +00:00
GetHookTable ( ) . Vhook ( "topics_topic_row_assign" , & t , & forum )
topicList = append ( topicList , & t )
reqUserList [ t . CreatedBy ] = true
reqUserList [ t . LastReplyBy ] = true
2019-05-17 08:40:41 +00:00
//log.Print("rlen: ", rlen)
//log.Print("rcap: ", rcap)
2020-03-01 06:22:43 +00:00
//log.Print("t.PostCount: ", t.PostCount)
//log.Print("t.PostCount == 2 && rlen < rcap: ", t.PostCount == 2 && rlen < rcap)
2019-05-19 01:01:11 +00:00
// Avoid the extra queries on topic list pages, if we already have what we want...
2019-10-27 23:13:24 +00:00
hRids := false
2020-03-01 06:22:43 +00:00
if tc != nil {
2021-01-02 21:46:24 +00:00
if t , e := tc . Get ( t . ID ) ; e == nil {
2019-05-19 01:01:11 +00:00
hRids = len ( t . Rids ) != 0
}
}
2019-10-27 23:13:24 +00:00
if t . PostCount == 2 && rlen < rcap && ! hRids && page < 5 {
rids , err := GetRidsForTopic ( t . ID , 0 )
2019-05-17 08:40:41 +00:00
if err != nil {
2021-03-24 12:07:46 +00:00
return nil , tList . defaultPagi ( ) , err
2019-05-17 08:40:41 +00:00
}
//log.Print("rids: ", rids)
if len ( rids ) == 0 {
continue
}
_ , _ = Rstore . Get ( rids [ 0 ] )
rlen ++
2019-10-27 23:13:24 +00:00
t . Rids = [ ] int { rids [ 0 ] }
2019-05-17 08:40:41 +00:00
}
2020-03-01 06:22:43 +00:00
if tc != nil {
2020-08-15 02:37:56 +00:00
if _ , e := tc . Get ( t . ID ) ; e == sql . ErrNoRows {
//_ = tc.Set(t.Topic())
_ = tc . Set ( & t . Topic )
2019-05-19 01:01:11 +00:00
}
2019-05-17 08:40:41 +00:00
}
2018-02-10 15:07:21 +00:00
}
2019-11-12 06:56:51 +00:00
if err = rows . Err ( ) ; err != nil {
2021-03-24 12:07:46 +00:00
return nil , tList . defaultPagi ( ) , err
2018-02-10 15:07:21 +00:00
}
2020-08-15 02:37:56 +00:00
// 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 {
2021-03-24 12:07:46 +00:00
return nil , tList . defaultPagi ( ) , err
2020-08-15 02:37:56 +00:00
}
}
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 ++
}
2018-02-10 15:07:21 +00:00
2020-08-15 02:37:56 +00:00
// TODO: What if a user is deleted via the Control Panel?
userList , err := Users . BulkGetMap ( idSlice )
if err != nil {
2021-03-24 12:07:46 +00:00
return nil , tList . defaultPagi ( ) , err
2020-08-15 02:37:56 +00:00
}
2018-02-10 15:07:21 +00:00
2020-08-15 02:37:56 +00:00
// 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 ]
}
2018-02-10 15:07:21 +00:00
}
2019-06-04 05:48:12 +00:00
pageList := Paginate ( page , lastPage , 5 )
2018-04-22 12:33:56 +00:00
return topicList , Paginator { pageList , page , lastPage } , nil
2018-02-10 15:07:21 +00:00
}
2017-12-26 07:17:26 +00:00
// Internal. Don't rely on it.
func ForumListToArgQ ( forums [ ] Forum ) ( argList [ ] interface { } , qlist string ) {
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.
2019-04-20 01:53:23 +00:00
// TODO: Check the TopicCount field on the forums instead? Make sure it's in sync first.
2017-12-26 07:17:26 +00:00
func ArgQToTopicCount ( argList [ ] interface { } , qlist string ) ( topicCount int , err error ) {
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
}
2020-12-17 22:54:44 +00:00
// Internal. Don't rely on it.
func ArgQToWeekViewTopicCount ( argList [ ] interface { } , qlist string ) ( topicCount int , err error ) {
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
}
2017-12-26 07:17:26 +00:00
func TopicCountInForums ( forums [ ] Forum ) ( topicCount int , err error ) {
2020-02-29 07:34:38 +00:00
for _ , f := range forums {
topicCount += f . TopicCount
}
return topicCount , nil
2017-12-26 07:17:26 +00:00
}