gosora/topic_store.go
Azareal 14a14b7e80 More work on Cosora, we have a screenshot of it up now, although it's super experimental at the moment.
This commit might be a little broken, another is coming to fix things up!

The topics list is now paginated.
Refactored the error handling system.
Added the Trumboyg WYSIWYG editor for Cosora.
Moved Delete() out of the TopicStore and into *Topic
You can now bulk delete and bulk lock topics on Cosora.
h1s are now formatted properly on Firefox.
Added more ARIA Labels.
SuperModOnly is now a piece of middleware for the Control Panel routes.
Refactored and extended the router generator.
Improved the SEO for the paginators.
Added bits of Microdata to improve SEO further.
Wrote benchmarks for users.Get() and users.BypassGet()
More errors are caught now.
You can now attach pcss files to posts.
Improved the error logging for JavaScript.
Topic list avatars now link to the associated profiles.
Added last poster avatars to the forum list.
2017-10-30 09:57:08 +00:00

352 lines
9.6 KiB
Go

/*
*
* Gosora Topic Store
* Copyright Azareal 2017 - 2018
*
*/
package main
import (
"database/sql"
"errors"
"log"
"strings"
"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
var ErrNoTitle = errors.New("This message is missing a title")
var ErrNoBody = errors.New("This message is missing a body")
type TopicStore interface {
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
// TODO: Implement these two methods
//Replies(tid int) ([]*Reply, error)
//RepliesRange(tid int, lower int, higher int) ([]*Reply, error)
GlobalCount() 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
Length() 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
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)
}
return &MemoryTopicStore{
items: make(map[int]*Topic),
capacity: capacity,
get: getStmt,
exists: existsStmt,
topicCount: topicCountStmt,
}
}
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
}
func (mts *MemoryTopicStore) Exists(id int) bool {
return mts.exists.QueryRow(id).Scan(&id) == nil
}
func (mts *MemoryTopicStore) Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error) {
topicName = strings.TrimSpace(topicName)
if topicName == "" {
return 0, ErrNoBody
}
content = strings.TrimSpace(content)
parsedContent := parseMessage(content, fid, "forums")
if strings.TrimSpace(parsedContent) == "" {
return 0, ErrNoBody
}
wcount := wordCount(content)
// TODO: Move this statement into the topic store
res, err := createTopicStmt.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
}
err = fstore.AddTopic(int(lastID), uid, fid)
return int(lastID), err
}
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()
}
// ! Is this concurrent?
// Length returns the number of topics in the memory cache
func (mts *MemoryTopicStore) Length() int {
return int(mts.length)
}
func (mts *MemoryTopicStore) SetCapacity(capacity int) {
mts.capacity = capacity
}
func (mts *MemoryTopicStore) GetCapacity() int {
return mts.capacity
}
// GlobalCount returns the total number of topics on these forums
func (mts *MemoryTopicStore) GlobalCount() 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
}
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)
}
return &SQLTopicStore{
get: getStmt,
exists: existsStmt,
topicCount: topicCountStmt,
}
}
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
}
func (sts *SQLTopicStore) Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error) {
topicName = strings.TrimSpace(topicName)
if topicName == "" {
return 0, ErrNoBody
}
content = strings.TrimSpace(content)
parsedContent := parseMessage(content, fid, "forums")
if strings.TrimSpace(parsedContent) == "" {
return 0, ErrNoBody
}
wcount := wordCount(content)
// TODO: Move this statement into the topic store
res, err := createTopicStmt.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
}
err = fstore.AddTopic(int(lastID), uid, fid)
return int(lastID), err
}
// ? - What're we going to do about this?
func (sts *SQLTopicStore) AddLastTopic(item *Topic, fid int) error {
// Coming Soon...
return nil
}
// GlobalCount returns the total number of topics on these forums
func (sts *SQLTopicStore) GlobalCount() int {
var tcount int
err := sts.topicCount.QueryRow().Scan(&tcount)
if err != nil {
LogError(err)
}
return tcount
}