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_84 = []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_85 = []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 == "" {