2017-12-26 07:17:26 +00:00
package common
2018-02-10 15:07:21 +00:00
import (
2019-04-20 01:53:23 +00:00
//"log"
2019-05-19 01:01:11 +00:00
"database/sql"
2018-02-10 15:07:21 +00:00
"strconv"
"sync"
2018-10-27 03:21:02 +00:00
"github.com/Azareal/Gosora/query_gen"
2018-02-10 15:07:21 +00:00
)
var TopicList TopicListInt
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
}
type TopicListInt interface {
2019-02-10 05:52:26 +00:00
GetListByCanSee ( canSee [ ] int , page int , orderby string , filterIDs [ ] int ) ( topicList [ ] * TopicsRow , forumList [ ] Forum , paginator Paginator , err error )
GetListByGroup ( group * Group , page int , orderby string , filterIDs [ ] int ) ( topicList [ ] * TopicsRow , forumList [ ] Forum , paginator Paginator , err error )
GetList ( page int , orderby string , filterIDs [ ] int ) ( topicList [ ] * TopicsRow , forumList [ ] Forum , paginator Paginator , err 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?
2018-02-10 15:07:21 +00:00
oddGroups map [ int ] * TopicListHolder
evenGroups map [ int ] * TopicListHolder
oddLock sync . RWMutex
evenLock 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
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.
2018-02-10 15:07:21 +00:00
func NewDefaultTopicList ( ) ( * DefaultTopicList , error ) {
tList := & DefaultTopicList {
oddGroups : make ( map [ int ] * TopicListHolder ) ,
evenGroups : make ( map [ int ] * TopicListHolder ) ,
}
2018-11-17 02:36:02 +00:00
err := tList . Tick ( )
2018-02-10 15:07:21 +00:00
if err != nil {
return nil , err
}
2018-06-24 13:49:29 +00:00
2018-02-10 15:07:21 +00:00
AddScheduledHalfSecondTask ( tList . Tick )
//AddScheduledSecondTask(tList.GroupCountTick) // TODO: Dynamically change the groups in the short list to be optimised every second
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")
2019-07-26 23:29:42 +00:00
oddLists := make ( map [ int ] * TopicListHolder )
evenLists := make ( map [ int ] * TopicListHolder )
addList := func ( gid int , holder * TopicListHolder ) {
2018-02-10 15:07:21 +00:00
if gid % 2 == 0 {
2018-06-24 13:49:29 +00:00
evenLists [ gid ] = holder
2018-02-10 15:07:21 +00:00
} else {
2018-06-24 13:49:29 +00:00
oddLists [ gid ] = holder
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
2018-06-24 13:49:29 +00:00
for _ , group := 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 group . UserCount == 0 && group . ID != GuestUser . Group {
2018-02-15 13:15:27 +00:00
continue
}
2019-05-17 08:40:41 +00:00
2019-07-26 23:29:42 +00:00
canSee := make ( [ ] byte , len ( group . CanSee ) )
2018-06-24 13:49:29 +00:00
for i , item := range group . CanSee {
canSee [ i ] = byte ( item )
2018-02-10 15:07:21 +00:00
}
2019-05-17 08:40:41 +00:00
2018-11-17 02:36:02 +00:00
var canSeeInt = make ( [ ] int , len ( canSee ) )
copy ( canSeeInt , group . CanSee )
sCanSee := string ( canSee )
permTree [ sCanSee ] = canSeeInt
gidToCanSee [ group . ID ] = sCanSee
}
2019-07-26 23:29:42 +00:00
canSeeHolders := make ( map [ string ] * TopicListHolder )
2018-11-17 02:36:02 +00:00
for name , canSee := range permTree {
2019-02-10 05:52:26 +00:00
topicList , forumList , paginator , err := tList . GetListByCanSee ( canSee , 1 , "" , nil )
2018-11-17 02:36:02 +00:00
if err != nil {
return err
}
canSeeHolders [ name ] = & TopicListHolder { topicList , forumList , paginator }
}
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 ( )
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
}
2019-02-10 05:52:26 +00:00
func ( tList * DefaultTopicList ) GetListByGroup ( group * Group , page int , orderby string , filterIDs [ ] int ) ( topicList [ ] * TopicsRow , forumList [ ] Forum , paginator 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
2019-02-10 05:52:26 +00:00
if page == 1 && orderby == "" && len ( filterIDs ) == 0 {
2018-02-10 15:07:21 +00:00
var holder * TopicListHolder
var ok bool
if group . ID % 2 == 0 {
tList . evenLock . RLock ( )
holder , ok = tList . evenGroups [ group . ID ]
tList . evenLock . RUnlock ( )
} else {
tList . oddLock . RLock ( )
holder , ok = tList . oddGroups [ group . ID ]
tList . oddLock . RUnlock ( )
}
if ok {
2018-04-22 12:33:56 +00:00
return holder . List , holder . ForumList , holder . 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?
2018-08-27 09:06:00 +00:00
//log.Printf("deoptimising for %d on page %d\n", group.ID, page)
2019-02-10 05:52:26 +00:00
return tList . GetListByCanSee ( group . CanSee , page , orderby , filterIDs )
2018-06-24 13:49:29 +00:00
}
2018-02-10 15:07:21 +00:00
2019-02-10 05:52:26 +00:00
func ( tList * DefaultTopicList ) GetListByCanSee ( canSee [ ] int , page int , orderby string , filterIDs [ ] int ) ( topicList [ ] * TopicsRow , forumList [ ] Forum , paginator 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 {
forum := Forums . DirtyGet ( fid )
if forum . Name != "" && forum . Active && ( forum . ParentType == "" || forum . ParentType == "forum" ) {
fcopy := forum . Copy ( )
// TODO: Add a hook here for plugin_guilds
forumList = append ( forumList , fcopy )
}
}
2019-02-10 05:52:26 +00:00
var inSlice = func ( haystack [ ] int , needle int ) bool {
for _ , item := range haystack {
if needle == item {
return true
}
}
return false
}
var filteredForums [ ] Forum
if len ( filterIDs ) > 0 {
for _ , forum := range forumList {
if inSlice ( filterIDs , forum . ID ) {
filteredForums = append ( filteredForums , forum )
}
}
} else {
filteredForums = forumList
}
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
2019-04-20 01:53:23 +00:00
return topicList , filteredForums , Paginator { [ ] int { } , 1 , 1 } , nil
2018-02-10 15:07:21 +00:00
}
2018-09-26 07:46:30 +00:00
topicList , paginator , err = tList . getList ( page , orderby , argList , qlist )
2019-04-20 01:53:23 +00:00
return topicList , filteredForums , paginator , err
2018-02-10 15:07:21 +00:00
}
// TODO: Reduce the number of returns
2019-02-10 05:52:26 +00:00
func ( tList * DefaultTopicList ) GetList ( page int , orderby string , filterIDs [ ] int ) ( topicList [ ] * TopicsRow , forumList [ ] Forum , paginator 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 {
2018-04-22 12:33:56 +00:00
return nil , nil , Paginator { nil , 1 , 1 } , err
2018-02-10 15:07:21 +00:00
}
2019-02-10 05:52:26 +00:00
var inSlice = func ( haystack [ ] int , needle int ) bool {
for _ , item := range haystack {
if needle == item {
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
}
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 {
forum := Forums . DirtyGet ( fid )
if forum . Name != "" && forum . Active && ( forum . ParentType == "" || forum . ParentType == "forum" ) {
fcopy := forum . Copy ( )
// TODO: Add a hook here for plugin_guilds
forumList = append ( forumList , fcopy )
}
}
// ? - 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
2018-04-22 12:33:56 +00:00
return topicList , forumList , Paginator { [ ] int { } , 1 , 1 } , err
2018-02-10 15:07:21 +00:00
}
2018-09-26 07:46:30 +00:00
topicList , paginator , err = tList . getList ( page , orderby , argList , qlist )
2018-04-22 12:33:56 +00:00
return topicList , forumList , paginator , 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
2018-09-26 07:46:30 +00:00
func ( tList * DefaultTopicList ) getList ( page int , orderby string , argList [ ] interface { } , qlist string ) ( topicList [ ] * TopicsRow , paginator Paginator , err error ) {
2019-04-20 01:53:23 +00:00
//log.Printf("argList: %+v\n",argList)
//log.Printf("qlist: %+v\n",qlist)
2018-02-10 15:07:21 +00:00
topicCount , err := ArgQToTopicCount ( argList , qlist )
if err != nil {
2018-04-22 12:33:56 +00:00
return nil , Paginator { nil , 1 , 1 } , err
2018-02-10 15:07:21 +00:00
}
offset , page , lastPage := PageOffset ( topicCount , page , Config . ItemsPerPage )
2018-09-26 07:46:30 +00:00
var orderq string
if orderby == "most-viewed" {
orderq = "views DESC, lastReplyAt DESC, createdBy DESC"
} else {
orderq = "sticky DESC, lastReplyAt DESC, createdBy DESC"
}
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
2019-05-17 08:40:41 +00:00
stmt , err := qgen . Builder . SimpleSelect ( "topics" , "tid, title, content, createdBy, is_closed, sticky, createdAt, lastReplyAt, lastReplyBy, lastReplyID, parentID, views, postCount, likeCount, attachCount, poll, data" , "parentID IN(" + qlist + ")" , orderq , "?,?" )
2018-02-10 15:07:21 +00:00
if err != nil {
2018-04-22 12:33:56 +00:00
return nil , Paginator { nil , 1 , 1 } , err
2018-02-10 15:07:21 +00:00
}
defer stmt . Close ( )
argList = append ( argList , offset )
argList = append ( argList , Config . ItemsPerPage )
rows , err := stmt . Query ( argList ... )
if err != nil {
2018-04-22 12:33:56 +00:00
return nil , Paginator { nil , 1 , 1 } , err
2018-02-10 15:07:21 +00:00
}
defer rows . Close ( )
2019-05-17 08:40:41 +00:00
rcache := Rstore . GetCache ( )
rcap := rcache . GetCapacity ( )
rlen := rcache . Length ( )
tcache := Topics . GetCache ( )
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-05-17 08:40:41 +00:00
topic := TopicsRow { }
err := rows . Scan ( & topic . ID , & topic . Title , & topic . Content , & topic . CreatedBy , & topic . IsClosed , & topic . Sticky , & topic . CreatedAt , & topic . LastReplyAt , & topic . LastReplyBy , & topic . LastReplyID , & topic . ParentID , & topic . ViewCount , & topic . PostCount , & topic . LikeCount , & topic . AttachCount , & topic . Poll , & topic . Data )
2018-02-10 15:07:21 +00:00
if err != nil {
2018-04-22 12:33:56 +00:00
return nil , Paginator { nil , 1 , 1 } , err
2018-02-10 15:07:21 +00:00
}
2019-02-23 06:29:19 +00:00
topic . Link = BuildTopicURL ( NameToSlug ( topic . Title ) , topic . 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-02-23 06:29:19 +00:00
forum := Forums . DirtyGet ( topic . ParentID )
topic . ForumName = forum . Name
topic . 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-02-23 06:29:19 +00:00
_ , _ , lastPage := PageOffset ( topic . PostCount , 1 , Config . ItemsPerPage )
topic . 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-02-23 06:29:19 +00:00
GetHookTable ( ) . Vhook ( "topics_topic_row_assign" , & topic , & forum )
topicList = append ( topicList , & topic )
reqUserList [ topic . CreatedBy ] = true
reqUserList [ topic . LastReplyBy ] = true
2019-05-17 08:40:41 +00:00
//log.Print("rlen: ", rlen)
//log.Print("rcap: ", rcap)
//log.Print("topic.PostCount: ", topic.PostCount)
//log.Print("topic.PostCount == 2 && rlen < rcap: ", topic.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...
var hRids = false
if tcache != nil {
if t , err := tcache . Get ( topic . ID ) ; err == nil {
hRids = len ( t . Rids ) != 0
}
}
if topic . PostCount == 2 && rlen < rcap && ! hRids && page < 5 {
2019-05-17 08:40:41 +00:00
rids , err := GetRidsForTopic ( topic . ID , 0 )
if err != nil {
return nil , Paginator { nil , 1 , 1 } , err
}
//log.Print("rids: ", rids)
if len ( rids ) == 0 {
continue
}
_ , _ = Rstore . Get ( rids [ 0 ] )
rlen ++
topic . Rids = [ ] int { rids [ 0 ] }
}
if tcache != nil {
2019-05-19 01:01:11 +00:00
if _ , err := tcache . Get ( topic . ID ) ; err == sql . ErrNoRows {
_ = tcache . Set ( topic . Topic ( ) )
}
2019-05-17 08:40:41 +00:00
}
2018-02-10 15:07:21 +00:00
}
err = rows . Err ( )
if err != nil {
2018-04-22 12:33:56 +00:00
return nil , Paginator { nil , 1 , 1 } , err
2018-02-10 15:07:21 +00:00
}
// Convert the user ID map to a slice, then bulk load the users
var 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 {
2018-04-22 12:33:56 +00:00
return nil , Paginator { nil , 1 , 1 } , err
2018-02-10 15:07:21 +00:00
}
// Second pass to the add the user data
// TODO: Use a pointer to TopicsRow instead of TopicsRow itself?
2019-02-23 06:29:19 +00:00
for _ , topic := range topicList {
topic . Creator = userList [ topic . CreatedBy ]
topic . LastUser = userList [ topic . 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
}
func TopicCountInForums ( forums [ ] Forum ) ( topicCount int , err error ) {
return ArgQToTopicCount ( ForumListToArgQ ( forums ) )
}