gosora/common/topic_store.go

179 lines
5.9 KiB
Go

/*
*
* Gosora Topic Store
* Copyright Azareal 2017 - 2019
*
*/
package common
import (
"database/sql"
"errors"
"strings"
"github.com/Azareal/Gosora/query_gen"
)
// TODO: Add the watchdog goroutine
// TODO: Add BulkGetMap
// 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)
Exists(id int) bool
Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error)
AddLastTopic(item *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 int, lower int, higher int) ([]*Reply, error)
GlobalCount() int
SetCache(cache TopicCache)
GetCache() TopicCache
}
type DefaultTopicStore struct {
cache TopicCache
get *sql.Stmt
exists *sql.Stmt
topicCount *sql.Stmt
create *sql.Stmt
}
// NewDefaultTopicStore gives you a new instance of DefaultTopicStore
func NewDefaultTopicStore(cache TopicCache) (*DefaultTopicStore, error) {
acc := qgen.NewAcc()
if cache == nil {
cache = NewNullTopicCache()
}
return &DefaultTopicStore{
cache: cache,
get: acc.Select("topics").Columns("title, content, createdBy, createdAt, lastReplyAt, is_closed, sticky, parentID, ipaddress, views, postCount, likeCount, poll, data").Where("tid = ?").Prepare(),
exists: acc.Select("topics").Columns("tid").Where("tid = ?").Prepare(),
topicCount: acc.Count("topics").Prepare(),
create: acc.Insert("topics").Columns("parentID, title, content, parsed_content, createdAt, lastReplyAt, lastReplyBy, ipaddress, words, createdBy").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,?").Prepare(),
}, acc.FirstError()
}
func (mts *DefaultTopicStore) DirtyGet(id int) *Topic {
topic, err := mts.cache.Get(id)
if err == nil {
return topic
}
topic = &Topic{ID: id}
err = mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data)
if err == nil {
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
_ = mts.cache.Add(topic)
return topic
}
return BlankTopic()
}
// TODO: Log weird cache errors?
func (mts *DefaultTopicStore) Get(id int) (topic *Topic, err error) {
topic, err = mts.cache.Get(id)
if err == nil {
return topic, nil
}
topic = &Topic{ID: id}
err = mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data)
if err == nil {
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
_ = mts.cache.Add(topic)
}
return topic, err
}
// BypassGet will always bypass the cache and pull the topic directly from the database
func (mts *DefaultTopicStore) BypassGet(id int) (*Topic, error) {
topic := &Topic{ID: id}
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data)
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
return topic, err
}
func (mts *DefaultTopicStore) Reload(id int) error {
topic := &Topic{ID: id}
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data)
if err == nil {
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
_ = mts.cache.Set(topic)
} else {
_ = mts.cache.Remove(id)
}
return err
}
func (mts *DefaultTopicStore) Exists(id int) bool {
return mts.exists.QueryRow(id).Scan(&id) == nil
}
func (mts *DefaultTopicStore) Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error) {
if topicName == "" {
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(topicName) > Config.MaxTopicTitleLength {
return 0, ErrLongTitle
}
parsedContent := strings.TrimSpace(ParseMessage(content, fid, "forums"))
if parsedContent == "" {
return 0, ErrNoBody
}
wcount := WordCount(content)
// TODO: Move this statement into the topic store
res, err := mts.create.Exec(fid, topicName, content, parsedContent, uid, ipaddress, wcount, uid)
if err != nil {
return 0, err
}
lastID, err := res.LastInsertId()
if err != nil {
return 0, err
}
return int(lastID), Forums.AddTopic(int(lastID), uid, fid)
}
// ? - What is this? Do we need it? Should it be in the main store interface?
func (mts *DefaultTopicStore) AddLastTopic(item *Topic, fid int) error {
// Coming Soon...
return nil
}
// GlobalCount returns the total number of topics on these forums
func (mts *DefaultTopicStore) GlobalCount() (tcount int) {
err := mts.topicCount.QueryRow().Scan(&tcount)
if err != nil {
LogError(err)
}
return tcount
}
func (mts *DefaultTopicStore) SetCache(cache TopicCache) {
mts.cache = cache
}
// TODO: We're temporarily doing this so that you can do tcache != nil in getTopicUser. Refactor it.
func (mts *DefaultTopicStore) GetCache() TopicCache {
_, ok := mts.cache.(*NullTopicCache)
if ok {
return nil
}
return mts.cache
}