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

     1  /*
     2  *
     3  *	Gosora Forum Store
     4  * 	Copyright Azareal 2017 - 2020
     5  *
     6   */
     7  package common
     8  
     9  import (
    10  	"database/sql"
    11  	"errors"
    12  	"log"
    13  
    14  	//"fmt"
    15  	"sort"
    16  	"sync"
    17  	"sync/atomic"
    18  
    19  	qgen "github.com/Azareal/Gosora/query_gen"
    20  )
    21  
    22  var forumCreateMutex sync.Mutex
    23  var forumPerms map[int]map[int]*ForumPerms // [gid][fid]*ForumPerms // TODO: Add an abstraction around this and make it more thread-safe
    24  var Forums ForumStore
    25  var ErrBlankName = errors.New("The name must not be blank")
    26  var ErrNoDeleteReports = errors.New("You cannot delete the Reports forum")
    27  
    28  // ForumStore is an interface for accessing the forums and the metadata stored on them
    29  type ForumStore interface {
    30  	LoadForums() error
    31  	Each(h func(*Forum) error) error
    32  	DirtyGet(id int) *Forum
    33  	Get(id int) (*Forum, error)
    34  	BypassGet(id int) (*Forum, error)
    35  	BulkGetCopy(ids []int) (forums []Forum, err error)
    36  	Reload(id int) error // ? - Should we move this to ForumCache? It might require us to do some unnecessary casting though
    37  	//Update(Forum) error
    38  	Delete(id int) error
    39  	AddTopic(tid, uid, fid int) error
    40  	RemoveTopic(fid int) error
    41  	RemoveTopics(fid, count int) error
    42  	UpdateLastTopic(tid, uid, fid int) error
    43  	Exists(id int) bool
    44  	GetAll() ([]*Forum, error)
    45  	GetAllIDs() ([]int, error)
    46  	GetAllVisible() ([]*Forum, error)
    47  	GetAllVisibleIDs() ([]int, error)
    48  	//GetChildren(parentID int, parentType string) ([]*Forum,error)
    49  	//GetFirstChild(parentID int, parentType string) (*Forum,error)
    50  	Create(name, desc string, active bool, preset string) (int, error)
    51  	UpdateOrder(updateMap map[int]int) error
    52  
    53  	Count() int
    54  }
    55  
    56  type ForumCache interface {
    57  	CacheGet(id int) (*Forum, error)
    58  	CacheSet(f *Forum) error
    59  	CacheDelete(id int)
    60  	Length() int
    61  }
    62  
    63  // MemoryForumStore is a struct which holds an arbitrary number of forums in memory, usually all of them, although we might introduce functionality to hold a smaller subset in memory for sites with an extremely large number of forums
    64  type MemoryForumStore struct {
    65  	forums    sync.Map     // map[int]*Forum
    66  	forumView atomic.Value // []*Forum
    67  
    68  	get          *sql.Stmt
    69  	getAll       *sql.Stmt
    70  	delete       *sql.Stmt
    71  	create       *sql.Stmt
    72  	count        *sql.Stmt
    73  	updateCache  *sql.Stmt
    74  	addTopics    *sql.Stmt
    75  	removeTopics *sql.Stmt
    76  	lastTopic    *sql.Stmt
    77  	updateOrder  *sql.Stmt
    78  }
    79  
    80  // NewMemoryForumStore gives you a new instance of MemoryForumStore
    81  func NewMemoryForumStore() (*MemoryForumStore, error) {
    82  	acc := qgen.NewAcc()
    83  	f := "forums"
    84  	set := func(s string) *sql.Stmt {
    85  		return acc.Update(f).Set(s).Where("fid=?").Prepare()
    86  	}
    87  	// TODO: Do a proper delete
    88  	return &MemoryForumStore{
    89  		get:          acc.Select(f).Columns("name, desc, tmpl, active, order, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID").Where("fid=?").Prepare(),
    90  		getAll:       acc.Select(f).Columns("fid, name, desc, tmpl, active, order, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID").Orderby("order ASC, fid ASC").Prepare(),
    91  		delete:       set("name='',active=0"),
    92  		create:       acc.Insert(f).Columns("name,desc,tmpl,active,preset").Fields("?,?,'',?,?").Prepare(),
    93  		count:        acc.Count(f).Where("name != ''").Prepare(),
    94  		updateCache:  set("lastTopicID=?,lastReplyerID=?"),
    95  		addTopics:    set("topicCount=topicCount+?"),
    96  		removeTopics: set("topicCount=topicCount-?"),
    97  		lastTopic:    acc.Select("topics").Columns("tid").Where("parentID=?").Orderby("lastReplyAt DESC,createdAt DESC").Limit("1").Prepare(),
    98  		updateOrder:  set("order=?"),
    99  	}, acc.FirstError()
   100  }
   101  
   102  // TODO: Rename to ReloadAll?
   103  // TODO: Add support for subforums
   104  func (s *MemoryForumStore) LoadForums() error {
   105  	var forumView []*Forum
   106  	addForum := func(f *Forum) {
   107  		s.forums.Store(f.ID, f)
   108  		if f.Active && f.Name != "" && f.ParentType == "" {
   109  			forumView = append(forumView, f)
   110  		}
   111  	}
   112  
   113  	rows, err := s.getAll.Query()
   114  	if err != nil {
   115  		return err
   116  	}
   117  	defer rows.Close()
   118  
   119  	i := 0
   120  	for ; rows.Next(); i++ {
   121  		f := &Forum{ID: 0, Active: true, Preset: "all"}
   122  		err = rows.Scan(&f.ID, &f.Name, &f.Desc, &f.Tmpl, &f.Active, &f.Order, &f.Preset, &f.ParentID, &f.ParentType, &f.TopicCount, &f.LastTopicID, &f.LastReplyerID)
   123  		if err != nil {
   124  			return err
   125  		}
   126  
   127  		if f.Name == "" {
   128  			DebugLog("Adding a placeholder forum")
   129  		} else {
   130  			log.Printf("Adding the '%s' forum", f.Name)
   131  		}
   132  
   133  		f.Link = BuildForumURL(NameToSlug(f.Name), f.ID)
   134  		f.LastTopic = Topics.DirtyGet(f.LastTopicID)
   135  		f.LastReplyer = Users.DirtyGet(f.LastReplyerID)
   136  		// TODO: Create a specialised function with a bit less overhead for getting the last page for a post count
   137  		_, _, lastPage := PageOffset(f.LastTopic.PostCount, 1, Config.ItemsPerPage)
   138  		f.LastPage = lastPage
   139  		addForum(f)
   140  	}
   141  	s.forumView.Store(forumView)
   142  	TopicListThaw.Thaw()
   143  	return rows.Err()
   144  }
   145  
   146  // TODO: Hide social groups too
   147  // ? - Will this be hit a lot by plugin_guilds?
   148  func (s *MemoryForumStore) rebuildView() {
   149  	var forumView []*Forum
   150  	s.forums.Range(func(_, val interface{}) bool {
   151  		f := val.(*Forum)
   152  		// ? - ParentType blank means that it doesn't have a parent
   153  		if f.Active && f.Name != "" && f.ParentType == "" {
   154  			forumView = append(forumView, f)
   155  		}
   156  		return true
   157  	})
   158  	sort.Sort(SortForum(forumView))
   159  	s.forumView.Store(forumView)
   160  	TopicListThaw.Thaw()
   161  }
   162  
   163  func (s *MemoryForumStore) Each(h func(*Forum) error) (err error) {
   164  	s.forums.Range(func(_, val interface{}) bool {
   165  		err = h(val.(*Forum))
   166  		if err != nil {
   167  			return false
   168  		}
   169  		return true
   170  	})
   171  	return err
   172  }
   173  
   174  func (s *MemoryForumStore) DirtyGet(id int) *Forum {
   175  	fint, ok := s.forums.Load(id)
   176  	if !ok || fint.(*Forum).Name == "" {
   177  		return &Forum{ID: -1, Name: ""}
   178  	}
   179  	return fint.(*Forum)
   180  }
   181  
   182  func (s *MemoryForumStore) CacheGet(id int) (*Forum, error) {
   183  	fint, ok := s.forums.Load(id)
   184  	if !ok || fint.(*Forum).Name == "" {
   185  		return nil, ErrNoRows
   186  	}
   187  	return fint.(*Forum), nil
   188  }
   189  
   190  func (s *MemoryForumStore) Get(id int) (*Forum, error) {
   191  	fint, ok := s.forums.Load(id)
   192  	if ok {
   193  		forum := fint.(*Forum)
   194  		if forum.Name == "" {
   195  			return nil, ErrNoRows
   196  		}
   197  		return forum, nil
   198  	}
   199  
   200  	forum, err := s.BypassGet(id)
   201  	if err != nil {
   202  		return nil, err
   203  	}
   204  	s.CacheSet(forum)
   205  	return forum, err
   206  }
   207  
   208  func (s *MemoryForumStore) BypassGet(id int) (*Forum, error) {
   209  	f := &Forum{ID: id}
   210  	err := s.get.QueryRow(id).Scan(&f.Name, &f.Desc, &f.Tmpl, &f.Active, &f.Order, &f.Preset, &f.ParentID, &f.ParentType, &f.TopicCount, &f.LastTopicID, &f.LastReplyerID)
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  	if f.Name == "" {
   215  		return nil, ErrNoRows
   216  	}
   217  	f.Link = BuildForumURL(NameToSlug(f.Name), f.ID)
   218  	f.LastTopic = Topics.DirtyGet(f.LastTopicID)
   219  	f.LastReplyer = Users.DirtyGet(f.LastReplyerID)
   220  	// TODO: Create a specialised function with a bit less overhead for getting the last page for a post count
   221  	_, _, lastPage := PageOffset(f.LastTopic.PostCount, 1, Config.ItemsPerPage)
   222  	f.LastPage = lastPage
   223  	//TopicListThaw.Thaw()
   224  
   225  	return f, err
   226  }
   227  
   228  // TODO: Optimise this
   229  func (s *MemoryForumStore) BulkGetCopy(ids []int) (forums []Forum, err error) {
   230  	forums = make([]Forum, len(ids))
   231  	for i, id := range ids {
   232  		f, err := s.Get(id)
   233  		if err != nil {
   234  			return nil, err
   235  		}
   236  		forums[i] = f.Copy()
   237  	}
   238  	return forums, nil
   239  }
   240  
   241  func (s *MemoryForumStore) Reload(id int) error {
   242  	forum, err := s.BypassGet(id)
   243  	if err != nil {
   244  		return err
   245  	}
   246  	s.CacheSet(forum)
   247  	return nil
   248  }
   249  
   250  func (s *MemoryForumStore) CacheSet(f *Forum) error {
   251  	s.forums.Store(f.ID, f)
   252  	s.rebuildView()
   253  	return nil
   254  }
   255  
   256  // ! Has a randomised order
   257  func (s *MemoryForumStore) GetAll() (forumView []*Forum, err error) {
   258  	s.forums.Range(func(_, val interface{}) bool {
   259  		forumView = append(forumView, val.(*Forum))
   260  		return true
   261  	})
   262  	sort.Sort(SortForum(forumView))
   263  	return forumView, nil
   264  }
   265  
   266  // ? - Can we optimise the sorting?
   267  func (s *MemoryForumStore) GetAllIDs() (ids []int, err error) {
   268  	s.forums.Range(func(_, val interface{}) bool {
   269  		ids = append(ids, val.(*Forum).ID)
   270  		return true
   271  	})
   272  	sort.Ints(ids)
   273  	return ids, nil
   274  }
   275  
   276  func (s *MemoryForumStore) GetAllVisible() (forumView []*Forum, err error) {
   277  	forumView = s.forumView.Load().([]*Forum)
   278  	return forumView, nil
   279  }
   280  
   281  func (s *MemoryForumStore) GetAllVisibleIDs() ([]int, error) {
   282  	forumView := s.forumView.Load().([]*Forum)
   283  	ids := make([]int, len(forumView))
   284  	for i := 0; i < len(forumView); i++ {
   285  		ids[i] = forumView[i].ID
   286  	}
   287  	return ids, nil
   288  }
   289  
   290  // TODO: Implement sub-forums.
   291  /*func (s *MemoryForumStore) GetChildren(parentID int, parentType string) ([]*Forum,error) {
   292  	return nil, nil
   293  }
   294  func (s *MemoryForumStore) GetFirstChild(parentID int, parentType string) (*Forum,error) {
   295  	return nil, nil
   296  }*/
   297  
   298  // TODO: Add a query for this rather than hitting cache
   299  func (s *MemoryForumStore) Exists(id int) bool {
   300  	forum, ok := s.forums.Load(id)
   301  	if !ok {
   302  		return false
   303  	}
   304  	return forum.(*Forum).Name != ""
   305  }
   306  
   307  // TODO: Batch deletions with name blanking? Is this necessary?
   308  func (s *MemoryForumStore) CacheDelete(id int) {
   309  	s.forums.Delete(id)
   310  	s.rebuildView()
   311  }
   312  
   313  // TODO: Add a hook to allow plugin_guilds to detect when one of it's forums has just been deleted?
   314  func (s *MemoryForumStore) Delete(id int) error {
   315  	if id == ReportForumID {
   316  		return ErrNoDeleteReports
   317  	}
   318  	_, err := s.delete.Exec(id)
   319  	s.CacheDelete(id)
   320  	return err
   321  }
   322  
   323  func (s *MemoryForumStore) AddTopic(tid, uid, fid int) error {
   324  	_, err := s.updateCache.Exec(tid, uid, fid)
   325  	if err != nil {
   326  		return err
   327  	}
   328  	_, err = s.addTopics.Exec(1, fid)
   329  	if err != nil {
   330  		return err
   331  	}
   332  	// TODO: Bypass the database and update this with a lock or an unsafe atomic swap
   333  	return s.Reload(fid)
   334  }
   335  
   336  func (s *MemoryForumStore) RefreshTopic(fid int) (err error) {
   337  	var tid int
   338  	err = s.lastTopic.QueryRow(fid).Scan(&tid)
   339  	if err == sql.ErrNoRows {
   340  		f, err := s.CacheGet(fid)
   341  		if err != nil || f.LastTopicID != 0 {
   342  			_, err = s.updateCache.Exec(0, 0, fid)
   343  			if err != nil {
   344  				return err
   345  			}
   346  			s.Reload(fid)
   347  		}
   348  		return nil
   349  	}
   350  	if err != nil {
   351  		return err
   352  	}
   353  
   354  	topic, err := Topics.Get(tid)
   355  	if err != nil {
   356  		return err
   357  	}
   358  	_, err = s.updateCache.Exec(tid, topic.CreatedBy, fid)
   359  	if err != nil {
   360  		return err
   361  	}
   362  	// TODO: Bypass the database and update this with a lock or an unsafe atomic swap
   363  	s.Reload(fid)
   364  	return nil
   365  }
   366  
   367  // TODO: Make this update more atomic
   368  func (s *MemoryForumStore) RemoveTopic(fid int) error {
   369  	_, err := s.removeTopics.Exec(1, fid)
   370  	if err != nil {
   371  		return err
   372  	}
   373  	return s.RefreshTopic(fid)
   374  }
   375  func (s *MemoryForumStore) RemoveTopics(fid, count int) error {
   376  	_, err := s.removeTopics.Exec(count, fid)
   377  	if err != nil {
   378  		return err
   379  	}
   380  	return s.RefreshTopic(fid)
   381  }
   382  
   383  // DEPRECATED. forum.Update() will be the way to do this in the future, once it's completed
   384  // TODO: Have a pointer to the last topic rather than storing it on the forum itself
   385  func (s *MemoryForumStore) UpdateLastTopic(tid, uid, fid int) error {
   386  	_, err := s.updateCache.Exec(tid, uid, fid)
   387  	if err != nil {
   388  		return err
   389  	}
   390  	// TODO: Bypass the database and update this with a lock or an unsafe atomic swap
   391  	return s.Reload(fid)
   392  }
   393  
   394  func (s *MemoryForumStore) Create(name, desc string, active bool, preset string) (int, error) {
   395  	if name == "" {
   396  		return 0, ErrBlankName
   397  	}
   398  	forumCreateMutex.Lock()
   399  	defer forumCreateMutex.Unlock()
   400  
   401  	res, err := s.create.Exec(name, desc, active, preset)
   402  	if err != nil {
   403  		return 0, err
   404  	}
   405  
   406  	fid64, err := res.LastInsertId()
   407  	if err != nil {
   408  		return 0, err
   409  	}
   410  	fid := int(fid64)
   411  
   412  	err = s.Reload(fid)
   413  	if err != nil {
   414  		return 0, err
   415  	}
   416  
   417  	PermmapToQuery(PresetToPermmap(preset), fid)
   418  	return fid, nil
   419  }
   420  
   421  // TODO: Make this atomic, maybe with a transaction?
   422  func (s *MemoryForumStore) UpdateOrder(updateMap map[int]int) error {
   423  	for fid, order := range updateMap {
   424  		_, err := s.updateOrder.Exec(order, fid)
   425  		if err != nil {
   426  			return err
   427  		}
   428  	}
   429  	return s.LoadForums()
   430  }
   431  
   432  // ! Might be slightly inaccurate, if the sync.Map is constantly shifting and churning, but it'll stabilise eventually. Also, slow. Don't use this on every request x.x
   433  // Length returns the number of forums in the memory cache
   434  func (s *MemoryForumStore) Length() (len int) {
   435  	s.forums.Range(func(_, _ interface{}) bool {
   436  		len++
   437  		return true
   438  	})
   439  	return len
   440  }
   441  
   442  // TODO: Get the total count of forums in the forum store rather than doing a heavy query for this?
   443  // Count returns the total number of forums
   444  func (s *MemoryForumStore) Count() (count int) {
   445  	err := s.count.QueryRow().Scan(&count)
   446  	if err != nil {
   447  		LogError(err)
   448  	}
   449  	return count
   450  }
   451  
   452  // TODO: Work on SqlForumStore
   453  
   454  // TODO: Work on the NullForumStore