gosora/topic_store.go
Azareal 47963e10a9 Began work on support for JS Plugins.
Renamed the rrow_assign hook to topic_reply_row_assign and gave it access to more data.
Fixed a bug where the topic store wouldn't fetch the last reply time for a topic.
Refactored the process of adding and removing topics from and to a *Forum.
Fixed a bug where editing the opening post of a topic would yield a vast number of <br>s instead of blank lines.
Selecting text in Shadow now has it's own CSS instead of falling back onto the browser defaults.
Fixed a bug in Shadow where not all the headers filled up the space they should.
Fixed a bug in Shadow where the footer is broken on mobile.
Added an ARIA Label to the topic list.
Refactored the last poster logic to reduce the number of bugs.
Renamed ReplyShort to Reply and Reply to ReplyUser.
Added a Copy method to Reply, Group, Forum, User, and Topic.
Rewrote Hello World into something slightly more useful for new plugin devs to learn off.
Added the GetLength() method to ForumCache.
2017-09-28 23:16:34 +01:00

359 lines
9.4 KiB
Go

/*
*
* Gosora Topic Store
* Copyright Azareal 2017 - 2018
*
*/
package main
import (
"database/sql"
"log"
"sync"
"sync/atomic"
"./query_gen/lib"
)
// 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
type TopicStore interface {
Get(id int) (*Topic, error)
BypassGet(id int) (*Topic, error)
Delete(id int) error
Exists(id int) bool
AddLastTopic(item *Topic, fid int) error // unimplemented
// TODO: Implement these two methods
//GetReplies() ([]*Reply, error)
//GetRepliesRange(lower int, higher int) ([]*Reply, error)
GetGlobalCount() int
}
type TopicCache interface {
CacheGet(id int) (*Topic, error)
CacheGetUnsafe(id int) (*Topic, error)
CacheSet(item *Topic) error
CacheAdd(item *Topic) error
CacheAddUnsafe(item *Topic) error
CacheRemove(id int) error
CacheRemoveUnsafe(id int) error
Flush()
Reload(id int) error
GetLength() int
SetCapacity(capacity int)
GetCapacity() int
}
type MemoryTopicStore struct {
items map[int]*Topic
length int64 // sync/atomic only lets us operate on int32s and int64s
capacity int
get *sql.Stmt
exists *sql.Stmt
topicCount *sql.Stmt
delete *sql.Stmt
sync.RWMutex
}
// NewMemoryTopicStore gives you a new instance of MemoryTopicStore
func NewMemoryTopicStore(capacity int) *MemoryTopicStore {
getStmt, err := qgen.Builder.SimpleSelect("topics", "title, content, createdBy, createdAt, lastReplyAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, data", "tid = ?", "", "")
if err != nil {
log.Fatal(err)
}
existsStmt, err := qgen.Builder.SimpleSelect("topics", "tid", "tid = ?", "", "")
if err != nil {
log.Fatal(err)
}
topicCountStmt, err := qgen.Builder.SimpleCount("topics", "", "")
if err != nil {
log.Fatal(err)
}
deleteStmt, err := qgen.Builder.SimpleDelete("topics", "tid = ?")
if err != nil {
log.Fatal(err)
}
return &MemoryTopicStore{
items: make(map[int]*Topic),
capacity: capacity,
get: getStmt,
exists: existsStmt,
topicCount: topicCountStmt,
delete: deleteStmt,
}
}
func (mts *MemoryTopicStore) CacheGet(id int) (*Topic, error) {
mts.RLock()
item, ok := mts.items[id]
mts.RUnlock()
if ok {
return item, nil
}
return item, ErrNoRows
}
func (mts *MemoryTopicStore) CacheGetUnsafe(id int) (*Topic, error) {
item, ok := mts.items[id]
if ok {
return item, nil
}
return item, ErrNoRows
}
func (mts *MemoryTopicStore) Get(id int) (*Topic, error) {
mts.RLock()
topic, ok := mts.items[id]
mts.RUnlock()
if ok {
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.PostCount, &topic.LikeCount, &topic.Data)
if err == nil {
topic.Link = buildTopicURL(nameToSlug(topic.Title), id)
_ = mts.CacheAdd(topic)
}
return topic, err
}
// BypassGet will always bypass the cache and pull the topic directly from the database
func (mts *MemoryTopicStore) 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.PostCount, &topic.LikeCount, &topic.Data)
topic.Link = buildTopicURL(nameToSlug(topic.Title), id)
return topic, err
}
func (mts *MemoryTopicStore) 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.PostCount, &topic.LikeCount, &topic.Data)
if err == nil {
topic.Link = buildTopicURL(nameToSlug(topic.Title), id)
_ = mts.CacheSet(topic)
} else {
_ = mts.CacheRemove(id)
}
return err
}
// TODO: Use a transaction here
func (mts *MemoryTopicStore) Delete(id int) error {
topic, err := mts.Get(id)
if err != nil {
return nil // Already gone, maybe we should check for other errors here
}
topicCreator, err := users.Get(topic.CreatedBy)
if err == nil {
wcount := wordCount(topic.Content)
err = topicCreator.decreasePostStats(wcount, true)
if err != nil {
return err
}
} else if err != ErrNoRows {
return err
}
err = fstore.RemoveTopic(topic.ParentID)
if err != nil && err != ErrNoRows {
return err
}
mts.Lock()
mts.CacheRemoveUnsafe(id)
_, err = mts.delete.Exec(id)
mts.Unlock()
return err
}
func (mts *MemoryTopicStore) Exists(id int) bool {
return mts.exists.QueryRow(id).Scan(&id) == nil
}
func (mts *MemoryTopicStore) CacheSet(item *Topic) error {
mts.Lock()
_, ok := mts.items[item.ID]
if ok {
mts.items[item.ID] = item
} else if int(mts.length) >= mts.capacity {
mts.Unlock()
return ErrStoreCapacityOverflow
} else {
mts.items[item.ID] = item
atomic.AddInt64(&mts.length, 1)
}
mts.Unlock()
return nil
}
func (mts *MemoryTopicStore) CacheAdd(item *Topic) error {
if int(mts.length) >= mts.capacity {
return ErrStoreCapacityOverflow
}
mts.Lock()
mts.items[item.ID] = item
mts.Unlock()
atomic.AddInt64(&mts.length, 1)
return nil
}
// TODO: Make these length increments thread-safe. Ditto for the other DataStores
func (mts *MemoryTopicStore) CacheAddUnsafe(item *Topic) error {
if int(mts.length) >= mts.capacity {
return ErrStoreCapacityOverflow
}
mts.items[item.ID] = item
atomic.AddInt64(&mts.length, 1)
return nil
}
// TODO: Make these length decrements thread-safe. Ditto for the other DataStores
func (mts *MemoryTopicStore) CacheRemove(id int) error {
mts.Lock()
delete(mts.items, id)
mts.Unlock()
atomic.AddInt64(&mts.length, -1)
return nil
}
func (mts *MemoryTopicStore) CacheRemoveUnsafe(id int) error {
delete(mts.items, id)
atomic.AddInt64(&mts.length, -1)
return nil
}
// ? - What is this? Do we need it? Should it be in the main store interface?
func (mts *MemoryTopicStore) AddLastTopic(item *Topic, fid int) error {
// Coming Soon...
return nil
}
func (mts *MemoryTopicStore) Flush() {
mts.Lock()
mts.items = make(map[int]*Topic)
mts.length = 0
mts.Unlock()
}
func (mts *MemoryTopicStore) GetLength() int {
return int(mts.length)
}
func (mts *MemoryTopicStore) SetCapacity(capacity int) {
mts.capacity = capacity
}
func (mts *MemoryTopicStore) GetCapacity() int {
return mts.capacity
}
// Return the total number of topics on these forums
func (mts *MemoryTopicStore) GetGlobalCount() int {
var tcount int
err := mts.topicCount.QueryRow().Scan(&tcount)
if err != nil {
LogError(err)
}
return tcount
}
type SQLTopicStore struct {
get *sql.Stmt
exists *sql.Stmt
topicCount *sql.Stmt
delete *sql.Stmt
}
func NewSQLTopicStore() *SQLTopicStore {
getStmt, err := qgen.Builder.SimpleSelect("topics", "title, content, createdBy, createdAt, lastReplyAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, data", "tid = ?", "", "")
if err != nil {
log.Fatal(err)
}
existsStmt, err := qgen.Builder.SimpleSelect("topics", "tid", "tid = ?", "", "")
if err != nil {
log.Fatal(err)
}
topicCountStmt, err := qgen.Builder.SimpleCount("topics", "", "")
if err != nil {
log.Fatal(err)
}
deleteStmt, err := qgen.Builder.SimpleDelete("topics", "tid = ?")
if err != nil {
log.Fatal(err)
}
return &SQLTopicStore{
get: getStmt,
exists: existsStmt,
topicCount: topicCountStmt,
delete: deleteStmt,
}
}
func (sts *SQLTopicStore) Get(id int) (*Topic, error) {
topic := Topic{ID: id}
err := sts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
topic.Link = buildTopicURL(nameToSlug(topic.Title), id)
return &topic, err
}
// BypassGet is an alias of Get(), as we don't have a cache for SQLTopicStore
func (sts *SQLTopicStore) BypassGet(id int) (*Topic, error) {
topic := &Topic{ID: id}
err := sts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
topic.Link = buildTopicURL(nameToSlug(topic.Title), id)
return topic, err
}
func (sts *SQLTopicStore) Exists(id int) bool {
return sts.exists.QueryRow(id).Scan(&id) == nil
}
// TODO: Use a transaction here
func (sts *SQLTopicStore) Delete(id int) error {
topic, err := sts.Get(id)
if err != nil {
return nil // Already gone, maybe we should check for other errors here
}
topicCreator, err := users.Get(topic.CreatedBy)
if err == nil {
wcount := wordCount(topic.Content)
err = topicCreator.decreasePostStats(wcount, true)
if err != nil {
return err
}
} else if err != ErrNoRows {
return err
}
err = fstore.RemoveTopic(topic.ParentID)
if err != nil && err != ErrNoRows {
return err
}
_, err = sts.delete.Exec(id)
return err
}
// ? - What're we going to do about this?
func (sts *SQLTopicStore) AddLastTopic(item *Topic, fid int) error {
// Coming Soon...
return nil
}
// Return the total number of topics on these forums
func (sts *SQLTopicStore) GetGlobalCount() int {
var tcount int
err := sts.topicCount.QueryRow().Scan(&tcount)
if err != nil {
LogError(err)
}
return tcount
}