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) # 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 :) 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 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.") log.Print("Loading the forums.")
fstore = NewMemoryForumStore() fstore = NewMemoryForumStore()
err = fstore.LoadForums() err = fstore.LoadForums()
@ -45,7 +55,7 @@ func initDatabase() (err error) {
} }
log.Print("Loading the plugins.") log.Print("Loading the plugins.")
err = LoadPlugins() err = initExtend()
if err != nil { if err != nil {
return err 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 package main
import "log" import (
import "net/http" "log"
"net/http"
)
var plugins = make(map[string]*Plugin) var plugins = make(map[string]*Plugin)
@ -15,7 +17,6 @@ var plugins = make(map[string]*Plugin)
var hooks = map[string][]func(interface{}) interface{}{ var hooks = map[string][]func(interface{}) interface{}{
"forums_frow_assign": nil, "forums_frow_assign": nil,
"topic_create_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 // Hooks with a variable number of arguments
@ -26,6 +27,7 @@ var vhooks = map[string]func(...interface{}) interface{}{
"forum_trow_assign": nil, "forum_trow_assign": nil,
"topics_topic_row_assign": nil, "topics_topic_row_assign": nil,
//"topics_user_row_assign": nil, //"topics_user_row_assign": nil,
"topic_reply_row_assign": nil,
"create_group_preappend": nil, // What is this? Investigate! "create_group_preappend": nil, // What is this? Investigate!
"topic_create_pre_loop": nil, "topic_create_pre_loop": nil,
} }
@ -100,6 +102,15 @@ type Plugin struct {
Uninstall func() error Uninstall func() error
Hooks map[string]int 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 // 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() defer rows.Close()
var uname string var uname string
var active bool var active, installed bool
var installed bool
for rows.Next() { for rows.Next() {
err = rows.Scan(&uname, &active, &installed) err = rows.Scan(&uname, &active, &installed)
if err != nil { 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 { type Forum struct {
ID int ID int
Link string Link string
Name string Name string
Desc string Desc string
Active bool Active bool
Preset string Preset string
ParentID int ParentID int
ParentType string ParentType string
TopicCount int TopicCount int
LastTopicLink string
LastTopic string LastTopic *Topic
LastTopicID int LastTopicID int
LastReplyer string LastReplyer *User
LastReplyerID int 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 { type ForumSimple struct {
ID int ID int
Name string Name string
@ -43,6 +46,36 @@ type ForumSimple struct {
Preset string 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 { func (forum *Forum) Update(name string, desc string, active bool, preset string) error {
if name == "" { if name == "" {
name = forum.Name name = forum.Name
@ -53,9 +86,13 @@ func (forum *Forum) Update(name string, desc string, active bool, preset string)
return err return err
} }
if forum.Preset != preset || preset == "custom" || preset == "" { 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) _ = fstore.Reload(forum.ID)
return nil
} }
// TODO: Replace this sorting mechanism with something a lot more efficient // 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 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 { func buildForumURL(slug string, fid int) string {
if slug == "" { if slug == "" {
return "/forum/" + strconv.Itoa(fid) return "/forum/" + strconv.Itoa(fid)

View File

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

View File

@ -186,7 +186,7 @@ func _gen_mysql() (err error) {
} }
log.Print("Preparing getForums statement.") 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 { if err != nil {
return err return err
} }
@ -534,7 +534,7 @@ func _gen_mysql() (err error) {
} }
log.Print("Preparing updateForumCache statement.") 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 { if err != nil {
return err return err
} }

View File

@ -80,7 +80,7 @@ func _gen_pgsql() (err error) {
} }
log.Print("Preparing updateForumCache statement.") 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 { if err != nil {
return err return err
} }

View File

@ -53,14 +53,6 @@ func gloinit() error {
log.Fatal(err) 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.") log.Print("Loading the static files.")
err = initStaticFiles() err = initStaticFiles()
if err != nil { if err != nil {
@ -548,7 +540,7 @@ func BenchmarkQueriesSerial(b *testing.B) {
} }
}) })
var replyItem Reply var replyItem ReplyUser
var isSuperAdmin bool var isSuperAdmin bool
var group int var group int
b.Run("topic_replies_scan", func(b *testing.B) { b.Run("topic_replies_scan", func(b *testing.B) {

View File

@ -11,6 +11,7 @@ type GroupAdmin struct {
CanDelete bool CanDelete bool
} }
// ! Fix the data races
type Group struct { type Group struct {
ID int ID int
Name string Name string
@ -25,3 +26,7 @@ type Group struct {
Forums []ForumPerms Forums []ForumPerms
CanSee []int // The IDs of the forums this group can see 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) 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.") log.Print("Loading the static files.")
err = initStaticFiles() err = initStaticFiles()
if err != nil { if err != nil {

View File

@ -10,7 +10,6 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"time"
) )
// ? - Should we add a new permission or permission zone (like per-forum permissions) specifically for profile comment creation // ? - 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? // Do a bulk forum fetch, just in case it's the SqlForumStore?
forum := fstore.DirtyGet(ffid) forum := fstore.DirtyGet(ffid)
if forum.Name != "" && forum.Active { if forum.Name != "" && forum.Active {
fcopy := *forum fcopy := forum.Copy()
if hooks["topic_create_frow_assign"] != nil { if hooks["topic_create_frow_assign"] != nil {
// TODO: Add the skip feature to all the other row based hooks? // TODO: Add the skip feature to all the other row based hooks?
if runHook("topic_create_frow_assign", &fcopy).(bool) { if runHook("topic_create_frow_assign", &fcopy).(bool) {
@ -144,12 +143,6 @@ func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) {
return return
} }
err = fstore.IncrementTopicCount(fid)
if err != nil {
InternalError(err, w)
return
}
_, err = addSubscriptionStmt.Exec(user.ID, lastID, "topic") _, err = addSubscriptionStmt.Exec(user.ID, lastID, "topic")
if err != nil { if err != nil {
InternalError(err, w) InternalError(err, w)
@ -163,7 +156,7 @@ func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) {
return 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 { if err != nil && err != ErrNoRows {
InternalError(err, w) InternalError(err, w)
} }
@ -219,7 +212,14 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) {
InternalError(err, w) InternalError(err, w)
return 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 { if err != nil && err != ErrNoRows {
InternalError(err, w) InternalError(err, w)
return return
@ -247,12 +247,6 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) {
go notifyWatchers(lastID) 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) http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther)
err = user.increasePostStats(wcount, false) err = user.increasePostStats(wcount, false)
if err != nil { if err != nil {
@ -629,7 +623,7 @@ func routeReportSubmit(w http.ResponseWriter, r *http.Request, user User, sitemI
InternalError(err, w) InternalError(err, w)
return 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 { if err != nil && err != ErrNoRows {
InternalError(err, w) InternalError(err, w)
return return

View File

@ -269,19 +269,12 @@ func TestForumStore(t *testing.T) {
} }
forum, err = fstore.Get(0) forum, err = fstore.Get(0)
if err == ErrNoRows { if err == nil {
t.Error("Couldn't find FID #0") t.Error("FID #0 shouldn't exist")
} else if err != nil { } else if err != ErrNoRows {
t.Fatal(err) 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) forum, err = fstore.Get(1)
if err == ErrNoRows { if err == ErrNoRows {
t.Error("Couldn't find FID #1") t.Error("Couldn't find FID #1")
@ -311,8 +304,8 @@ func TestForumStore(t *testing.T) {
} }
ok = fstore.Exists(0) ok = fstore.Exists(0)
if !ok { if ok {
t.Error("FID #0 should exist") t.Error("FID #0 shouldn't exist")
} }
ok = fstore.Exists(1) ok = fstore.Exists(1)

View File

@ -4,6 +4,7 @@ import (
//"log" //"log"
//"fmt" //"fmt"
"html" "html"
"log"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
@ -26,7 +27,7 @@ func routeEditTopic(w http.ResponseWriter, r *http.Request, user User) {
return return
} }
oldTopic, err := topics.Get(tid) topic, err := topics.Get(tid)
if err == ErrNoRows { if err == ErrNoRows {
PreErrorJSQ("The topic you tried to edit doesn't exist.", w, r, isJs) PreErrorJSQ("The topic you tried to edit doesn't exist.", w, r, isJs)
return return
@ -36,7 +37,7 @@ func routeEditTopic(w http.ResponseWriter, r *http.Request, user User) {
} }
// TODO: Add hooks to make use of headerLite // TODO: Add hooks to make use of headerLite
_, ok := SimpleForumUserCheck(w, r, &user, oldTopic.ParentID) _, ok := SimpleForumUserCheck(w, r, &user, topic.ParentID)
if !ok { if !ok {
return return
} }
@ -47,25 +48,20 @@ func routeEditTopic(w http.ResponseWriter, r *http.Request, user User) {
topicName := r.PostFormValue("topic_name") topicName := r.PostFormValue("topic_name")
topicContent := html.EscapeString(r.PostFormValue("topic_content")) topicContent := html.EscapeString(r.PostFormValue("topic_content"))
log.Print("topicContent ", topicContent)
// TODO: Move this bit to the TopicStore err = topic.Update(topicName, topicContent)
_, err = editTopicStmt.Exec(topicName, preparseMessage(topicContent), parseMessage(html.EscapeString(preparseMessage(topicContent))), tid)
if err != nil { if err != nil {
InternalErrorJSQ(err, w, r, isJs) InternalErrorJSQ(err, w, r, isJs)
return 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 { if err != nil && err != ErrNoRows {
InternalError(err, w) InternalErrorJSQ(err, w, r, isJs)
return return
} }
tcache, ok := topics.(TopicCache)
if ok {
tcache.CacheRemove(oldTopic.ID)
}
if !isJs { if !isJs {
http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther) http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther)
} else { } 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, `preset` varchar(100) DEFAULT '' not null,
`parentID` int DEFAULT 0 not null, /* TODO: Add support for subforums */ `parentID` int DEFAULT 0 not null, /* TODO: Add support for subforums */
`parentType` varchar(50) DEFAULT '' not null, `parentType` varchar(50) DEFAULT '' not null,
`lastTopic` varchar(100) DEFAULT '' not null,
`lastTopicID` int DEFAULT 0 not null, `lastTopicID` int DEFAULT 0 not null,
`lastReplyer` varchar(100) DEFAULT '' not null,
`lastReplyerID` int DEFAULT 0 not null, `lastReplyerID` int DEFAULT 0 not null,
`lastTopicTime` datetime not null,
primary key(`fid`) primary key(`fid`)
) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci; ) 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 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`,`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 (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}'); 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 Title string
CurrentUser User CurrentUser User
Header *HeaderVars Header *HeaderVars
ItemList []Reply ItemList []ReplyUser
Topic TopicUser Topic TopicUser
Page int Page int
LastPage int LastPage int
@ -72,7 +72,7 @@ type ForumPage struct {
CurrentUser User CurrentUser User
Header *HeaderVars Header *HeaderVars
ItemList []*TopicsRow ItemList []*TopicsRow
Forum Forum Forum *Forum
Page int Page int
LastPage int LastPage int
} }
@ -88,7 +88,7 @@ type ProfilePage struct {
Title string Title string
CurrentUser User CurrentUser User
Header *HeaderVars Header *HeaderVars
ItemList []Reply ItemList []ReplyUser
ProfileOwner User 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 CurrentUser User
Header *HeaderVars Header *HeaderVars
ItemList []*TopicsRow ItemList []*TopicsRow
Forum Forum Forum *Forum
SocialGroup *SocialGroup SocialGroup *SocialGroup
Page int Page int
LastPage 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("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", "") 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("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 = ?") 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? // ? - 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 ID int
ParentID int ParentID int
Content string Content string
@ -36,7 +35,7 @@ type Reply struct /* Should probably rename this to ReplyUser and rename ReplySh
ActionIcon string ActionIcon string
} }
type ReplyShort struct { type Reply struct {
ID int ID int
ParentID int ParentID int
Content string Content string
@ -51,14 +50,18 @@ type ReplyShort struct {
LikeCount int LikeCount int
} }
func getReply(id int) (*ReplyShort, error) { func (reply *Reply) Copy() Reply {
reply := ReplyShort{ID: id} 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) err := getReplyStmt.QueryRow(id).Scan(&reply.ParentID, &reply.Content, &reply.CreatedBy, &reply.CreatedAt, &reply.LastEdit, &reply.LastEditBy, &reply.IPAddress, &reply.LikeCount)
return &reply, err return &reply, err
} }
func getUserReply(id int) (*ReplyShort, error) { func getUserReply(id int) (*Reply, error) {
reply := ReplyShort{ID: id} reply := Reply{ID: id}
err := getUserReplyStmt.QueryRow(id).Scan(&reply.ParentID, &reply.Content, &reply.CreatedBy, &reply.CreatedAt, &reply.LastEdit, &reply.LastEditBy, &reply.IPAddress) err := getUserReplyStmt.QueryRow(id).Scan(&reply.ParentID, &reply.Content, &reply.CreatedBy, &reply.CreatedAt, &reply.LastEdit, &reply.LastEditBy, &reply.IPAddress)
return &reply, err return &reply, err
} }

View File

@ -159,6 +159,7 @@ func routeTopics(w http.ResponseWriter, r *http.Request, user User) {
return return
} }
// TODO: Make CanSee a method on *Group with a canSee field?
var canSee []int var canSee []int
if user.IsSuperAdmin { if user.IsSuperAdmin {
canSee, err = fstore.GetAllVisibleIDs() 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] 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 preRenderHooks["pre_render_view_forum"] != nil {
if runPreRenderHook("pre_render_view_forum", w, r, &user, &pi) { if runPreRenderHook("pre_render_view_forum", w, r, &user, &pi) {
return return
@ -417,16 +418,22 @@ func routeForums(w http.ResponseWriter, r *http.Request, user User) {
} }
for _, fid := range canSee { for _, fid := range canSee {
//log.Print(forums[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) var forum = fstore.DirtyGet(fid).Copy()
if forum.ParentID == 0 && forum.Name != "" && forum.Active { if forum.ParentID == 0 && forum.Name != "" && forum.Active {
if forum.LastTopicID != 0 { if forum.LastTopicID != 0 {
forum.LastTopicTime, err = relativeTime(forum.LastTopicTime) //topic, user := forum.GetLast()
if err != nil { //if topic.ID != 0 && user.ID != 0 {
InternalError(err, w) 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 { } else {
forum.LastTopic = "None"
forum.LastTopicTime = "" forum.LastTopicTime = ""
} }
if hooks["forums_frow_assign"] != nil { 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) { func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
var err error var err error
var page, offset int var page, offset int
var replyList []Reply var replyList []ReplyUser
page, _ = strconv.Atoi(r.FormValue("page")) page, _ = strconv.Atoi(r.FormValue("page"))
@ -465,7 +472,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
} }
// Get the topic... // Get the topic...
topic, err := getTopicuser(tid) topic, err := getTopicUser(tid)
if err == ErrNoRows { if err == ErrNoRows {
NotFound(w, r) NotFound(w, r)
return return
@ -488,7 +495,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
BuildWidgets("view_topic", &topic, headerVars, r) BuildWidgets("view_topic", &topic, headerVars, r)
topic.Content = parseMessage(topic.Content) topic.ContentHTML = parseMessage(topic.Content)
topic.ContentLines = strings.Count(topic.Content, "\n") topic.ContentLines = strings.Count(topic.Content, "\n")
// We don't want users posting in locked topics... // 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 page = 1
} }
tpage := TopicPage{topic.Title, user, headerVars, replyList, topic, page, lastPage}
// Get the replies.. // Get the replies..
rows, err := getTopicRepliesOffsetStmt.Query(topic.ID, offset, config.ItemsPerPage) rows, err := getTopicRepliesOffsetStmt.Query(topic.ID, offset, config.ItemsPerPage)
if err == ErrNoRows { if err == ErrNoRows {
@ -554,7 +563,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
} }
defer rows.Close() defer rows.Close()
replyItem := Reply{ClassName: ""} replyItem := ReplyUser{ClassName: ""}
for rows.Next() { 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) 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 { if err != nil {
@ -628,9 +637,8 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
} }
replyItem.Liked = false replyItem.Liked = false
// TODO: Rename this to topic_rrow_assign if vhooks["topic_reply_row_assign"] != nil {
if hooks["rrow_assign"] != nil { runVhook("topic_reply_row_assign", &tpage, &replyItem)
runHook("rrow_assign", &replyItem)
} }
replyList = append(replyList, replyItem) replyList = append(replyList, replyItem)
} }
@ -640,7 +648,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) {
return return
} }
tpage := TopicPage{topic.Title, user, headerVars, replyList, topic, page, lastPage} tpage.ItemList = replyList
if preRenderHooks["pre_render_view_topic"] != nil { if preRenderHooks["pre_render_view_topic"] != nil {
if runPreRenderHook("pre_render_view_topic", w, r, &user, &tpage) { if runPreRenderHook("pre_render_view_topic", w, r, &user, &tpage) {
return return
@ -658,7 +666,7 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user User) {
var err error var err error
var replyContent, replyCreatedByName, replyCreatedAt, replyAvatar, replyTag, replyClassName string var replyContent, replyCreatedByName, replyCreatedAt, replyAvatar, replyTag, replyClassName string
var rid, replyCreatedBy, replyLastEdit, replyLastEditBy, replyLines, replyGroup int var rid, replyCreatedBy, replyLastEdit, replyLastEditBy, replyLines, replyGroup int
var replyList []Reply var replyList []ReplyUser
// SEO URLs... // SEO URLs...
halves := strings.Split(r.URL.Path[len("/user/"):], ".") 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 // 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() err = rows.Err()
if err != nil { if err != nil {

View File

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

View File

@ -106,9 +106,9 @@ func compileTemplates() error {
log.Print("Compiling the templates") 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} 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 var replyList []ReplyUser
replyList = append(replyList, Reply{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", config.DefaultGroup, "", 0, 0, "", "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""}) 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) var varList = make(map[string]VarItem)
tpage := TopicPage{"Title", user, headerVars, replyList, topic, 1, 1} tpage := TopicPage{"Title", user, headerVars, replyList, topic, 1, 1}
@ -135,6 +135,7 @@ func compileTemplates() error {
} }
for _, forum := range forums { for _, forum := range forums {
//log.Printf("*forum %+v\n", *forum)
forumList = append(forumList, *forum) forumList = append(forumList, *forum)
} }
varList = make(map[string]VarItem) varList = make(map[string]VarItem)
@ -154,7 +155,7 @@ func compileTemplates() error {
//var topicList []TopicUser //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}) //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} forumPage := ForumPage{"General Forum", user, headerVars, topicsList, forumItem, 1, 1}
forumTmpl, err := c.compileTemplate("forum.html", "templates/", "ForumPage", forumPage, varList) forumTmpl, err := c.compileTemplate("forum.html", "templates/", "ForumPage", forumPage, varList)
if err != nil { 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_64 = []byte(`-1`)
var topic_65 = []byte(`0px;background-repeat:no-repeat, repeat-y;`) var topic_65 = []byte(`0px;background-repeat:no-repeat, repeat-y;`)
var topic_66 = []byte(`"> var topic_66 = []byte(`">
`)
var topic_67 = []byte(`
<p class="editable_block user_content" style="margin:0;padding:0;">`) <p class="editable_block user_content" style="margin:0;padding:0;">`)
var topic_67 = []byte(`</p> var topic_68 = []byte(`</p>
<span class="controls"> <span class="controls">
<a href="`) <a href="`)
var topic_68 = []byte(`" class="username real_username">`) var topic_69 = []byte(`" class="username real_username">`)
var topic_69 = []byte(`</a>&nbsp;&nbsp; var topic_70 = []byte(`</a>&nbsp;&nbsp;
`) `)
var topic_70 = []byte(`<a href="/reply/like/submit/`) var topic_71 = []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(`" 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(` style="background-color:#D6FFD6;"`)
var topic_73 = []byte(`></button></a>`) var topic_74 = []byte(`></button></a>`)
var topic_74 = []byte(`<a href="/reply/edit/submit/`) var topic_75 = []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(`" 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(`<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(`" 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(`<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(`' style="font-weight:normal;" title="View IP"><button class="username ip_label"></button></a>`)
var topic_80 = []byte(` var topic_81 = []byte(`
<a href="/report/submit/`) <a href="/report/submit/`)
var topic_81 = []byte(`?session=`) var topic_82 = []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_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 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><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 class="username hide_on_micro user_tag">`)
var topic_86 = []byte(`</a>`) var topic_87 = []byte(`</a>`)
var topic_87 = []byte(`<a class="username hide_on_micro level">`) var topic_88 = []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(`</a><a class="username hide_on_micro level_label" style="float:right;" title="Level"></a>`)
var topic_89 = []byte(` var topic_90 = []byte(`
</span> </span>
</article> </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"> <div class="rowblock topic_reply_form">
<form action="/reply/create/" method="post"> <form action="/reply/create/" method="post">
<input name="tid" value='`) <input name="tid" value='`)
var topic_92 = []byte(`' type="hidden" /> var topic_93 = []byte(`' type="hidden" />
<div class="formrow real_first_child"> <div class="formrow real_first_child">
<div class="formitem"><textarea name="reply-content" placeholder="Insert reply here" required></textarea></div> <div class="formitem"><textarea name="reply-content" placeholder="Insert reply here" required></textarea></div>
</div> </div>
@ -238,7 +240,7 @@ var topic_92 = []byte(`' type="hidden" />
</form> </form>
</div> </div>
`) `)
var topic_93 = []byte(` var topic_94 = []byte(`
</main> </main>
@ -632,17 +634,18 @@ var forums_11 = []byte(`
<span style="float: right;"> <span style="float: right;">
<a href="`) <a href="`)
var forums_12 = []byte(`" style="float: right;font-size: 14px;">`) 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(`<br /><span class="rowsmall">`)
var forums_15 = []byte(`</span>`) var forums_16 = []byte(`</span>`)
var forums_16 = []byte(` var forums_17 = []byte(`
</span> </span>
<div style="clear: both;"></div> <div style="clear: both;"></div>
</div> </div>
`) `)
var forums_17 = []byte(`<div class="rowitem passive">You don't have access to any forums.</div>`) var forums_18 = []byte(`<div class="rowitem passive">You don't have access to any forums.</div>`)
var forums_18 = []byte(` var forums_19 = []byte(`
</div> </div>
</main> </main>
@ -653,7 +656,7 @@ var topics_0 = []byte(`
<div class="rowblock rowhead"> <div class="rowblock rowhead">
<div class="rowitem"><h1>Topic List</h1></div> <div class="rowitem"><h1>Topic List</h1></div>
</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_1 = []byte(`<div class="rowitem topic_left passive datarow `)
var topics_2 = []byte(`topic_sticky`) var topics_2 = []byte(`topic_sticky`)

View File

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

View File

@ -5,7 +5,7 @@
<div class="rowitem"><a>Forums</a></div> <div class="rowitem"><a>Forums</a></div>
</div> </div>
<div class="rowblock"> <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;"> {{if .Desc}}<span style="float: left;">
<a href="{{.Link}}" style="">{{.Name}}</a> <a href="{{.Link}}" style="">{{.Name}}</a>
<br /><span class="rowsmall">{{.Desc}}</span> <br /><span class="rowsmall">{{.Desc}}</span>
@ -15,7 +15,7 @@
</span>{{end}} </span>{{end}}
<span style="float: right;"> <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}} {{if .LastTopicTime}}<br /><span class="rowsmall">{{.LastTopicTime}}</span>{{end}}
</span> </span>
<div style="clear: both;"></div> <div style="clear: both;"></div>

View File

@ -23,7 +23,7 @@
<article class="rowblock post_container top_post"> <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}}"> <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> <textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea>
<span class="controls"> <span class="controls">
@ -56,6 +56,7 @@
</article> </article>
{{else}} {{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}}"> <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> <p class="editable_block user_content" style="margin:0;padding:0;">{{.ContentHtml}}</p>
<span class="controls"> <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}} {{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>
<div class="content_container"> <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> <textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea>
<div class="button_container"> <div class="button_container">
{{if .CurrentUser.Loggedin}} {{if .CurrentUser.Loggedin}}
@ -58,6 +58,7 @@
<span class="action_icon" style="font-size: 18px;padding-right: 5px;">{{.ActionIcon}}</span> <span class="action_icon" style="font-size: 18px;padding-right: 5px;">{{.ActionIcon}}</span>
<span>{{.ActionType}}</span> <span>{{.ActionType}}</span>
{{else}} {{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="editable_block user_content">{{.ContentHtml}}</div>
<div class="button_container"> <div class="button_container">
{{if $.CurrentUser.Loggedin}} {{if $.CurrentUser.Loggedin}}

View File

@ -4,7 +4,7 @@
<div class="rowblock rowhead"> <div class="rowblock rowhead">
<div class="rowitem"><h1>Topic List</h1></div> <div class="rowitem"><h1>Topic List</h1></div>
</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}}"> {{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="topic_inner_right rowsmall" style="float: right;">
<span class="replyCount">{{.PostCount}} replies</span><br /> <span class="replyCount">{{.PostCount}} replies</span><br />

View File

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

View File

@ -7,8 +7,13 @@
package main package main
//import "fmt" //import "fmt"
import "strconv" import (
import "html/template" "html"
"html/template"
"strconv"
)
// ? - Add a TopicMeta struct for *Forums?
type Topic struct { type Topic struct {
ID int ID int
@ -54,6 +59,7 @@ type TopicUser struct {
Group int Group int
Avatar string Avatar string
ContentLines int ContentLines int
ContentHTML string
Tag string Tag string
URL string URL string
URLPrefix string URLPrefix string
@ -138,6 +144,18 @@ func (topic *Topic) RemoveLike(uid int) error {
return nil 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) { func (topic *Topic) CreateActionReply(action string, ipaddress string, user User) (err error) {
_, err = createActionReplyStmt.Exec(topic.ID, action, ipaddress, user.ID) _, err = createActionReplyStmt.Exec(topic.ID, action, ipaddress, user.ID)
if err != nil { if err != nil {
@ -152,8 +170,12 @@ func (topic *Topic) CreateActionReply(action string, ipaddress string, user User
return err 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 // 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) tcache, tok := topics.(TopicCache)
ucache, uok := users.(UserCache) ucache, uok := users.(UserCache)
if tok && uok { 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? // 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() { } else if ucache.GetLength() < ucache.GetCapacity() {
topic, err = topics.Get(tid) topic, err = topics.Get(tid)
if err != nil { if err != nil {
@ -175,7 +197,7 @@ func getTopicuser(tid int) (TopicUser, error) {
if err != nil { if err != nil {
return TopicUser{ID: tid}, err 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 return tu, err
} }
func copyTopicToTopicuser(topic *Topic, user *User) (tu TopicUser) { func copyTopicToTopicUser(topic *Topic, user *User) (tu TopicUser) {
tu.UserLink = user.Link tu.UserLink = user.Link
tu.CreatedByName = user.Name tu.CreatedByName = user.Name
tu.Group = user.Group tu.Group = user.Group
@ -220,6 +242,11 @@ func copyTopicToTopicuser(topic *Topic, user *User) (tu TopicUser) {
return tu 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) { func getTopicByReply(rid int) (*Topic, error) {
topic := Topic{ID: 0} 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) 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 // NewMemoryTopicStore gives you a new instance of MemoryTopicStore
func NewMemoryTopicStore(capacity int) *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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -114,7 +114,7 @@ func (mts *MemoryTopicStore) Get(id int) (*Topic, error) {
} }
topic = &Topic{ID: id} 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 { if err == nil {
topic.Link = buildTopicURL(nameToSlug(topic.Title), id) topic.Link = buildTopicURL(nameToSlug(topic.Title), id)
_ = mts.CacheAdd(topic) _ = 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 // BypassGet will always bypass the cache and pull the topic directly from the database
func (mts *MemoryTopicStore) BypassGet(id int) (*Topic, error) { func (mts *MemoryTopicStore) BypassGet(id int) (*Topic, error) {
topic := &Topic{ID: id} 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) topic.Link = buildTopicURL(nameToSlug(topic.Title), id)
return topic, err return topic, err
} }
func (mts *MemoryTopicStore) Reload(id int) error { func (mts *MemoryTopicStore) Reload(id int) error {
topic := &Topic{ID: id} 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 { if err == nil {
topic.Link = buildTopicURL(nameToSlug(topic.Title), id) topic.Link = buildTopicURL(nameToSlug(topic.Title), id)
_ = mts.CacheSet(topic) _ = mts.CacheSet(topic)
@ -160,7 +160,7 @@ func (mts *MemoryTopicStore) Delete(id int) error {
return err return err
} }
err = fstore.DecrementTopicCount(topic.ParentID) err = fstore.RemoveTopic(topic.ParentID)
if err != nil && err != ErrNoRows { if err != nil && err != ErrNoRows {
return err return err
} }
@ -271,7 +271,7 @@ type SQLTopicStore struct {
} }
func NewSQLTopicStore() *SQLTopicStore { 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -297,7 +297,7 @@ func NewSQLTopicStore() *SQLTopicStore {
func (sts *SQLTopicStore) Get(id int) (*Topic, error) { func (sts *SQLTopicStore) Get(id int) (*Topic, error) {
topic := Topic{ID: id} 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) topic.Link = buildTopicURL(nameToSlug(topic.Title), id)
return &topic, err 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 // BypassGet is an alias of Get(), as we don't have a cache for SQLTopicStore
func (sts *SQLTopicStore) BypassGet(id int) (*Topic, error) { func (sts *SQLTopicStore) BypassGet(id int) (*Topic, error) {
topic := &Topic{ID: id} 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) topic.Link = buildTopicURL(nameToSlug(topic.Title), id)
return topic, err return topic, err
} }
@ -332,7 +332,7 @@ func (sts *SQLTopicStore) Delete(id int) error {
return err return err
} }
err = fstore.DecrementTopicCount(topic.ParentID) err = fstore.RemoveTopic(topic.ParentID)
if err != nil && err != ErrNoRows { if err != nil && err != ErrNoRows {
return err return err
} }

View File

@ -201,6 +201,10 @@ func (user *User) decreasePostStats(wcount int, topic bool) error {
return err return err
} }
func (user *User) Copy() User {
return *user
}
// TODO: Write unit tests for this // TODO: Write unit tests for this
func (user *User) initPerms() { func (user *User) initPerms() {
if user.TempGroup != 0 { if user.TempGroup != 0 {
@ -290,6 +294,11 @@ func wordsToScore(wcount int, topic bool) (score int) {
return score 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 // TODO: Write unit tests for this
func buildProfileURL(slug string, uid int) string { func buildProfileURL(slug string, uid int) string {
if slug == "" { if slug == "" {