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.
This commit is contained in:
Azareal 2017-09-28 23:16:34 +01:00
parent f5d5f755bb
commit 47963e10a9
39 changed files with 653 additions and 356 deletions

View File

@ -1,8 +1,8 @@
# Gosora [![Azareal's Discord Chat](https://img.shields.io/badge/style-Invite-7289DA.svg?style=flat&label=Discord)](https://discord.gg/eyYvtTf)
A super fast forum software written in Go.
A super fast forum software written in Go. You can talk to us on our Discord chat!
The initial code-base was forked from one of my side projects, but has now gone far beyond that. We're still fairly early in development, so the code-base might change at an incredible rate. We plan to start stabilising it somewhat once we enter alpha.
The initial code-base was forked from one of my side projects, but has now gone far beyond that. We're still fairly early in development, so the code-base might change at an incredible rate. We plan to stop making as many breaking changes once we release the first alpha.
If you like this software, please give it a star and give us some feedback :)

View File

@ -25,6 +25,16 @@ func initDatabase() (err error) {
return err
}
// We have to put this here, otherwise LoadForums() won't be able to get the last poster data when building it's forums
log.Print("Initialising the user and topic stores")
if config.CacheTopicUser == CACHE_STATIC {
users = NewMemoryUserStore(config.UserCacheCapacity)
topics = NewMemoryTopicStore(config.TopicCacheCapacity)
} else {
users = NewSQLUserStore()
topics = NewSQLTopicStore()
}
log.Print("Loading the forums.")
fstore = NewMemoryForumStore()
err = fstore.LoadForums()
@ -45,7 +55,7 @@ func initDatabase() (err error) {
}
log.Print("Loading the plugins.")
err = LoadPlugins()
err = initExtend()
if err != nil {
return err
}

View File

@ -1,47 +0,0 @@
/* Copyright Azareal 2016 - 2017 */
package main
import "github.com/robertkrimen/otto"
var vm *Otto
var js_plugins map[string]*otto.Script = make(map[string]*otto.Script)
var js_vars map[string]*otto.Object = make(map[string]*otto.Object)
func init()
{
var err error
vm = otto.New()
js_vars["current_page"], err = vm.Object(`current_page = {}`)
if err != nil {
log.Fatal(err)
}
}
func js_add_plugin(plugin string) error
{
script, err := otto.Compile("./extend/" + plugin + ".js")
if err != nil {
return err
}
vm.Run(script)
return nil
}
func js_add_hook(hook string, plugin string)
{
hooks[hook] = func(data interface{}) interface{} {
switch d := data.(type) {
case Page:
current_page := js_vars["current_page"]
current_page.Set("Title", d.Title)
case TopicPage:
case ProfilePage:
case Reply:
default:
log.Print("Not a valid JS datatype")
}
}
}

View File

@ -6,8 +6,10 @@
*/
package main
import "log"
import "net/http"
import (
"log"
"net/http"
)
var plugins = make(map[string]*Plugin)
@ -15,7 +17,6 @@ var plugins = make(map[string]*Plugin)
var hooks = map[string][]func(interface{}) interface{}{
"forums_frow_assign": nil,
"topic_create_frow_assign": nil,
"rrow_assign": nil, // TODO: Rename this hook to topic_rrow_assign
}
// Hooks with a variable number of arguments
@ -26,6 +27,7 @@ var vhooks = map[string]func(...interface{}) interface{}{
"forum_trow_assign": nil,
"topics_topic_row_assign": nil,
//"topics_user_row_assign": nil,
"topic_reply_row_assign": nil,
"create_group_preappend": nil, // What is this? Investigate!
"topic_create_pre_loop": nil,
}
@ -100,6 +102,15 @@ type Plugin struct {
Uninstall func() error
Hooks map[string]int
Data interface{} // Usually used for hosting the VMs / reusable elements of non-native plugins
}
func initExtend() (err error) {
err = InitPluginLangs()
if err != nil {
return err
}
return LoadPlugins()
}
// LoadPlugins polls the database to see which plugins have been activated and which have been installed
@ -111,8 +122,7 @@ func LoadPlugins() error {
defer rows.Close()
var uname string
var active bool
var installed bool
var active, installed bool
for rows.Next() {
err = rows.Scan(&uname, &active, &installed)
if err != nil {

View File

@ -0,0 +1,5 @@
current_page.test = true;
// This shouldn't ever fail
var errmsg = "gotcha";
errmsg;

View File

@ -0,0 +1,7 @@
{
"UName":"heytherejs",
"Name":"HeythereJS",
"Author":"Azareal",
"URL":"https://github.com/Azareal/Gosora",
"Main":"main.js"
}

View File

@ -19,23 +19,26 @@ type ForumAdmin struct {
}
type Forum struct {
ID int
Link string
Name string
Desc string
Active bool
Preset string
ParentID int
ParentType string
TopicCount int
LastTopicLink string
LastTopic string
ID int
Link string
Name string
Desc string
Active bool
Preset string
ParentID int
ParentType string
TopicCount int
LastTopic *Topic
LastTopicID int
LastReplyer string
LastReplyer *User
LastReplyerID int
LastTopicTime string
LastTopicTime string // So that we can re-calculate the relative time on the spot in /forums/
//LastLock sync.RWMutex // ? - Is this safe to copy? Use a pointer to it? Should we do an fstore.Reload() instead?
}
// ? - What is this for?
type ForumSimple struct {
ID int
Name string
@ -43,6 +46,36 @@ type ForumSimple struct {
Preset string
}
func (forum *Forum) Copy() (fcopy Forum) {
//forum.LastLock.RLock()
fcopy = *forum
//forum.LastLock.RUnlock()
return fcopy
}
/*func (forum *Forum) GetLast() (topic *Topic, user *User) {
forum.LastLock.RLock()
topic = forum.LastTopic
if topic == nil {
topic = &Topic{ID: 0}
}
user = forum.LastReplyer
if user == nil {
user = &User{ID: 0}
}
forum.LastLock.RUnlock()
return topic, user
}
func (forum *Forum) SetLast(topic *Topic, user *User) {
forum.LastLock.Lock()
forum.LastTopic = topic
forum.LastReplyer = user
forum.LastLock.Unlock()
}*/
// TODO: Write tests for this
func (forum *Forum) Update(name string, desc string, active bool, preset string) error {
if name == "" {
name = forum.Name
@ -53,9 +86,13 @@ func (forum *Forum) Update(name string, desc string, active bool, preset string)
return err
}
if forum.Preset != preset || preset == "custom" || preset == "" {
permmapToQuery(presetToPermmap(preset), forum.ID)
err = permmapToQuery(presetToPermmap(preset), forum.ID)
if err != nil {
return err
}
}
_ = fstore.Reload(forum.ID)
return nil
}
// TODO: Replace this sorting mechanism with something a lot more efficient
@ -72,6 +109,11 @@ func (sf SortForum) Less(i, j int) bool {
return sf[i].ID < sf[j].ID
}
// ! Don't use this outside of tests and possibly template_init.go
func makeDummyForum(fid int, link string, name string, desc string, active bool, preset string, parentID int, parentType string, topicCount int) *Forum {
return &Forum{ID: fid, Link: link, Name: name, Desc: desc, Active: active, Preset: preset, ParentID: parentID, ParentType: parentType, TopicCount: topicCount}
}
func buildForumURL(slug string, fid int) string {
if slug == "" {
return "/forum/" + strconv.Itoa(fid)

View File

@ -27,14 +27,13 @@ type ForumStore interface {
LoadForums() error
DirtyGet(id int) *Forum
Get(id int) (*Forum, error)
GetCopy(id int) (Forum, error)
BypassGet(id int) (*Forum, error)
Reload(id int) error // ? - Should we move this to ForumCache? It might require us to do some unnecessary casting though
//Update(Forum) error
Delete(id int) error
IncrementTopicCount(id int) error
DecrementTopicCount(id int) error
UpdateLastTopic(topicName string, tid int, username string, uid int, time string, fid int) error
AddTopic(tid int, uid int, fid int) error
RemoveTopic(fid int) error
UpdateLastTopic(tid int, uid int, fid int) error
Exists(id int) bool
GetAll() ([]*Forum, error)
GetAllIDs() ([]int, error)
@ -51,6 +50,7 @@ type ForumCache interface {
CacheGet(id int) (*Forum, error)
CacheSet(forum *Forum) error
CacheDelete(id int)
GetLength() int
}
// 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
@ -58,7 +58,6 @@ type MemoryForumStore struct {
forums sync.Map // map[int]*Forum
forumView atomic.Value // []*Forum
//fids []int
forumCount int
get *sql.Stmt
getAll *sql.Stmt
@ -94,10 +93,6 @@ func NewMemoryForumStore() *MemoryForumStore {
// TODO: Add support for subforums
func (mfs *MemoryForumStore) LoadForums() error {
log.Print("Adding the uncategorised forum")
forumUpdateMutex.Lock()
defer forumUpdateMutex.Unlock()
var forumView []*Forum
addForum := func(forum *Forum) {
mfs.forums.Store(forum.ID, forum)
@ -114,8 +109,8 @@ func (mfs *MemoryForumStore) LoadForums() error {
var i = 0
for ; rows.Next(); i++ {
forum := Forum{ID: 0, Active: true, Preset: "all"}
err = rows.Scan(&forum.ID, &forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopic, &forum.LastTopicID, &forum.LastReplyer, &forum.LastReplyerID, &forum.LastTopicTime)
forum := &Forum{ID: 0, Active: true, Preset: "all"}
err = rows.Scan(&forum.ID, &forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.ParentID, &forum.ParentType, &forum.TopicCount, &forum.LastTopicID, &forum.LastReplyerID)
if err != nil {
return err
}
@ -129,15 +124,27 @@ func (mfs *MemoryForumStore) LoadForums() error {
}
forum.Link = buildForumURL(nameToSlug(forum.Name), forum.ID)
forum.LastTopicLink = buildTopicURL(nameToSlug(forum.LastTopic), forum.LastTopicID)
addForum(&forum)
topic, err := topics.Get(forum.LastTopicID)
if err != nil {
topic = getDummyTopic()
}
user, err := users.Get(forum.LastReplyerID)
if err != nil {
user = getDummyUser()
}
forum.LastTopic = topic
forum.LastReplyer = user
//forum.SetLast(topic, user)
addForum(forum)
}
mfs.forumCount = i
mfs.forumView.Store(forumView)
return rows.Err()
}
// TODO: Hide social groups too
// ? - Will this be hit a lot by plugin_socialgroups?
func (mfs *MemoryForumStore) rebuildView() {
var forumView []*Forum
mfs.forums.Range(func(_ interface{}, value interface{}) bool {
@ -173,46 +180,75 @@ func (mfs *MemoryForumStore) Get(id int) (*Forum, error) {
if !ok || fint.(*Forum).Name == "" {
var forum = &Forum{ID: id}
err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.TopicCount, &forum.LastTopic, &forum.LastTopicID, &forum.LastReplyer, &forum.LastReplyerID, &forum.LastTopicTime)
if err != nil {
return forum, err
}
forum.Link = buildForumURL(nameToSlug(forum.Name), forum.ID)
forum.LastTopicLink = buildTopicURL(nameToSlug(forum.LastTopic), forum.LastTopicID)
topic, err := topics.Get(forum.LastTopicID)
if err != nil {
topic = getDummyTopic()
}
user, err := users.Get(forum.LastReplyerID)
if err != nil {
user = getDummyUser()
}
forum.LastTopic = topic
forum.LastReplyer = user
//forum.SetLast(topic, user)
mfs.CacheSet(forum)
return forum, err
}
return fint.(*Forum), nil
}
func (mfs *MemoryForumStore) GetCopy(id int) (Forum, error) {
fint, ok := mfs.forums.Load(id)
if !ok || fint.(*Forum).Name == "" {
var forum = Forum{ID: id}
err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.TopicCount, &forum.LastTopic, &forum.LastTopicID, &forum.LastReplyer, &forum.LastReplyerID, &forum.LastTopicTime)
forum.Link = buildForumURL(nameToSlug(forum.Name), forum.ID)
forum.LastTopicLink = buildTopicURL(nameToSlug(forum.LastTopic), forum.LastTopicID)
return forum, err
}
return *fint.(*Forum), nil
}
func (mfs *MemoryForumStore) BypassGet(id int) (*Forum, error) {
var forum = Forum{ID: id}
var forum = &Forum{ID: id}
err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.TopicCount, &forum.LastTopic, &forum.LastTopicID, &forum.LastReplyer, &forum.LastReplyerID, &forum.LastTopicTime)
if err != nil {
return nil, err
}
forum.Link = buildForumURL(nameToSlug(forum.Name), forum.ID)
forum.LastTopicLink = buildTopicURL(nameToSlug(forum.LastTopic), forum.LastTopicID)
return &forum, err
topic, err := topics.Get(forum.LastTopicID)
if err != nil {
topic = getDummyTopic()
}
user, err := users.Get(forum.LastReplyerID)
if err != nil {
user = getDummyUser()
}
forum.LastTopic = topic
forum.LastReplyer = user
//forum.SetLast(topic, user)
return forum, err
}
func (mfs *MemoryForumStore) Reload(id int) error {
var forum = Forum{ID: id}
var forum = &Forum{ID: id}
err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.TopicCount, &forum.LastTopic, &forum.LastTopicID, &forum.LastReplyer, &forum.LastReplyerID, &forum.LastTopicTime)
if err != nil {
return err
}
forum.Link = buildForumURL(nameToSlug(forum.Name), forum.ID)
forum.LastTopicLink = buildTopicURL(nameToSlug(forum.LastTopic), forum.LastTopicID)
mfs.CacheSet(&forum)
topic, err := topics.Get(forum.LastTopicID)
if err != nil {
topic = getDummyTopic()
}
user, err := users.Get(forum.LastReplyerID)
if err != nil {
user = getDummyUser()
}
forum.LastTopic = topic
forum.LastReplyer = user
//forum.SetLast(topic, user)
mfs.CacheSet(forum)
return nil
}
@ -281,8 +317,6 @@ func (mfs *MemoryForumStore) Delete(id int) error {
if id == 1 {
return errors.New("You cannot delete the Reports forum")
}
forumUpdateMutex.Lock()
defer forumUpdateMutex.Unlock()
_, err := mfs.delete.Exec(id)
if err != nil {
return err
@ -291,53 +325,40 @@ func (mfs *MemoryForumStore) Delete(id int) error {
return nil
}
// ! Is this racey?
func (mfs *MemoryForumStore) IncrementTopicCount(id int) error {
forum, err := mfs.Get(id)
func (mfs *MemoryForumStore) AddTopic(tid int, uid int, fid int) error {
_, err := updateForumCacheStmt.Exec(tid, uid, fid)
if err != nil {
return err
}
_, err = addTopicsToForumStmt.Exec(1, id)
_, err = addTopicsToForumStmt.Exec(1, fid)
if err != nil {
return err
}
forum.TopicCount++
// TODO: Bypass the database and update this with a lock or an unsafe atomic swap
mfs.Reload(fid)
return nil
}
// ! Is this racey?
func (mfs *MemoryForumStore) DecrementTopicCount(id int) error {
forum, err := mfs.Get(id)
// TODO: Update the forum cache with the latest topic
func (mfs *MemoryForumStore) RemoveTopic(fid int) error {
_, err := removeTopicsFromForumStmt.Exec(1, fid)
if err != nil {
return err
}
_, err = removeTopicsFromForumStmt.Exec(1, id)
if err != nil {
return err
}
forum.TopicCount--
// TODO: Bypass the database and update this with a lock or an unsafe atomic swap
mfs.Reload(fid)
return nil
}
// DEPRECATED. forum.Update() will be the way to do this in the future, once it's completed
// TODO: Have a pointer to the last topic rather than storing it on the forum itself
// ! Is this racey?
func (mfs *MemoryForumStore) UpdateLastTopic(topicName string, tid int, username string, uid int, time string, fid int) error {
forum, err := mfs.Get(fid)
func (mfs *MemoryForumStore) UpdateLastTopic(tid int, uid int, fid int) error {
_, err := updateForumCacheStmt.Exec(tid, uid, fid)
if err != nil {
return err
}
_, err = updateForumCacheStmt.Exec(topicName, tid, username, uid, fid)
if err != nil {
return err
}
forum.LastTopic = topicName
forum.LastTopicID = tid
forum.LastReplyer = username
forum.LastReplyerID = uid
forum.LastTopicTime = time
// TODO: Bypass the database and update this with a lock or an unsafe atomic swap
mfs.Reload(fid)
return nil
}
@ -354,19 +375,25 @@ func (mfs *MemoryForumStore) Create(forumName string, forumDesc string, active b
}
fid := int(fid64)
mfs.forums.Store(fid, &Forum{fid, buildForumURL(nameToSlug(forumName), fid), forumName, forumDesc, active, preset, 0, "", 0, "", "", 0, "", 0, ""})
mfs.forumCount++
err = mfs.Reload(fid)
if err != nil {
return 0, err
}
// TODO: Add a GroupStore. How would it interact with the ForumStore?
permmapToQuery(presetToPermmap(preset), fid)
forumCreateMutex.Unlock()
if active {
mfs.rebuildView()
}
return fid, nil
}
// ! 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
func (mfs *MemoryForumStore) GetLength() (length int) {
mfs.forums.Range(func(_ interface{}, value interface{}) bool {
length++
return true
})
return length
}
// TODO: Get the total count of forums in the forum store minus the blanked forums rather than doing a heavy query for this?
// GetGlobalCount returns the total number of forums
func (mfs *MemoryForumStore) GetGlobalCount() (fcount int) {

View File

@ -186,7 +186,7 @@ func _gen_mysql() (err error) {
}
log.Print("Preparing getForums statement.")
getForumsStmt, err = db.Prepare("SELECT `fid`,`name`,`desc`,`active`,`preset`,`parentID`,`parentType`,`topicCount`,`lastTopic`,`lastTopicID`,`lastReplyer`,`lastReplyerID`,`lastTopicTime` FROM `forums` ORDER BY fid ASC")
getForumsStmt, err = db.Prepare("SELECT `fid`,`name`,`desc`,`active`,`preset`,`parentID`,`parentType`,`topicCount`,`lastTopicID`,`lastReplyerID` FROM `forums` ORDER BY fid ASC")
if err != nil {
return err
}
@ -534,7 +534,7 @@ func _gen_mysql() (err error) {
}
log.Print("Preparing updateForumCache statement.")
updateForumCacheStmt, err = db.Prepare("UPDATE `forums` SET `lastTopic` = ?,`lastTopicID` = ?,`lastReplyer` = ?,`lastReplyerID` = ?,`lastTopicTime` = UTC_TIMESTAMP() WHERE `fid` = ?")
updateForumCacheStmt, err = db.Prepare("UPDATE `forums` SET `lastTopicID` = ?,`lastReplyerID` = ? WHERE `fid` = ?")
if err != nil {
return err
}

View File

@ -80,7 +80,7 @@ func _gen_pgsql() (err error) {
}
log.Print("Preparing updateForumCache statement.")
updateForumCacheStmt, err = db.Prepare("UPDATE `forums` SET `lastTopic` = ?,`lastTopicID` = ?,`lastReplyer` = ?,`lastReplyerID` = ?,`lastTopicTime` = LOCALTIMESTAMP() WHERE `fid` = ?")
updateForumCacheStmt, err = db.Prepare("UPDATE `forums` SET `lastTopicID` = ?,`lastReplyerID` = ? WHERE `fid` = ?")
if err != nil {
return err
}

View File

@ -53,14 +53,6 @@ func gloinit() error {
log.Fatal(err)
}
if config.CacheTopicUser == CACHE_STATIC {
users = NewMemoryUserStore(config.UserCacheCapacity)
topics = NewMemoryTopicStore(config.TopicCacheCapacity)
} else {
users = NewSQLUserStore()
topics = NewSQLTopicStore()
}
log.Print("Loading the static files.")
err = initStaticFiles()
if err != nil {
@ -548,7 +540,7 @@ func BenchmarkQueriesSerial(b *testing.B) {
}
})
var replyItem Reply
var replyItem ReplyUser
var isSuperAdmin bool
var group int
b.Run("topic_replies_scan", func(b *testing.B) {

View File

@ -11,6 +11,7 @@ type GroupAdmin struct {
CanDelete bool
}
// ! Fix the data races
type Group struct {
ID int
Name string
@ -25,3 +26,7 @@ type Group struct {
Forums []ForumPerms
CanSee []int // The IDs of the forums this group can see
}
func (group *Group) Copy() Group {
return *group
}

View File

@ -82,14 +82,6 @@ func main() {
log.Fatal(err)
}
if config.CacheTopicUser == CACHE_STATIC {
users = NewMemoryUserStore(config.UserCacheCapacity)
topics = NewMemoryTopicStore(config.TopicCacheCapacity)
} else {
users = NewSQLUserStore()
topics = NewSQLTopicStore()
}
log.Print("Loading the static files.")
err = initStaticFiles()
if err != nil {

View File

@ -10,7 +10,6 @@ import (
"regexp"
"strconv"
"strings"
"time"
)
// ? - Should we add a new permission or permission zone (like per-forum permissions) specifically for profile comment creation
@ -79,7 +78,7 @@ func routeTopicCreate(w http.ResponseWriter, r *http.Request, user User, sfid st
// Do a bulk forum fetch, just in case it's the SqlForumStore?
forum := fstore.DirtyGet(ffid)
if forum.Name != "" && forum.Active {
fcopy := *forum
fcopy := forum.Copy()
if hooks["topic_create_frow_assign"] != nil {
// TODO: Add the skip feature to all the other row based hooks?
if runHook("topic_create_frow_assign", &fcopy).(bool) {
@ -144,12 +143,6 @@ func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) {
return
}
err = fstore.IncrementTopicCount(fid)
if err != nil {
InternalError(err, w)
return
}
_, err = addSubscriptionStmt.Exec(user.ID, lastID, "topic")
if err != nil {
InternalError(err, w)
@ -163,7 +156,7 @@ func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) {
return
}
err = fstore.UpdateLastTopic(topicName, int(lastID), user.Name, user.ID, time.Now().Format("2006-01-02 15:04:05"), fid)
err = fstore.AddTopic(int(lastID), user.ID, fid)
if err != nil && err != ErrNoRows {
InternalError(err, w)
}
@ -219,7 +212,14 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) {
InternalError(err, w)
return
}
err = fstore.UpdateLastTopic(topic.Title, tid, user.Name, user.ID, time.Now().Format("2006-01-02 15:04:05"), topic.ParentID)
// Flush the topic out of the cache
tcache, ok := topics.(TopicCache)
if ok {
tcache.CacheRemove(tid)
}
err = fstore.UpdateLastTopic(tid, user.ID, topic.ParentID)
if err != nil && err != ErrNoRows {
InternalError(err, w)
return
@ -247,12 +247,6 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) {
go notifyWatchers(lastID)
}
// Flush the topic out of the cache
tcache, ok := topics.(TopicCache)
if ok {
tcache.CacheRemove(tid)
}
http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther)
err = user.increasePostStats(wcount, false)
if err != nil {
@ -629,7 +623,7 @@ func routeReportSubmit(w http.ResponseWriter, r *http.Request, user User, sitemI
InternalError(err, w)
return
}
err = fstore.UpdateLastTopic(title, int(lastID), user.Name, user.ID, time.Now().Format("2006-01-02 15:04:05"), fid)
err = fstore.UpdateLastTopic(int(lastID), user.ID, fid)
if err != nil && err != ErrNoRows {
InternalError(err, w)
return

View File

@ -269,19 +269,12 @@ func TestForumStore(t *testing.T) {
}
forum, err = fstore.Get(0)
if err == ErrNoRows {
t.Error("Couldn't find FID #0")
} else if err != nil {
if err == nil {
t.Error("FID #0 shouldn't exist")
} else if err != ErrNoRows {
t.Fatal(err)
}
if forum.ID != 0 {
t.Error("forum.ID doesn't not match the requested UID. Got '" + strconv.Itoa(forum.ID) + "' instead.")
}
if forum.Name != "Uncategorised" {
t.Error("FID #0 is named '" + forum.Name + "' and not 'Uncategorised'")
}
forum, err = fstore.Get(1)
if err == ErrNoRows {
t.Error("Couldn't find FID #1")
@ -311,8 +304,8 @@ func TestForumStore(t *testing.T) {
}
ok = fstore.Exists(0)
if !ok {
t.Error("FID #0 should exist")
if ok {
t.Error("FID #0 shouldn't exist")
}
ok = fstore.Exists(1)

View File

@ -4,6 +4,7 @@ import (
//"log"
//"fmt"
"html"
"log"
"net"
"net/http"
"strconv"
@ -26,7 +27,7 @@ func routeEditTopic(w http.ResponseWriter, r *http.Request, user User) {
return
}
oldTopic, err := topics.Get(tid)
topic, err := topics.Get(tid)
if err == ErrNoRows {
PreErrorJSQ("The topic you tried to edit doesn't exist.", w, r, isJs)
return
@ -36,7 +37,7 @@ func routeEditTopic(w http.ResponseWriter, r *http.Request, user User) {
}
// TODO: Add hooks to make use of headerLite
_, ok := SimpleForumUserCheck(w, r, &user, oldTopic.ParentID)
_, ok := SimpleForumUserCheck(w, r, &user, topic.ParentID)
if !ok {
return
}
@ -47,25 +48,20 @@ func routeEditTopic(w http.ResponseWriter, r *http.Request, user User) {
topicName := r.PostFormValue("topic_name")
topicContent := html.EscapeString(r.PostFormValue("topic_content"))
log.Print("topicContent ", topicContent)
// TODO: Move this bit to the TopicStore
_, err = editTopicStmt.Exec(topicName, preparseMessage(topicContent), parseMessage(html.EscapeString(preparseMessage(topicContent))), tid)
err = topic.Update(topicName, topicContent)
if err != nil {
InternalErrorJSQ(err, w, r, isJs)
return
}
err = fstore.UpdateLastTopic(topicName, tid, user.Name, user.ID, time.Now().Format("2006-01-02 15:04:05"), oldTopic.ParentID)
err = fstore.UpdateLastTopic(topic.ID, user.ID, topic.ParentID)
if err != nil && err != ErrNoRows {
InternalError(err, w)
InternalErrorJSQ(err, w, r, isJs)
return
}
tcache, ok := topics.(TopicCache)
if ok {
tcache.CacheRemove(oldTopic.ID)
}
if !isJs {
http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther)
} else {

91
module_ottojs.go Normal file
View File

@ -0,0 +1,91 @@
/*
*
* OttoJS Plugin Module
* Copyright Azareal 2016 - 2018
*
*/
package main
import (
"errors"
"github.com/robertkrimen/otto"
)
type OttoPluginLang struct {
vm *otto.Otto
plugins map[string]*otto.Script
vars map[string]*otto.Object
}
func init() {
pluginLangs["ottojs"] = &OttoPluginLang{
plugins: make(map[string]*otto.Script),
vars: make(map[string]*otto.Object),
}
}
func (js *OttoPluginLang) Init() (err error) {
js.vm = otto.New()
js.vars["current_page"], err = js.vm.Object(`var current_page = {}`)
return err
}
func (js *OttoPluginLang) GetName() string {
return "ottojs"
}
func (js *OttoPluginLang) GetExts() []string {
return []string{".js"}
}
func (js *OttoPluginLang) AddPlugin(meta PluginMeta) (plugin *Plugin, err error) {
script, err := js.vm.Compile("./extend/"+meta.UName+"/"+meta.Main, nil)
if err != nil {
return nil, err
}
var pluginInit = func() error {
retValue, err := js.vm.Run(script)
if err != nil {
return err
}
if retValue.IsString() {
ret, err := retValue.ToString()
if err != nil {
return err
}
if ret != "" {
return errors.New(ret)
}
}
return nil
}
var pluginActivate func() error
var pluginDeactivate func()
var pluginInstall func() error
var pluginUninstall func() error
plugin = NewPlugin(meta.UName, meta.Name, meta.Author, meta.URL, meta.Settings, meta.Tag, "ottojs", pluginInit, pluginActivate, pluginDeactivate, pluginInstall, pluginUninstall)
plugin.Data = script
return plugin, nil
}
/*func (js *OttoPluginLang) addHook(hook string, plugin string) {
hooks[hook] = func(data interface{}) interface{} {
switch d := data.(type) {
case Page:
currentPage := js.vars["current_page"]
currentPage.Set("Title", d.Title)
case TopicPage:
case ProfilePage:
case Reply:
default:
log.Print("Not a valid JS datatype")
}
}
}*/

View File

@ -26,11 +26,8 @@ CREATE TABLE `forums`(
`preset` varchar(100) DEFAULT '' not null,
`parentID` int DEFAULT 0 not null, /* TODO: Add support for subforums */
`parentType` varchar(50) DEFAULT '' not null,
`lastTopic` varchar(100) DEFAULT '' not null,
`lastTopicID` int DEFAULT 0 not null,
`lastReplyer` varchar(100) DEFAULT '' not null,
`lastReplyerID` int DEFAULT 0 not null,
`lastTopicTime` datetime not null,
primary key(`fid`)
) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci;
@ -233,7 +230,7 @@ INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`) VALUES ('Awaiting
INSERT INTO users_groups(`name`,`permissions`,`plugin_perms`,`tag`) VALUES ('Not Loggedin','{"ViewTopic":true}','{}','Guest');
INSERT INTO forums(`name`,`active`) VALUES ('Reports',0);
INSERT INTO forums(`name`,`lastTopicTime`,`lastTopicID`,`lastReplyer`,`lastReplyerID`,`lastTopic`) VALUES ('General',UTC_TIMESTAMP(),1,"Admin",1,'Test Topic');
INSERT INTO forums(`name`,`lastTopicID`,`lastReplyerID`) VALUES ("General",1,1);
INSERT INTO forums_permissions(`gid`,`fid`,`permissions`) VALUES (1,1,'{"ViewTopic":true,"CreateReply":true,"CreateTopic":true,"PinTopic":true,"CloseTopic":true}');
INSERT INTO forums_permissions(`gid`,`fid`,`permissions`) VALUES (2,1,'{"ViewTopic":true,"CreateReply":true,"CloseTopic":true}');

View File

@ -54,7 +54,7 @@ type TopicPage struct {
Title string
CurrentUser User
Header *HeaderVars
ItemList []Reply
ItemList []ReplyUser
Topic TopicUser
Page int
LastPage int
@ -72,7 +72,7 @@ type ForumPage struct {
CurrentUser User
Header *HeaderVars
ItemList []*TopicsRow
Forum Forum
Forum *Forum
Page int
LastPage int
}
@ -88,7 +88,7 @@ type ProfilePage struct {
Title string
CurrentUser User
Header *HeaderVars
ItemList []Reply
ItemList []ReplyUser
ProfileOwner User
}

View File

@ -1,23 +0,0 @@
package main
func init() {
plugins["helloworld"] = NewPlugin("helloworld", "Hello World", "Azareal", "http://github.com/Azareal", "", "", "", initHelloworld, nil, deactivateHelloworld, nil, nil)
}
// init_helloworld is separate from init() as we don't want the plugin to run if the plugin is disabled
func initHelloworld() error {
plugins["helloworld"].AddHook("rrow_assign", helloworldReply)
return nil
}
func deactivateHelloworld() {
plugins["helloworld"].RemoveHook("rrow_assign", helloworldReply)
}
func helloworldReply(data interface{}) interface{} {
reply := data.(*Reply)
reply.Content = "Hello World!"
reply.ContentHtml = "Hello World!"
reply.Tag = "Auto"
return nil
}

24
plugin_heythere.go Normal file
View File

@ -0,0 +1,24 @@
package main
func init() {
plugins["heythere"] = NewPlugin("heythere", "Hey There", "Azareal", "http://github.com/Azareal", "", "", "", initHeythere, nil, deactivateHeythere, nil, nil)
}
// init_heythere is separate from init() as we don't want the plugin to run if the plugin is disabled
func initHeythere() error {
plugins["heythere"].AddHook("topic_reply_row_assign", heythereReply)
return nil
}
func deactivateHeythere() {
plugins["heythere"].RemoveHook("topic_reply_row_assign", heythereReply)
}
func heythereReply(data ...interface{}) interface{} {
currentUser := data[0].(*TopicPage).CurrentUser
reply := data[1].(*ReplyUser)
reply.Content = "Hey there, " + currentUser.Name + "!"
reply.ContentHtml = "Hey there, " + currentUser.Name + "!"
reply.Tag = "Auto"
return nil
}

View File

@ -56,7 +56,7 @@ type SocialGroupPage struct {
CurrentUser User
Header *HeaderVars
ItemList []*TopicsRow
Forum Forum
Forum *Forum
SocialGroup *SocialGroup
Page int
LastPage int

116
pluginlangs.go Normal file
View File

@ -0,0 +1,116 @@
package main
import (
"encoding/json"
"errors"
"io/ioutil"
"path/filepath"
)
var pluginLangs = make(map[string]PluginLang)
// For non-native plugins to bind JSON files to. E.g. JS and Lua
type PluginMeta struct {
UName string
Name string
Author string
URL string
Settings string
Tag string
Main string // The main file
Hooks map[string]string // Hooks mapped to functions
}
type PluginLang interface {
GetName() string
GetExts() []string
Init() error
AddPlugin(meta PluginMeta) (*Plugin, error)
//AddHook(name string, handler interface{}) error
//RemoveHook(name string, handler interface{})
//RunHook(name string, data interface{}) interface{}
//RunVHook(name string data ...interface{}) interface{}
}
/*
var ext = filepath.Ext(pluginFile.Name())
if ext == ".txt" || ext == ".go" {
continue
}
*/
func InitPluginLangs() error {
for _, pluginLang := range pluginLangs {
pluginLang.Init()
}
pluginList, err := GetPluginFiles()
if err != nil {
return err
}
for _, pluginItem := range pluginList {
pluginFile, err := ioutil.ReadFile("./extend/" + pluginItem + "/plugin.json")
if err != nil {
return err
}
var plugin PluginMeta
err = json.Unmarshal(pluginFile, &plugin)
if err != nil {
return err
}
if plugin.UName == "" {
return errors.New("The UName field must not be blank on plugin '" + pluginItem + "'")
}
if plugin.Name == "" {
return errors.New("The Name field must not be blank on plugin '" + pluginItem + "'")
}
if plugin.Author == "" {
return errors.New("The Author field must not be blank on plugin '" + pluginItem + "'")
}
if plugin.Main == "" {
return errors.New("Couldn't find a main file for plugin '" + pluginItem + "'")
}
var ext = filepath.Ext(plugin.Main)
pluginLang, err := ExtToPluginLang(ext)
if err != nil {
return err
}
pplugin, err := pluginLang.AddPlugin(plugin)
if err != nil {
return err
}
plugins[plugin.UName] = pplugin
}
return nil
}
func GetPluginFiles() (pluginList []string, err error) {
pluginFiles, err := ioutil.ReadDir("./extend")
if err != nil {
return nil, err
}
for _, pluginFile := range pluginFiles {
if !pluginFile.IsDir() {
continue
}
pluginList = append(pluginList, pluginFile.Name())
}
return pluginList, nil
}
func ExtToPluginLang(ext string) (PluginLang, error) {
for _, pluginLang := range pluginLangs {
for _, registeredExt := range pluginLang.GetExts() {
if registeredExt == ext {
return pluginLang, nil
}
}
}
return nil, errors.New("No plugin lang handlers are capable of handling extension '" + ext + "'")
}

View File

@ -215,7 +215,7 @@ func write_selects(adapter qgen.DB_Adapter) error {
adapter.SimpleSelect("getGroups", "users_groups", "gid, name, permissions, plugin_perms, is_mod, is_admin, is_banned, tag", "", "", "")
adapter.SimpleSelect("getForums", "forums", "fid, name, desc, active, preset, parentID, parentType, topicCount, lastTopic, lastTopicID, lastReplyer, lastReplyerID, lastTopicTime", "", "fid ASC", "")
adapter.SimpleSelect("getForums", "forums", "fid, name, desc, active, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID", "", "fid ASC", "")
adapter.SimpleSelect("getForumsPermissions", "forums_permissions", "gid, fid, permissions", "", "gid ASC, fid ASC", "")
@ -358,7 +358,7 @@ func write_updates(adapter qgen.DB_Adapter) error {
adapter.SimpleUpdate("removeTopicsFromForum", "forums", "topicCount = topicCount - ?", "fid = ?")
adapter.SimpleUpdate("updateForumCache", "forums", "lastTopic = ?, lastTopicID = ?, lastReplyer = ?, lastReplyerID = ?, lastTopicTime = UTC_TIMESTAMP()", "fid = ?")
adapter.SimpleUpdate("updateForumCache", "forums", "lastTopicID = ?, lastReplyerID = ?", "fid = ?")
adapter.SimpleUpdate("addLikesToTopic", "topics", "likeCount = likeCount + ?", "tid = ?")

View File

@ -8,8 +8,7 @@ package main
// ? - Should we add a reply store to centralise all the reply logic? Would this cover profile replies too or would that be seperate?
type Reply struct /* Should probably rename this to ReplyUser and rename ReplyShort to Reply */
{
type ReplyUser struct {
ID int
ParentID int
Content string
@ -36,7 +35,7 @@ type Reply struct /* Should probably rename this to ReplyUser and rename ReplySh
ActionIcon string
}
type ReplyShort struct {
type Reply struct {
ID int
ParentID int
Content string
@ -51,14 +50,18 @@ type ReplyShort struct {
LikeCount int
}
func getReply(id int) (*ReplyShort, error) {
reply := ReplyShort{ID: id}
func (reply *Reply) Copy() Reply {
return *reply
}
func getReply(id int) (*Reply, error) {
reply := Reply{ID: id}
err := getReplyStmt.QueryRow(id).Scan(&reply.ParentID, &reply.Content, &reply.CreatedBy, &reply.CreatedAt, &reply.LastEdit, &reply.LastEditBy, &reply.IPAddress, &reply.LikeCount)
return &reply, err
}
func getUserReply(id int) (*ReplyShort, error) {
reply := ReplyShort{ID: id}
func getUserReply(id int) (*Reply, error) {
reply := Reply{ID: id}
err := getUserReplyStmt.QueryRow(id).Scan(&reply.ParentID, &reply.Content, &reply.CreatedBy, &reply.CreatedAt, &reply.LastEdit, &reply.LastEditBy, &reply.IPAddress)
return &reply, err
}

View File

@ -159,6 +159,7 @@ func routeTopics(w http.ResponseWriter, r *http.Request, user User) {
return
}
// TODO: Make CanSee a method on *Group with a canSee field?
var canSee []int
if user.IsSuperAdmin {
canSee, err = fstore.GetAllVisibleIDs()
@ -379,7 +380,7 @@ func routeForum(w http.ResponseWriter, r *http.Request, user User, sfid string)
topicItem.LastUser = userList[topicItem.LastReplyBy]
}
pi := ForumPage{forum.Name, user, headerVars, topicList, *forum, page, lastPage}
pi := ForumPage{forum.Name, user, headerVars, topicList, forum, page, lastPage}
if preRenderHooks["pre_render_view_forum"] != nil {
if runPreRenderHook("pre_render_view_forum", w, r, &user, &pi) {
return
@ -417,16 +418,22 @@ func routeForums(w http.ResponseWriter, r *http.Request, user User) {
}
for _, fid := range canSee {
//log.Print(forums[fid])
var forum = *fstore.DirtyGet(fid)
// Avoid data races by copying the struct into something we can freely mold without worrying about breaking something somewhere else
var forum = fstore.DirtyGet(fid).Copy()
if forum.ParentID == 0 && forum.Name != "" && forum.Active {
if forum.LastTopicID != 0 {
forum.LastTopicTime, err = relativeTime(forum.LastTopicTime)
if err != nil {
InternalError(err, w)
//topic, user := forum.GetLast()
//if topic.ID != 0 && user.ID != 0 {
if forum.LastTopic.ID != 0 && forum.LastReplyer.ID != 0 {
forum.LastTopicTime, err = relativeTime(forum.LastTopic.LastReplyAt)
if err != nil {
InternalError(err, w)
return
}
} else {
forum.LastTopicTime = ""
}
} else {
forum.LastTopic = "None"
forum.LastTopicTime = ""
}
if hooks["forums_frow_assign"] != nil {
@ -448,7 +455,7 @@ func routeForums(w http.ResponseWriter, r *http.Request, user User) {
func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
var err error
var page, offset int
var replyList []Reply
var replyList []ReplyUser
page, _ = strconv.Atoi(r.FormValue("page"))
@ -465,7 +472,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
}
// Get the topic...
topic, err := getTopicuser(tid)
topic, err := getTopicUser(tid)
if err == ErrNoRows {
NotFound(w, r)
return
@ -488,7 +495,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
BuildWidgets("view_topic", &topic, headerVars, r)
topic.Content = parseMessage(topic.Content)
topic.ContentHTML = parseMessage(topic.Content)
topic.ContentLines = strings.Count(topic.Content, "\n")
// We don't want users posting in locked topics...
@ -543,6 +550,8 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
page = 1
}
tpage := TopicPage{topic.Title, user, headerVars, replyList, topic, page, lastPage}
// Get the replies..
rows, err := getTopicRepliesOffsetStmt.Query(topic.ID, offset, config.ItemsPerPage)
if err == ErrNoRows {
@ -554,7 +563,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
}
defer rows.Close()
replyItem := Reply{ClassName: ""}
replyItem := ReplyUser{ClassName: ""}
for rows.Next() {
err := rows.Scan(&replyItem.ID, &replyItem.Content, &replyItem.CreatedBy, &replyItem.CreatedAt, &replyItem.LastEdit, &replyItem.LastEditBy, &replyItem.Avatar, &replyItem.CreatedByName, &replyItem.Group, &replyItem.URLPrefix, &replyItem.URLName, &replyItem.Level, &replyItem.IPAddress, &replyItem.LikeCount, &replyItem.ActionType)
if err != nil {
@ -628,9 +637,8 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
}
replyItem.Liked = false
// TODO: Rename this to topic_rrow_assign
if hooks["rrow_assign"] != nil {
runHook("rrow_assign", &replyItem)
if vhooks["topic_reply_row_assign"] != nil {
runVhook("topic_reply_row_assign", &tpage, &replyItem)
}
replyList = append(replyList, replyItem)
}
@ -640,7 +648,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
return
}
tpage := TopicPage{topic.Title, user, headerVars, replyList, topic, page, lastPage}
tpage.ItemList = replyList
if preRenderHooks["pre_render_view_topic"] != nil {
if runPreRenderHook("pre_render_view_topic", w, r, &user, &tpage) {
return
@ -658,7 +666,7 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user User) {
var err error
var replyContent, replyCreatedByName, replyCreatedAt, replyAvatar, replyTag, replyClassName string
var rid, replyCreatedBy, replyLastEdit, replyLastEditBy, replyLines, replyGroup int
var replyList []Reply
var replyList []ReplyUser
// SEO URLs...
halves := strings.Split(r.URL.Path[len("/user/"):], ".")
@ -736,7 +744,7 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user User) {
// TODO: Add a hook here
replyList = append(replyList, Reply{rid, puser.ID, replyContent, parseMessage(replyContent), replyCreatedBy, buildProfileURL(nameToSlug(replyCreatedByName), replyCreatedBy), replyCreatedByName, replyGroup, replyCreatedAt, replyLastEdit, replyLastEditBy, replyAvatar, replyClassName, replyLines, replyTag, "", "", "", 0, "", replyLiked, replyLikeCount, "", ""})
replyList = append(replyList, ReplyUser{rid, puser.ID, replyContent, parseMessage(replyContent), replyCreatedBy, buildProfileURL(nameToSlug(replyCreatedByName), replyCreatedBy), replyCreatedByName, replyGroup, replyCreatedAt, replyLastEdit, replyLastEditBy, replyAvatar, replyClassName, replyLines, replyTag, "", "", "", 0, "", replyLiked, replyLikeCount, "", ""})
}
err = rows.Err()
if err != nil {

View File

@ -73,7 +73,7 @@ w.Write(forums_0)
if len(tmpl_forums_vars.ItemList) != 0 {
for _, item := range tmpl_forums_vars.ItemList {
w.Write(forums_1)
if item.Desc != "" || item.LastTopicTime != "" {
if item.Desc != "" || item.LastTopic.Title != "" {
w.Write(forums_2)
}
w.Write(forums_3)
@ -93,21 +93,25 @@ w.Write([]byte(item.Name))
w.Write(forums_10)
}
w.Write(forums_11)
w.Write([]byte(item.LastTopicLink))
w.Write([]byte(item.LastTopic.Link))
w.Write(forums_12)
w.Write([]byte(item.LastTopic))
if item.LastTopic.Title != "" {
w.Write([]byte(item.LastTopic.Title))
} else {
w.Write(forums_13)
if item.LastTopicTime != "" {
w.Write(forums_14)
w.Write([]byte(item.LastTopicTime))
w.Write(forums_15)
}
w.Write(forums_14)
if item.LastTopicTime != "" {
w.Write(forums_15)
w.Write([]byte(item.LastTopicTime))
w.Write(forums_16)
}
} else {
w.Write(forums_17)
}
} else {
w.Write(forums_18)
}
w.Write(forums_19)
w.Write(footer_0)
if len(tmpl_forums_vars.Header.Themes) != 0 {
for _, item := range tmpl_forums_vars.Header.Themes {

View File

@ -106,9 +106,9 @@ func compileTemplates() error {
log.Print("Compiling the templates")
topic := TopicUser{1, "blah", "Blah", "Hey there!", 0, false, false, "Date", "Date", 0, "", "127.0.0.1", 0, 1, "classname", "weird-data", buildProfileURL("fake-user", 62), "Fake User", config.DefaultGroup, "", 0, "", "", "", "", 58, false}
var replyList []Reply
replyList = append(replyList, Reply{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", config.DefaultGroup, "", 0, 0, "", "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""})
topic := TopicUser{1, "blah", "Blah", "Hey there!", 0, false, false, "Date", "Date", 0, "", "127.0.0.1", 0, 1, "classname", "weird-data", buildProfileURL("fake-user", 62), "Fake User", config.DefaultGroup, "", 0, "", "", "", "", "", 58, false}
var replyList []ReplyUser
replyList = append(replyList, ReplyUser{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", config.DefaultGroup, "", 0, 0, "", "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""})
var varList = make(map[string]VarItem)
tpage := TopicPage{"Title", user, headerVars, replyList, topic, 1, 1}
@ -135,6 +135,7 @@ func compileTemplates() error {
}
for _, forum := range forums {
//log.Printf("*forum %+v\n", *forum)
forumList = append(forumList, *forum)
}
varList = make(map[string]VarItem)
@ -154,7 +155,7 @@ func compileTemplates() error {
//var topicList []TopicUser
//topicList = append(topicList,TopicUser{1,"topic-title","Topic Title","The topic content.",1,false,false,"Date","Date",1,"","127.0.0.1",0,1,"classname","","admin-fred","Admin Fred",config.DefaultGroup,"",0,"","","","",58,false})
forumItem := Forum{1, "general", "General Forum", "Where the general stuff happens", true, "all", 0, "", 0, "", "", 0, "", 0, ""}
forumItem := makeDummyForum(1, "general-forum.1", "General Forum", "Where the general stuff happens", true, "all", 0, "", 0)
forumPage := ForumPage{"General Forum", user, headerVars, topicsList, forumItem, 1, 1}
forumTmpl, err := c.compileTemplate("forum.html", "templates/", "ForumPage", forumPage, varList)
if err != nil {

View File

@ -185,50 +185,52 @@ var topic_63 = []byte(`), url(/static/post-avatar-bg.jpg);background-position: 0
var topic_64 = []byte(`-1`)
var topic_65 = []byte(`0px;background-repeat:no-repeat, repeat-y;`)
var topic_66 = []byte(`">
`)
var topic_67 = []byte(`
<p class="editable_block user_content" style="margin:0;padding:0;">`)
var topic_67 = []byte(`</p>
var topic_68 = []byte(`</p>
<span class="controls">
<a href="`)
var topic_68 = []byte(`" class="username real_username">`)
var topic_69 = []byte(`</a>&nbsp;&nbsp;
var topic_69 = []byte(`" class="username real_username">`)
var topic_70 = []byte(`</a>&nbsp;&nbsp;
`)
var topic_70 = []byte(`<a href="/reply/like/submit/`)
var topic_71 = []byte(`" class="mod_button" title="Love it" style="color:#202020;"><button class="username like_label"`)
var topic_72 = []byte(` style="background-color:#D6FFD6;"`)
var topic_73 = []byte(`></button></a>`)
var topic_74 = []byte(`<a href="/reply/edit/submit/`)
var topic_75 = []byte(`" class="mod_button" title="Edit Reply"><button class="username edit_item edit_label"></button></a>`)
var topic_76 = []byte(`<a href="/reply/delete/submit/`)
var topic_77 = []byte(`" class="mod_button" title="Delete Reply"><button class="username delete_item trash_label"></button></a>`)
var topic_78 = []byte(`<a class="mod_button" href='/users/ips/?ip=`)
var topic_79 = []byte(`' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>`)
var topic_80 = []byte(`
var topic_71 = []byte(`<a href="/reply/like/submit/`)
var topic_72 = []byte(`" class="mod_button" title="Love it" style="color:#202020;"><button class="username like_label"`)
var topic_73 = []byte(` style="background-color:#D6FFD6;"`)
var topic_74 = []byte(`></button></a>`)
var topic_75 = []byte(`<a href="/reply/edit/submit/`)
var topic_76 = []byte(`" class="mod_button" title="Edit Reply"><button class="username edit_item edit_label"></button></a>`)
var topic_77 = []byte(`<a href="/reply/delete/submit/`)
var topic_78 = []byte(`" class="mod_button" title="Delete Reply"><button class="username delete_item trash_label"></button></a>`)
var topic_79 = []byte(`<a class="mod_button" href='/users/ips/?ip=`)
var topic_80 = []byte(`' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>`)
var topic_81 = []byte(`
<a href="/report/submit/`)
var topic_81 = []byte(`?session=`)
var topic_82 = []byte(`&type=reply" class="mod_button report_item" title="Flag Reply"><button class="username report_item flag_label"></button></a>
var topic_82 = []byte(`?session=`)
var topic_83 = []byte(`&type=reply" class="mod_button report_item" title="Flag Reply"><button class="username report_item flag_label"></button></a>
`)
var topic_83 = []byte(`<a class="username hide_on_micro like_count">`)
var topic_84 = []byte(`</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>`)
var topic_85 = []byte(`<a class="username hide_on_micro user_tag">`)
var topic_86 = []byte(`</a>`)
var topic_87 = []byte(`<a class="username hide_on_micro level">`)
var topic_88 = []byte(`</a><a class="username hide_on_micro level_label" style="float:right;" title="Level"></a>`)
var topic_89 = []byte(`
var topic_84 = []byte(`<a class="username hide_on_micro like_count">`)
var topic_85 = []byte(`</a><a class="username hide_on_micro like_count_label" title="Like Count"></a>`)
var topic_86 = []byte(`<a class="username hide_on_micro user_tag">`)
var topic_87 = []byte(`</a>`)
var topic_88 = []byte(`<a class="username hide_on_micro level">`)
var topic_89 = []byte(`</a><a class="username hide_on_micro level_label" style="float:right;" title="Level"></a>`)
var topic_90 = []byte(`
</span>
</article>
`)
var topic_90 = []byte(`</div>
var topic_91 = []byte(`</div>
`)
var topic_91 = []byte(`
var topic_92 = []byte(`
<div class="rowblock topic_reply_form">
<form action="/reply/create/" method="post">
<input name="tid" value='`)
var topic_92 = []byte(`' type="hidden" />
var topic_93 = []byte(`' type="hidden" />
<div class="formrow real_first_child">
<div class="formitem"><textarea name="reply-content" placeholder="Insert reply here" required></textarea></div>
</div>
@ -238,7 +240,7 @@ var topic_92 = []byte(`' type="hidden" />
</form>
</div>
`)
var topic_93 = []byte(`
var topic_94 = []byte(`
</main>
@ -632,17 +634,18 @@ var forums_11 = []byte(`
<span style="float: right;">
<a href="`)
var forums_12 = []byte(`" style="float: right;font-size: 14px;">`)
var forums_13 = []byte(`</a>
var forums_13 = []byte(`None`)
var forums_14 = []byte(`</a>
`)
var forums_14 = []byte(`<br /><span class="rowsmall">`)
var forums_15 = []byte(`</span>`)
var forums_16 = []byte(`
var forums_15 = []byte(`<br /><span class="rowsmall">`)
var forums_16 = []byte(`</span>`)
var forums_17 = []byte(`
</span>
<div style="clear: both;"></div>
</div>
`)
var forums_17 = []byte(`<div class="rowitem passive">You don't have access to any forums.</div>`)
var forums_18 = []byte(`
var forums_18 = []byte(`<div class="rowitem passive">You don't have access to any forums.</div>`)
var forums_19 = []byte(`
</div>
</main>
@ -653,7 +656,7 @@ var topics_0 = []byte(`
<div class="rowblock rowhead">
<div class="rowitem"><h1>Topic List</h1></div>
</div>
<div id="topic_list" class="rowblock topic_list">
<div id="topic_list" class="rowblock topic_list" aria-label="The main topic list">
`)
var topics_1 = []byte(`<div class="rowitem topic_left passive datarow `)
var topics_2 = []byte(`topic_sticky`)

View File

@ -123,7 +123,7 @@ w.Write(topic_22)
w.Write(topic_23)
}
w.Write(topic_24)
w.Write([]byte(tmpl_topic_vars.Topic.Content))
w.Write([]byte(tmpl_topic_vars.Topic.ContentHTML))
w.Write(topic_25)
w.Write([]byte(tmpl_topic_vars.Topic.Content))
w.Write(topic_26)
@ -219,66 +219,67 @@ w.Write(topic_64)
w.Write(topic_65)
}
w.Write(topic_66)
w.Write([]byte(item.ContentHtml))
w.Write(topic_67)
w.Write([]byte(item.UserLink))
w.Write([]byte(item.ContentHtml))
w.Write(topic_68)
w.Write([]byte(item.CreatedByName))
w.Write([]byte(item.UserLink))
w.Write(topic_69)
if tmpl_topic_vars.CurrentUser.Perms.LikeItem {
w.Write([]byte(item.CreatedByName))
w.Write(topic_70)
w.Write([]byte(strconv.Itoa(item.ID)))
if tmpl_topic_vars.CurrentUser.Perms.LikeItem {
w.Write(topic_71)
if item.Liked {
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_72)
}
if item.Liked {
w.Write(topic_73)
}
if tmpl_topic_vars.CurrentUser.Perms.EditReply {
w.Write(topic_74)
w.Write([]byte(strconv.Itoa(item.ID)))
}
if tmpl_topic_vars.CurrentUser.Perms.EditReply {
w.Write(topic_75)
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_76)
}
if tmpl_topic_vars.CurrentUser.Perms.DeleteReply {
w.Write(topic_76)
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_77)
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_78)
}
if tmpl_topic_vars.CurrentUser.Perms.ViewIPs {
w.Write(topic_78)
w.Write([]byte(item.IPAddress))
w.Write(topic_79)
}
w.Write([]byte(item.IPAddress))
w.Write(topic_80)
w.Write([]byte(strconv.Itoa(item.ID)))
}
w.Write(topic_81)
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write([]byte(strconv.Itoa(item.ID)))
w.Write(topic_82)
if item.LikeCount > 0 {
w.Write([]byte(tmpl_topic_vars.CurrentUser.Session))
w.Write(topic_83)
w.Write([]byte(strconv.Itoa(item.LikeCount)))
if item.LikeCount > 0 {
w.Write(topic_84)
w.Write([]byte(strconv.Itoa(item.LikeCount)))
w.Write(topic_85)
}
if item.Tag != "" {
w.Write(topic_85)
w.Write([]byte(item.Tag))
w.Write(topic_86)
} else {
w.Write([]byte(item.Tag))
w.Write(topic_87)
w.Write([]byte(strconv.Itoa(item.Level)))
} else {
w.Write(topic_88)
}
w.Write([]byte(strconv.Itoa(item.Level)))
w.Write(topic_89)
}
}
}
w.Write(topic_90)
if tmpl_topic_vars.CurrentUser.Perms.CreateReply {
w.Write(topic_91)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_92)
}
}
}
w.Write(topic_91)
if tmpl_topic_vars.CurrentUser.Perms.CreateReply {
w.Write(topic_92)
w.Write([]byte(strconv.Itoa(tmpl_topic_vars.Topic.ID)))
w.Write(topic_93)
}
w.Write(topic_94)
w.Write(footer_0)
if len(tmpl_topic_vars.Header.Themes) != 0 {
for _, item := range tmpl_topic_vars.Header.Themes {

View File

@ -126,7 +126,7 @@ w.Write([]byte(strconv.Itoa(tmpl_topic_alt_vars.Topic.Level)))
w.Write(topic_alt_24)
}
w.Write(topic_alt_25)
w.Write([]byte(tmpl_topic_alt_vars.Topic.Content))
w.Write([]byte(tmpl_topic_alt_vars.Topic.ContentHTML))
w.Write(topic_alt_26)
w.Write([]byte(tmpl_topic_alt_vars.Topic.Content))
w.Write(topic_alt_27)

View File

@ -5,7 +5,7 @@
<div class="rowitem"><a>Forums</a></div>
</div>
<div class="rowblock">
{{range .ItemList}}<div class="rowitem {{if (.Desc) or (.LastTopicTime)}}datarow{{end}}">
{{range .ItemList}}<div class="rowitem {{if (.Desc) or (.LastTopic.Title)}}datarow{{end}}">
{{if .Desc}}<span style="float: left;">
<a href="{{.Link}}" style="">{{.Name}}</a>
<br /><span class="rowsmall">{{.Desc}}</span>
@ -15,7 +15,7 @@
</span>{{end}}
<span style="float: right;">
<a href="{{.LastTopicLink}}" style="float: right;font-size: 14px;">{{.LastTopic}}</a>
<a href="{{.LastTopic.Link}}" style="float: right;font-size: 14px;">{{if .LastTopic.Title}}{{.LastTopic.Title}}{{else}}None{{end}}</a>
{{if .LastTopicTime}}<br /><span class="rowsmall">{{.LastTopicTime}}</span>{{end}}
</span>
<div style="clear: both;"></div>

View File

@ -23,7 +23,7 @@
<article class="rowblock post_container top_post">
<div class="rowitem passive editable_parent post_item {{.Topic.ClassName}}" style="{{if .Topic.Avatar}}background-image:url({{.Topic.Avatar}}), url(/static/post-avatar-bg.jpg);background-position: 0px {{if le .Topic.ContentLines 5}}-1{{end}}0px;background-repeat:no-repeat, repeat-y;{{end}}">
<p class="hide_on_edit topic_content user_content" style="margin:0;padding:0;">{{.Topic.Content}}</p>
<p class="hide_on_edit topic_content user_content" style="margin:0;padding:0;">{{.Topic.ContentHTML}}</p>
<textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea>
<span class="controls">
@ -56,6 +56,7 @@
</article>
{{else}}
<article class="rowitem passive deletable_block editable_parent post_item {{.ClassName}}" style="{{if .Avatar}}background-image:url({{.Avatar}}), url(/static/post-avatar-bg.jpg);background-position: 0px {{if le .ContentLines 5}}-1{{end}}0px;background-repeat:no-repeat, repeat-y;{{end}}">
{{/** TODO: We might end up with <br>s in the inline editor, fix this **/}}
<p class="editable_block user_content" style="margin:0;padding:0;">{{.ContentHtml}}</p>
<span class="controls">

View File

@ -27,7 +27,7 @@
{{if .Topic.Tag}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag">{{.Topic.Tag}}</div><div class="tag_post"></div></div>{{else}}<div class="tag_block"><div class="tag_pre"></div><div class="post_tag post_level">Level {{.Topic.Level}}</div><div class="tag_post"></div></div>{{end}}
</div>
<div class="content_container">
<div class="hide_on_edit topic_content user_content">{{.Topic.Content}}</div>
<div class="hide_on_edit topic_content user_content">{{.Topic.ContentHTML}}</div>
<textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea>
<div class="button_container">
{{if .CurrentUser.Loggedin}}
@ -58,6 +58,7 @@
<span class="action_icon" style="font-size: 18px;padding-right: 5px;">{{.ActionIcon}}</span>
<span>{{.ActionType}}</span>
{{else}}
{{/** TODO: We might end up with <br>s in the inline editor, fix this **/}}
<div class="editable_block user_content">{{.ContentHtml}}</div>
<div class="button_container">
{{if $.CurrentUser.Loggedin}}

View File

@ -4,7 +4,7 @@
<div class="rowblock rowhead">
<div class="rowitem"><h1>Topic List</h1></div>
</div>
<div id="topic_list" class="rowblock topic_list">
<div id="topic_list" class="rowblock topic_list" aria-label="The main topic list">
{{range .ItemList}}<div class="rowitem topic_left passive datarow {{if .Sticky}}topic_sticky{{else if .IsClosed}}topic_closed{{end}}" style="{{if .Creator.Avatar}}background-image: url({{.Creator.Avatar}});background-position: left;background-repeat: no-repeat;background-size: 64px;padding-left: 72px;{{end}}">
<span class="topic_inner_right rowsmall" style="float: right;">
<span class="replyCount">{{.PostCount}} replies</span><br />

View File

@ -7,6 +7,11 @@ body {
background-color: #222222;
margin: 0;
}
p::selection, span::selection, a::selection {
background-color: hsl(0,0%,75%);
color: hsl(0,0%,20%);
font-weight: 100;
}
#back {
margin-left: auto;
@ -133,7 +138,7 @@ a {
}
.rowblock:not(.opthead):not(.colstack_head):not(.rowhead) .rowitem {
font-size: 15px;
font-size: 15px; /*16px*/
}
.rowblock:last-child, .colstack_item:last-child {
@ -479,7 +484,7 @@ input, select, textarea {
}
/* Forum View */
.rowhead, .opthead, .colstack_head {
.rowhead, .opthead, .colstack_head, .rowhead .rowitem {
display: flex;
flex-direction: row;
}
@ -797,6 +802,9 @@ input, select, textarea {
.topic_list .topic_right {
display: none;
}
#poweredBy span {
display: none;
}
}
@media(max-width: 470px) {

View File

@ -7,8 +7,13 @@
package main
//import "fmt"
import "strconv"
import "html/template"
import (
"html"
"html/template"
"strconv"
)
// ? - Add a TopicMeta struct for *Forums?
type Topic struct {
ID int
@ -54,6 +59,7 @@ type TopicUser struct {
Group int
Avatar string
ContentLines int
ContentHTML string
Tag string
URL string
URLPrefix string
@ -138,6 +144,18 @@ func (topic *Topic) RemoveLike(uid int) error {
return nil
}
func (topic *Topic) Update(name string, content string) error {
content = preparseMessage(content)
parsed_content := parseMessage(html.EscapeString(content))
_, err := editTopicStmt.Exec(name, content, parsed_content, topic.ID)
tcache, ok := topics.(TopicCache)
if ok {
tcache.CacheRemove(topic.ID)
}
return err
}
func (topic *Topic) CreateActionReply(action string, ipaddress string, user User) (err error) {
_, err = createActionReplyStmt.Exec(topic.ID, action, ipaddress, user.ID)
if err != nil {
@ -152,8 +170,12 @@ func (topic *Topic) CreateActionReply(action string, ipaddress string, user User
return err
}
func (topic *Topic) Copy() Topic {
return *topic
}
// TODO: Refactor the caller to take a Topic and a User rather than a combined TopicUser
func getTopicuser(tid int) (TopicUser, error) {
func getTopicUser(tid int) (TopicUser, error) {
tcache, tok := topics.(TopicCache)
ucache, uok := users.(UserCache)
if tok && uok {
@ -165,7 +187,7 @@ func getTopicuser(tid int) (TopicUser, error) {
}
// We might be better off just passing seperate topic and user structs to the caller?
return copyTopicToTopicuser(topic, user), nil
return copyTopicToTopicUser(topic, user), nil
} else if ucache.GetLength() < ucache.GetCapacity() {
topic, err = topics.Get(tid)
if err != nil {
@ -175,7 +197,7 @@ func getTopicuser(tid int) (TopicUser, error) {
if err != nil {
return TopicUser{ID: tid}, err
}
return copyTopicToTopicuser(topic, user), nil
return copyTopicToTopicUser(topic, user), nil
}
}
@ -193,7 +215,7 @@ func getTopicuser(tid int) (TopicUser, error) {
return tu, err
}
func copyTopicToTopicuser(topic *Topic, user *User) (tu TopicUser) {
func copyTopicToTopicUser(topic *Topic, user *User) (tu TopicUser) {
tu.UserLink = user.Link
tu.CreatedByName = user.Name
tu.Group = user.Group
@ -220,6 +242,11 @@ func copyTopicToTopicuser(topic *Topic, user *User) (tu TopicUser) {
return tu
}
// For use in tests and for generating blank topics for forums which don't have a last poster
func getDummyTopic() *Topic {
return &Topic{ID: 0, Title: ""}
}
func getTopicByReply(rid int) (*Topic, error) {
topic := Topic{ID: 0}
err := getTopicByReplyStmt.QueryRow(rid).Scan(&topic.ID, &topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)

View File

@ -61,7 +61,7 @@ type MemoryTopicStore struct {
// NewMemoryTopicStore gives you a new instance of MemoryTopicStore
func NewMemoryTopicStore(capacity int) *MemoryTopicStore {
getStmt, err := qgen.Builder.SimpleSelect("topics", "title, content, createdBy, createdAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, data", "tid = ?", "", "")
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)
}
@ -114,7 +114,7 @@ func (mts *MemoryTopicStore) Get(id int) (*Topic, error) {
}
topic = &Topic{ID: id}
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
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)
@ -125,14 +125,14 @@ func (mts *MemoryTopicStore) Get(id int) (*Topic, error) {
// 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.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
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.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
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)
@ -160,7 +160,7 @@ func (mts *MemoryTopicStore) Delete(id int) error {
return err
}
err = fstore.DecrementTopicCount(topic.ParentID)
err = fstore.RemoveTopic(topic.ParentID)
if err != nil && err != ErrNoRows {
return err
}
@ -271,7 +271,7 @@ type SQLTopicStore struct {
}
func NewSQLTopicStore() *SQLTopicStore {
getStmt, err := qgen.Builder.SimpleSelect("topics", "title, content, createdBy, createdAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, data", "tid = ?", "", "")
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)
}
@ -297,7 +297,7 @@ func NewSQLTopicStore() *SQLTopicStore {
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.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
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
}
@ -305,7 +305,7 @@ func (sts *SQLTopicStore) Get(id int) (*Topic, error) {
// 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.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
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
}
@ -332,7 +332,7 @@ func (sts *SQLTopicStore) Delete(id int) error {
return err
}
err = fstore.DecrementTopicCount(topic.ParentID)
err = fstore.RemoveTopic(topic.ParentID)
if err != nil && err != ErrNoRows {
return err
}

View File

@ -201,6 +201,10 @@ func (user *User) decreasePostStats(wcount int, topic bool) error {
return err
}
func (user *User) Copy() User {
return *user
}
// TODO: Write unit tests for this
func (user *User) initPerms() {
if user.TempGroup != 0 {
@ -290,6 +294,11 @@ func wordsToScore(wcount int, topic bool) (score int) {
return score
}
// For use in tests and to help generate dummy users for forums which don't have last posters
func getDummyUser() *User {
return &User{ID: 0, Name: ""}
}
// TODO: Write unit tests for this
func buildProfileURL(slug string, uid int) string {
if slug == "" {