From 47963e10a94a559e22fb887f87efe58f52a6e424 Mon Sep 17 00:00:00 2001 From: Azareal Date: Thu, 28 Sep 2017 23:16:34 +0100 Subject: [PATCH] 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
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. --- README.md | 4 +- database.go | 12 ++- experimental/module_ottojs.go | 47 ---------- extend.go | 20 +++- extend/heytherejs/main.js | 5 + extend/heytherejs/plugin.json | 7 ++ forum.go | 70 +++++++++++--- forum_store.go | 169 ++++++++++++++++++++-------------- gen_mysql.go | 4 +- gen_pgsql.go | 2 +- general_test.go | 10 +- group.go | 5 + main.go | 8 -- member_routes.go | 28 +++--- misc_test.go | 17 +--- mod_routes.go | 18 ++-- module_ottojs.go | 91 ++++++++++++++++++ mysql.sql | 5 +- pages.go | 6 +- plugin_helloworld.go | 23 ----- plugin_heythere.go | 24 +++++ plugin_socialgroups.go | 2 +- pluginlangs.go | 116 +++++++++++++++++++++++ query_gen/main.go | 4 +- reply.go | 17 ++-- routes.go | 42 +++++---- template_forums.go | 20 ++-- template_init.go | 9 +- template_list.go | 71 +++++++------- template_topic.go | 61 ++++++------ template_topic_alt.go | 2 +- templates/forums.html | 4 +- templates/topic.html | 3 +- templates/topic_alt.html | 3 +- templates/topics.html | 2 +- themes/shadow/public/main.css | 12 ++- topic.go | 39 ++++++-- topic_store.go | 18 ++-- user.go | 9 ++ 39 files changed, 653 insertions(+), 356 deletions(-) delete mode 100644 experimental/module_ottojs.go create mode 100644 extend/heytherejs/main.js create mode 100644 extend/heytherejs/plugin.json create mode 100644 module_ottojs.go delete mode 100644 plugin_helloworld.go create mode 100644 plugin_heythere.go create mode 100644 pluginlangs.go diff --git a/README.md b/README.md index 3d5d78f3..3ebd9582 100644 --- a/README.md +++ b/README.md @@ -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 :) diff --git a/database.go b/database.go index 4c52352f..3a6804ca 100644 --- a/database.go +++ b/database.go @@ -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 } diff --git a/experimental/module_ottojs.go b/experimental/module_ottojs.go deleted file mode 100644 index 126ff1c1..00000000 --- a/experimental/module_ottojs.go +++ /dev/null @@ -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") - } - } -} - diff --git a/extend.go b/extend.go index eaf66f80..f91980a6 100644 --- a/extend.go +++ b/extend.go @@ -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 { diff --git a/extend/heytherejs/main.js b/extend/heytherejs/main.js new file mode 100644 index 00000000..86f23665 --- /dev/null +++ b/extend/heytherejs/main.js @@ -0,0 +1,5 @@ +current_page.test = true; + +// This shouldn't ever fail +var errmsg = "gotcha"; +errmsg; \ No newline at end of file diff --git a/extend/heytherejs/plugin.json b/extend/heytherejs/plugin.json new file mode 100644 index 00000000..5862681c --- /dev/null +++ b/extend/heytherejs/plugin.json @@ -0,0 +1,7 @@ +{ + "UName":"heytherejs", + "Name":"HeythereJS", + "Author":"Azareal", + "URL":"https://github.com/Azareal/Gosora", + "Main":"main.js" +} \ No newline at end of file diff --git a/forum.go b/forum.go index a1fdf735..bb2c2728 100644 --- a/forum.go +++ b/forum.go @@ -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) diff --git a/forum_store.go b/forum_store.go index 73abbf68..97751978 100644 --- a/forum_store.go +++ b/forum_store.go @@ -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) { diff --git a/gen_mysql.go b/gen_mysql.go index 7dfd8c2c..90afab99 100644 --- a/gen_mysql.go +++ b/gen_mysql.go @@ -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 } diff --git a/gen_pgsql.go b/gen_pgsql.go index cfa68b2d..eaae16c0 100644 --- a/gen_pgsql.go +++ b/gen_pgsql.go @@ -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 } diff --git a/general_test.go b/general_test.go index b34053c6..8aa39f7c 100644 --- a/general_test.go +++ b/general_test.go @@ -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) { diff --git a/group.go b/group.go index 799c4bbd..54c40686 100644 --- a/group.go +++ b/group.go @@ -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 +} diff --git a/main.go b/main.go index 8d439100..2079420c 100644 --- a/main.go +++ b/main.go @@ -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 { diff --git a/member_routes.go b/member_routes.go index 977a71a5..3d8b5f8a 100644 --- a/member_routes.go +++ b/member_routes.go @@ -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 diff --git a/misc_test.go b/misc_test.go index 66f191dc..8e70a6d5 100644 --- a/misc_test.go +++ b/misc_test.go @@ -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) diff --git a/mod_routes.go b/mod_routes.go index 97f5e34d..06ca5d5e 100644 --- a/mod_routes.go +++ b/mod_routes.go @@ -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 { diff --git a/module_ottojs.go b/module_ottojs.go new file mode 100644 index 00000000..80e69fef --- /dev/null +++ b/module_ottojs.go @@ -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") + } + } +}*/ diff --git a/mysql.sql b/mysql.sql index 6aea704e..c0c77e1f 100644 --- a/mysql.sql +++ b/mysql.sql @@ -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}'); diff --git a/pages.go b/pages.go index 3f54ff31..feb273d7 100644 --- a/pages.go +++ b/pages.go @@ -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 } diff --git a/plugin_helloworld.go b/plugin_helloworld.go deleted file mode 100644 index 60f8d6f4..00000000 --- a/plugin_helloworld.go +++ /dev/null @@ -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 -} diff --git a/plugin_heythere.go b/plugin_heythere.go new file mode 100644 index 00000000..1e16aaaf --- /dev/null +++ b/plugin_heythere.go @@ -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 +} diff --git a/plugin_socialgroups.go b/plugin_socialgroups.go index f0b488f7..4f8651bd 100644 --- a/plugin_socialgroups.go +++ b/plugin_socialgroups.go @@ -56,7 +56,7 @@ type SocialGroupPage struct { CurrentUser User Header *HeaderVars ItemList []*TopicsRow - Forum Forum + Forum *Forum SocialGroup *SocialGroup Page int LastPage int diff --git a/pluginlangs.go b/pluginlangs.go new file mode 100644 index 00000000..ac61c2fd --- /dev/null +++ b/pluginlangs.go @@ -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 + "'") +} diff --git a/query_gen/main.go b/query_gen/main.go index 08db342c..f13cefd3 100644 --- a/query_gen/main.go +++ b/query_gen/main.go @@ -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 = ?") diff --git a/reply.go b/reply.go index fcaedf30..3ac90fa5 100644 --- a/reply.go +++ b/reply.go @@ -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 } diff --git a/routes.go b/routes.go index 29a992f0..dafc9574 100644 --- a/routes.go +++ b/routes.go @@ -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 { diff --git a/template_forums.go b/template_forums.go index de35f365..00501d12 100644 --- a/template_forums.go +++ b/template_forums.go @@ -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 { diff --git a/template_init.go b/template_init.go index b705580d..eae8398b 100644 --- a/template_init.go +++ b/template_init.go @@ -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 { diff --git a/template_list.go b/template_list.go index 4c52b488..a0aaf3fa 100644 --- a/template_list.go +++ b/template_list.go @@ -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(`

`) -var topic_67 = []byte(`

+var topic_68 = []byte(`

`) -var topic_69 = []byte(`   +var topic_69 = []byte(`" class="username real_username">`) +var topic_70 = []byte(`   `) -var topic_70 = []byte(``) -var topic_74 = []byte(``) -var topic_76 = []byte(``) -var topic_78 = []byte(``) -var topic_80 = []byte(` +var topic_71 = []byte(``) +var topic_75 = []byte(``) +var topic_77 = []byte(``) +var topic_79 = []byte(``) +var topic_81 = []byte(` +var topic_82 = []byte(`?session=`) +var topic_83 = []byte(`&type=reply" class="mod_button report_item" title="Flag Reply"> `) -var topic_83 = []byte(``) -var topic_85 = []byte(``) -var topic_86 = []byte(``) -var topic_87 = []byte(``) -var topic_88 = []byte(``) -var topic_89 = []byte(` +var topic_84 = []byte(``) +var topic_86 = []byte(``) +var topic_87 = []byte(``) +var topic_88 = []byte(``) +var topic_89 = []byte(``) +var topic_90 = []byte(` `) -var topic_90 = []byte(` +var topic_91 = []byte(` `) -var topic_91 = []byte(` +var topic_92 = []byte(`
+var topic_93 = []byte(`' type="hidden" />
@@ -238,7 +240,7 @@ var topic_92 = []byte(`' type="hidden" />
`) -var topic_93 = []byte(` +var topic_94 = []byte(` @@ -632,17 +634,18 @@ var forums_11 = []byte(` `) -var forums_13 = []byte(` +var forums_13 = []byte(`None`) +var forums_14 = []byte(` `) -var forums_14 = []byte(`
`) -var forums_15 = []byte(``) -var forums_16 = []byte(` +var forums_15 = []byte(`
`) +var forums_16 = []byte(``) +var forums_17 = []byte(`
`) -var forums_17 = []byte(`
You don't have access to any forums.
`) -var forums_18 = []byte(` +var forums_18 = []byte(`
You don't have access to any forums.
`) +var forums_19 = []byte(` @@ -653,7 +656,7 @@ var topics_0 = []byte(`

Topic List

-
+
`) var topics_1 = []byte(`
- {{range .ItemList}}
+ {{range .ItemList}}
{{if .Desc}} {{.Name}}
{{.Desc}} @@ -15,7 +15,7 @@
{{end}} - {{.LastTopic}} + {{if .LastTopic.Title}}{{.LastTopic.Title}}{{else}}None{{end}} {{if .LastTopicTime}}
{{.LastTopicTime}}{{end}}
diff --git a/templates/topic.html b/templates/topic.html index 3e90c3c1..a8992cff 100644 --- a/templates/topic.html +++ b/templates/topic.html @@ -23,7 +23,7 @@
-

{{.Topic.Content}}

+

{{.Topic.ContentHTML}}

@@ -56,6 +56,7 @@
{{else}}
+ {{/** TODO: We might end up with
s in the inline editor, fix this **/}}

{{.ContentHtml}}

diff --git a/templates/topic_alt.html b/templates/topic_alt.html index 3a88c875..e1a37e38 100644 --- a/templates/topic_alt.html +++ b/templates/topic_alt.html @@ -27,7 +27,7 @@ {{if .Topic.Tag}}
{{else}}
{{end}}
-
{{.Topic.Content}}
+
{{.Topic.ContentHTML}}
{{if .CurrentUser.Loggedin}} @@ -58,6 +58,7 @@ {{.ActionIcon}} {{.ActionType}} {{else}} + {{/** TODO: We might end up with
s in the inline editor, fix this **/}}
{{.ContentHtml}}
{{if $.CurrentUser.Loggedin}} diff --git a/templates/topics.html b/templates/topics.html index 3354e2b2..0a0227af 100644 --- a/templates/topics.html +++ b/templates/topics.html @@ -4,7 +4,7 @@

Topic List

-
+
{{range .ItemList}}
{{.PostCount}} replies
diff --git a/themes/shadow/public/main.css b/themes/shadow/public/main.css index beccba17..609a9eb4 100644 --- a/themes/shadow/public/main.css +++ b/themes/shadow/public/main.css @@ -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) { diff --git a/topic.go b/topic.go index ff7d0757..260f170c 100644 --- a/topic.go +++ b/topic.go @@ -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) diff --git a/topic_store.go b/topic_store.go index c9819e19..999a4960 100644 --- a/topic_store.go +++ b/topic_store.go @@ -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 } diff --git a/user.go b/user.go index 6eab9fce..d131e1ef 100644 --- a/user.go +++ b/user.go @@ -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 == "" {