gosora/common/topic_store.go
Azareal 6870d242e9 Add ClearIPs() to TopicStore.
Add LockMany() to TopicStore.
Add RemoveMany() to TopicCache.
Add ClearIPs() to PollStore.
Add Purge() to RegLogStore.
Add DeleteOlderThanDays() to RegLogStore.
Add Purge() to LoginLogStore.
Add DeleteOlderThanDays() to LoginLogStore.
Add SetInt() to MetaStore.
Add SetInt64() to MetaStore.

Use Createf() in RegLogItem.Create()
Use Count() in SQLRegLogStore.Count()
Use Count() in SQLLoginLogStore.Count()
Use Countf() in SQLLoginLogStore.CountUser()

Add trailing triple dot parser test case.
Removed a block of commented code in gen router.
Reduce boilerplate.
2021-04-27 20:20:26 +10:00

328 lines
8.8 KiB
Go

/*
*
* Gosora Topic Store
* Copyright Azareal 2017 - 2020
*
*/
package common
import (
"database/sql"
"errors"
"strconv"
"strings"
qgen "github.com/Azareal/Gosora/query_gen"
)
// TODO: Add the watchdog goroutine
// TODO: Add some sort of update method
// ? - Should we add stick, lock, unstick, and unlock methods? These might be better on the Topics not the TopicStore
var Topics TopicStore
var ErrNoTitle = errors.New("This message is missing a title")
var ErrLongTitle = errors.New("The title is too long")
var ErrNoBody = errors.New("This message is missing a body")
type TopicStore interface {
DirtyGet(id int) *Topic
Get(id int) (*Topic, error)
BypassGet(id int) (*Topic, error)
BulkGetMap(ids []int) (list map[int]*Topic, err error)
Exists(id int) bool
Create(fid int, name, content string, uid int, ip string) (tid int, err error)
AddLastTopic(t *Topic, fid int) error // unimplemented
Reload(id int) error // Too much SQL logic to move into TopicCache
// TODO: Implement these two methods
//Replies(tid int) ([]*Reply, error)
//RepliesRange(tid, lower, higher int) ([]*Reply, error)
Count() int
CountUser(uid int) int
CountMegaUser(uid int) int
CountBigUser(uid int) int
ClearIPs() error
LockMany(tids []int) error
SetCache(cache TopicCache)
GetCache() TopicCache
}
type DefaultTopicStore struct {
cache TopicCache
get *sql.Stmt
exists *sql.Stmt
count *sql.Stmt
countUser *sql.Stmt
countWordUser *sql.Stmt
create *sql.Stmt
clearIPs *sql.Stmt
lockTen *sql.Stmt
}
// NewDefaultTopicStore gives you a new instance of DefaultTopicStore
func NewDefaultTopicStore(cache TopicCache) (*DefaultTopicStore, error) {
acc := qgen.NewAcc()
if cache == nil {
cache = NewNullTopicCache()
}
t := "topics"
return &DefaultTopicStore{
cache: cache,
get: acc.Select(t).Columns("title,content,createdBy,createdAt,lastReplyBy,lastReplyAt,lastReplyID,is_closed,sticky,parentID,ip,views,postCount,likeCount,attachCount,poll,data").Where("tid=?").Stmt(),
exists: acc.Exists(t, "tid").Stmt(),
count: acc.Count(t).Stmt(),
countUser: acc.Count(t).Where("createdBy=?").Stmt(),
countWordUser: acc.Count(t).Where("createdBy=? AND words>=?").Stmt(),
create: acc.Insert(t).Columns("parentID,title,content,parsed_content,createdAt,lastReplyAt,lastReplyBy,ip,words,createdBy").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,?").Prepare(),
clearIPs: acc.Update(t).Set("ip=''").Where("ip!=''").Stmt(),
lockTen: acc.Update(t).Set("is_closed=1").Where("tid IN(" + inqbuild2(10) + ")").Stmt(),
}, acc.FirstError()
}
func (s *DefaultTopicStore) DirtyGet(id int) *Topic {
t, e := s.cache.Get(id)
if e == nil {
return t
}
t, e = s.BypassGet(id)
if e == nil {
_ = s.cache.Set(t)
return t
}
return BlankTopic()
}
// TODO: Log weird cache errors?
func (s *DefaultTopicStore) Get(id int) (t *Topic, e error) {
t, e = s.cache.Get(id)
if e == nil {
return t, nil
}
t, e = s.BypassGet(id)
if e == nil {
_ = s.cache.Set(t)
}
return t, e
}
// BypassGet will always bypass the cache and pull the topic directly from the database
func (s *DefaultTopicStore) BypassGet(id int) (*Topic, error) {
t := &Topic{ID: id}
e := s.get.QueryRow(id).Scan(&t.Title, &t.Content, &t.CreatedBy, &t.CreatedAt, &t.LastReplyBy, &t.LastReplyAt, &t.LastReplyID, &t.IsClosed, &t.Sticky, &t.ParentID, &t.IP, &t.ViewCount, &t.PostCount, &t.LikeCount, &t.AttachCount, &t.Poll, &t.Data)
if e == nil {
t.Link = BuildTopicURL(NameToSlug(t.Title), id)
}
return t, e
}
/*func (s *DefaultTopicStore) GetByUser(uid int) (list map[int]*Topic, err error) {
t := &Topic{ID: id}
err := s.get.QueryRow(id).Scan(&t.Title, &t.Content, &t.CreatedBy, &t.CreatedAt, &t.LastReplyBy, &t.LastReplyAt, &t.LastReplyID, &t.IsClosed, &t.Sticky, &t.ParentID, &t.IP, &t.ViewCount, &t.PostCount, &t.LikeCount, &t.AttachCount, &t.Poll, &t.Data)
if err == nil {
t.Link = BuildTopicURL(NameToSlug(t.Title), id)
}
return t, err
}*/
// TODO: Avoid duplicating much of this logic from user_store.go
func (s *DefaultTopicStore) BulkGetMap(ids []int) (list map[int]*Topic, e error) {
idCount := len(ids)
list = make(map[int]*Topic)
if idCount == 0 {
return list, nil
}
var stillHere []int
sliceList := s.cache.BulkGet(ids)
if len(sliceList) > 0 {
for i, sliceItem := range sliceList {
if sliceItem != nil {
list[sliceItem.ID] = sliceItem
} else {
stillHere = append(stillHere, ids[i])
}
}
ids = stillHere
}
// If every user is in the cache, then return immediately
if len(ids) == 0 {
return list, nil
} else if len(ids) == 1 {
t, e := s.Get(ids[0])
if e != nil {
return list, e
}
list[t.ID] = t
return list, nil
}
idList, q := inqbuild(ids)
rows, e := qgen.NewAcc().Select("topics").Columns("tid,title,content,createdBy,createdAt,lastReplyBy,lastReplyAt,lastReplyID,is_closed,sticky,parentID,ip,views,postCount,likeCount,attachCount,poll,data").Where("tid IN(" + q + ")").Query(idList...)
if e != nil {
return list, e
}
defer rows.Close()
for rows.Next() {
t := &Topic{}
e := rows.Scan(&t.ID, &t.Title, &t.Content, &t.CreatedBy, &t.CreatedAt, &t.LastReplyBy, &t.LastReplyAt, &t.LastReplyID, &t.IsClosed, &t.Sticky, &t.ParentID, &t.IP, &t.ViewCount, &t.PostCount, &t.LikeCount, &t.AttachCount, &t.Poll, &t.Data)
if e != nil {
return list, e
}
t.Link = BuildTopicURL(NameToSlug(t.Title), t.ID)
_ = s.cache.Set(t)
list[t.ID] = t
}
if e = rows.Err(); e != nil {
return list, e
}
// Did we miss any topics?
if idCount > len(list) {
var sidList string
for i, id := range ids {
if _, ok := list[id]; !ok {
if i == 0 {
sidList += strconv.Itoa(id)
} else {
sidList += ","+strconv.Itoa(id)
}
}
}
if sidList != "" {
e = errors.New("Unable to find topics with the following IDs: " + sidList)
}
}
return list, e
}
func (s *DefaultTopicStore) Reload(id int) error {
t, e := s.BypassGet(id)
if e == nil {
_ = s.cache.Set(t)
} else {
_ = s.cache.Remove(id)
}
TopicListThaw.Thaw()
return e
}
func (s *DefaultTopicStore) Exists(id int) bool {
return s.exists.QueryRow(id).Scan(&id) == nil
}
func (s *DefaultTopicStore) ClearIPs() error {
_, e := s.clearIPs.Exec()
return e
}
func (s *DefaultTopicStore) LockMany(tids []int) (e error) {
tc, i := Topics.GetCache(), 0
singles := func() error {
for ; i < len(tids); i++ {
_, e := topicStmts.lock.Exec(tids[i])
if e != nil {
return e
}
}
return nil
}
if len(tids) < 10 {
if e = singles(); e != nil {
return e
}
if tc != nil {
_ = tc.RemoveMany(tids)
}
TopicListThaw.Thaw()
return nil
}
for ; (i + 10) < len(tids); i += 10 {
_, e := s.lockTen.Exec(tids[i], tids[i+1], tids[i+2], tids[i+3], tids[i+4], tids[i+5], tids[i+6], tids[i+7], tids[i+8], tids[i+9])
if e != nil {
return e
}
}
if e = singles(); e != nil {
return e
}
if tc != nil {
_ = tc.RemoveMany(tids)
}
TopicListThaw.Thaw()
return nil
}
func (s *DefaultTopicStore) Create(fid int, name, content string, uid int, ip string) (tid int, err error) {
if name == "" {
return 0, ErrNoTitle
}
// ? This number might be a little screwy with Unicode, but it's the only consistent thing we have, as Unicode characters can be any number of bytes in theory?
if len(name) > Config.MaxTopicTitleLength {
return 0, ErrLongTitle
}
parsedContent := strings.TrimSpace(ParseMessage(content, fid, "forums", nil, nil))
if parsedContent == "" {
return 0, ErrNoBody
}
// TODO: Move this statement into the topic store
if Config.DisablePostIP {
ip = ""
}
res, err := s.create.Exec(fid, name, content, parsedContent, uid, ip, WordCount(content), uid)
if err != nil {
return 0, err
}
lastID, err := res.LastInsertId()
if err != nil {
return 0, err
}
tid = int(lastID)
//TopicListThaw.Thaw() // redundant
return tid, Forums.AddTopic(tid, uid, fid)
}
// ? - What is this? Do we need it? Should it be in the main store interface?
func (s *DefaultTopicStore) AddLastTopic(t *Topic, fid int) error {
// Coming Soon...
return nil
}
// Count returns the total number of topics on these forums
func (s *DefaultTopicStore) Count() (count int) {
return Countf(s.count)
}
func (s *DefaultTopicStore) CountUser(uid int) (count int) {
return Countf(s.countUser, uid)
}
func (s *DefaultTopicStore) CountMegaUser(uid int) (count int) {
return Countf(s.countWordUser, uid, SettingBox.Load().(SettingMap)["megapost_min_words"].(int))
}
func (s *DefaultTopicStore) CountBigUser(uid int) (count int) {
return Countf(s.countWordUser, uid, SettingBox.Load().(SettingMap)["bigpost_min_words"].(int))
}
func (s *DefaultTopicStore) SetCache(cache TopicCache) {
s.cache = cache
}
// TODO: We're temporarily doing this so that you can do tcache != nil in getTopicUser. Refactor it.
func (s *DefaultTopicStore) GetCache() TopicCache {
_, ok := s.cache.(*NullTopicCache)
if ok {
return nil
}
return s.cache
}