From 7be011a30df3f20922f65c6c5efc842d149e497e Mon Sep 17 00:00:00 2001 From: Azareal Date: Sun, 24 Jun 2018 23:49:29 +1000 Subject: [PATCH] Almost finished live topic lists, you can find them at /topics/. You can disable them via config.json The topic list cache can handle more groups now, but don't go too crazy with groups (e.g. thousands of them). Make the suspicious request logs more descriptive. Added the phrases API endpoint. Split the template phrases up by prefix, more work on this coming up. Removed #dash_saved and part of #dash_username. Removed some temporary artifacts from trying to implement FA5 in Nox. Removed some commented CSS. Fixed template artifact deletion on Windows. Tweaked HTTPSRedirect to make it more compact. Fixed NullUserCache not complying with the expectations for BulkGet. Swapped out a few RunVhook calls for more appropriate RunVhookNoreturn calls. Removed a few redundant IsAdmin checks when IsMod would suffice. Commented out a few pushers. Desktop notification permission requests are no longer served to guests. Split topics.html into topics.html and topics_topic.html RunThemeTemplate should now fallback to interpreted templates properly when the transpiled variants aren't avaialb.e Changed TopicsRow.CreatedAt from a string to a time.Time Added SkipTmplPtrMap to CTemplateConfig. Added SetBuildTags to CTemplateSet. A bit more data is dumped when something goes wrong while transpiling templates now. topics_topic, topic_posts, and topic_alt_posts are now transpiled for the client, although not all of them are ready to be served to the client yet. Client rendered templates now support phrases. Client rendered templates now support loops. Fixed loadAlerts in global.js Refactored some of the template initialisation code to make it less repetitive. Split topic.html into topic.html and topic_posts.html Split topic_alt.html into topic_alt.html and topic_alt_posts.html Added comments for PollCache. Fixed a data race in the MemoryPollCache. The writer is now closed properly in WsHubImpl.broadcastMessage. Fixed a potential deadlock in WsHubImpl.broadcastMessage. Removed some old commented code in websockets.go Added the DisableLiveTopicList config setting. --- build.bat | 4 +- common/extend.go | 1 + common/files.go | 67 +- common/forum_perms.go | 14 +- common/group_store.go | 4 + common/menus.go | 3 +- common/null_user_cache.go | 4 +- common/pages.go | 6 +- common/phrases.go | 50 +- common/poll_cache.go | 41 +- common/routes_common.go | 8 +- common/site.go | 2 + common/template_init.go | 92 ++- common/templates/templates.go | 30 +- common/theme_list.go | 9 +- common/topic.go | 44 +- common/topic_list.go | 133 ++-- common/user.go | 22 + common/websockets.go | 235 ++++++- gen_router.go | 765 ++++++++++++----------- langs/english.json | 174 +++--- main.go | 3 + public/global.js | 165 +++-- router_gen/main.go | 6 +- router_gen/routes.go | 7 +- routes.go | 91 +++ routes/forum.go | 2 +- routes/profile.go | 2 +- routes/stubs.go | 5 +- routes/topic.go | 8 +- run-nowebsockets.bat | 4 +- run.bat | 4 +- run_mssql.bat | 4 +- run_tests.bat | 4 +- run_tests_mssql.bat | 4 +- templates/account_own_edit.html | 2 - templates/forum.html | 20 +- templates/header.html | 1 + templates/topic.html | 81 +-- templates/topic_alt.html | 97 +-- templates/topic_alt_posts.html | 34 + templates/topic_posts.html | 32 + templates/topics.html | 52 +- templates/topics_topic.html | 33 + themes/cosora/public/account.css | 9 - themes/cosora/public/main.css | 52 +- themes/nox/public/account.css | 3 - themes/nox/public/main.css | 16 - themes/shadow/public/account.css | 3 - themes/shadow/public/main.css | 22 +- themes/tempra-conflux/public/account.css | 2 +- themes/tempra-conflux/public/main.css | 20 +- themes/tempra-simple/public/account.css | 2 +- themes/tempra-simple/public/main.css | 2 +- 54 files changed, 1504 insertions(+), 996 deletions(-) create mode 100644 templates/topic_alt_posts.html create mode 100644 templates/topic_posts.html create mode 100644 templates/topics_topic.html diff --git a/build.bat b/build.bat index cf435db3..088fb4bd 100644 --- a/build.bat +++ b/build.bat @@ -2,7 +2,9 @@ rem TODO: Make these deletes a little less noisy del "template_*.go" del "gen_*.go" -del "tmpl_client/template_*.go" +cd tmpl_client +del "template_*.go" +cd .. del "gosora.exe" echo Generating the dynamic code diff --git a/common/extend.go b/common/extend.go index 34c4924a..dd3882b4 100644 --- a/common/extend.go +++ b/common/extend.go @@ -371,6 +371,7 @@ func RunHookNoreturn(name string, data interface{}) { } } +// TODO: Use RunHook semantics to allow multiple lined up plugins / modules their turn? func RunVhook(name string, data ...interface{}) interface{} { hook := Vhooks[name] if hook != nil { diff --git a/common/files.go b/common/files.go index cf22554f..684c80a5 100644 --- a/common/files.go +++ b/common/files.go @@ -3,7 +3,9 @@ package common import ( "bytes" "errors" + "fmt" "mime" + "strconv" "strings" "sync" //"errors" @@ -40,6 +42,9 @@ func (list SFileList) JSTmplInit() error { DebugLog("Initialising the client side templates") var fragMap = make(map[string][][]byte) fragMap["alert"] = tmpl.GetFrag("alert") + fragMap["topics_topic"] = tmpl.GetFrag("topics_topic") + fragMap["topic_posts"] = tmpl.GetFrag("topic_posts") + fragMap["topic_alt_posts"] = tmpl.GetFrag("topic_alt_posts") DebugLog("fragMap: ", fragMap) return filepath.Walk("./tmpl_client", func(path string, f os.FileInfo, err error) error { if f.IsDir() { @@ -56,18 +61,27 @@ func (list SFileList) JSTmplInit() error { return err } + path = strings.TrimPrefix(path, "tmpl_client/") + tmplName := strings.TrimSuffix(path, ".go") + shortName := strings.TrimPrefix(tmplName, "template_") + var replace = func(data []byte, replaceThis string, withThis string) []byte { return bytes.Replace(data, []byte(replaceThis), []byte(withThis), -1) } - startIndex, hasFunc := skipAllUntilCharsExist(data, 0, []byte("func Template")) + startIndex, hasFunc := skipAllUntilCharsExist(data, 0, []byte("func init() {")) + if !hasFunc { + return errors.New("no init function found") + } + data = data[startIndex-len([]byte("func init() {")):] + data = replace(data, "func ", "function ") + data = replace(data, "function init() {", "tmplInits[\""+tmplName+"\"] = ") + data = replace(data, " error {\n", " {\nlet out = \"\"\n") + funcIndex, hasFunc := skipAllUntilCharsExist(data, 0, []byte("function Template_")) if !hasFunc { return errors.New("no template function found") } - data = data[startIndex-len([]byte("func Template")):] - data = replace(data, "func ", "function ") - data = replace(data, " error {\n", " {\nlet out = \"\"\n") - spaceIndex, hasSpace := skipUntilIfExists(data, 10, ' ') + spaceIndex, hasSpace := skipUntilIfExists(data, funcIndex, ' ') if !hasSpace { return errors.New("no spaces found after the template function name") } @@ -75,13 +89,16 @@ func (list SFileList) JSTmplInit() error { if !hasBrace { return errors.New("no right brace found after the template function name") } - //fmt.Println("spaceIndex: ", spaceIndex) - //fmt.Println("endBrace: ", endBrace) - //fmt.Println("string(data[spaceIndex:endBrace]): ", string(data[spaceIndex:endBrace])) + fmt.Println("spaceIndex: ", spaceIndex) + fmt.Println("endBrace: ", endBrace) + fmt.Println("string(data[spaceIndex:endBrace]): ", string(data[spaceIndex:endBrace])) + preLen := len(data) data = replace(data, string(data[spaceIndex:endBrace]), "") data = replace(data, "))\n", "\n") endBrace -= preLen - len(data) // Offset it as we've deleted portions + fmt.Println("new endBrace: ", endBrace) + fmt.Println("data: ", string(data)) /*var showPos = func(data []byte, index int) (out string) { out = "[" @@ -99,6 +116,9 @@ func (list SFileList) JSTmplInit() error { var each = func(phrase string, handle func(index int)) { //fmt.Println("find each '" + phrase + "'") var index = endBrace + if index < 0 { + panic("index under zero: " + strconv.Itoa(index)) + } var foundIt bool for { //fmt.Println("in index: ", index) @@ -145,9 +165,24 @@ func (list SFileList) JSTmplInit() error { data[braceAt-1] = ')' // Drop a brace here to satisfy JS } }) + each("for _, item := range ", func(index int) { + //fmt.Println("for index: ", index) + braceAt, hasBrace := skipUntilIfExists(data, index, '{') + if hasBrace { + if data[braceAt-1] != ' ' { + panic("couldn't find space before brace, found ' " + string(data[braceAt-1]) + "' instead") + } + data[braceAt-1] = ')' // Drop a brace here to satisfy JS + } + }) + data = replace(data, "for _, item := range ", "for(item of ") data = replace(data, "w.Write([]byte(", "out += ") data = replace(data, "w.Write(", "out += ") data = replace(data, "strconv.Itoa(", "") + data = replace(data, "common.", "") + data = replace(data, shortName+"_tmpl_phrase_id = RegisterTmplPhraseNames([]string{", "[") + data = replace(data, "var phrases = GetTmplPhrasesBytes("+shortName+"_tmpl_phrase_id)", "let phrases = tmplPhrases[\""+tmplName+"\"];") + //data = replace(data, "var phrases = GetTmplPhrasesBytes("+shortName+"_tmpl_phrase_id)", "let phrases = tmplPhrases[\""+tmplName+"\"];\nconsole.log('tmplName:','"+tmplName+"')\nconsole.log('phrases:', phrases);") data = replace(data, "if ", "if(") data = replace(data, "return nil", "return out") data = replace(data, " )", ")") @@ -155,19 +190,25 @@ func (list SFileList) JSTmplInit() error { data = replace(data, "\n", ";\n") data = replace(data, "{;", "{") data = replace(data, "};", "}") + data = replace(data, "[;", "[") data = replace(data, ";;", ";") + data = replace(data, ",;", ",") + data = replace(data, "=;", "=") + data = replace(data, `, + }); +}`, "\n\t];") + data = replace(data, `= +}`, "= []") - path = strings.TrimPrefix(path, "tmpl_client/") - tmplName := strings.TrimSuffix(path, ".go") - fragset, ok := fragMap[strings.TrimPrefix(tmplName, "template_")] + fragset, ok := fragMap[shortName] if !ok { DebugLog("tmplName: ", tmplName) return errors.New("couldn't find template in fragmap") } - var sfrags = []byte("let alert_frags = [];\n") + var sfrags = []byte("let " + shortName + "_frags = [];\n") for _, frags := range fragset { - sfrags = append(sfrags, []byte("alert_frags.push(`"+string(frags)+"`);\n")...) + sfrags = append(sfrags, []byte(shortName+"_frags.push(`"+string(frags)+"`);\n")...) } data = append(sfrags, data...) data = replace(data, "\n;", "\n") diff --git a/common/forum_perms.go b/common/forum_perms.go index 4a794d6c..523ed290 100644 --- a/common/forum_perms.go +++ b/common/forum_perms.go @@ -174,20 +174,30 @@ func PermmapToQuery(permmap map[string]*ForumPerms, fid int) error { if err != nil { return err } - return FPStore.Reload(fid) + err = FPStore.Reload(fid) + if err != nil { + return err + } + return TopicList.RebuildPermTree() } +// TODO: FPStore.Reload? func ReplaceForumPermsForGroup(gid int, presetSet map[int]string, permSets map[int]*ForumPerms) error { tx, err := qgen.Builder.Begin() if err != nil { return err } defer tx.Rollback() + err = ReplaceForumPermsForGroupTx(tx, gid, presetSet, permSets) if err != nil { return err } - return tx.Commit() + err = tx.Commit() + if err != nil { + return err + } + return TopicList.RebuildPermTree() } func ReplaceForumPermsForGroupTx(tx *sql.Tx, gid int, presetSets map[int]string, permSets map[int]*ForumPerms) error { diff --git a/common/group_store.go b/common/group_store.go index 0f342894..2ba85057 100644 --- a/common/group_store.go +++ b/common/group_store.go @@ -284,6 +284,10 @@ func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod if err != nil { return gid, err } + err = TopicList.RebuildPermTree() + if err != nil { + return gid, err + } return gid, nil } diff --git a/common/menus.go b/common/menus.go index 2ab9c986..78805511 100644 --- a/common/menus.go +++ b/common/menus.go @@ -204,7 +204,8 @@ func skipAllUntilCharsExist(tmplData []byte, i int, expects []byte) (newI int, h expectIndex := 0 //fmt.Printf("tmplData: %+v\n", string(tmplData)) for ; j < len(tmplData) && expectIndex < len(expects); j++ { - //fmt.Println("tmplData[j]: ", string(tmplData[j]) + " ") + //fmt.Println("j: ", j) + //fmt.Println("tmplData[j]: ", string(tmplData[j])+" ") if tmplData[j] == expects[expectIndex] { //fmt.Printf("expects[expectIndex]: %+v - %d\n", string(expects[expectIndex]), expectIndex) expectIndex++ diff --git a/common/null_user_cache.go b/common/null_user_cache.go index bcfd758d..057244a6 100644 --- a/common/null_user_cache.go +++ b/common/null_user_cache.go @@ -13,8 +13,8 @@ func NewNullUserCache() *NullUserCache { func (mus *NullUserCache) Get(id int) (*User, error) { return nil, ErrNoRows } -func (mus *NullUserCache) BulkGet(_ []int) (list []*User) { - return nil +func (mus *NullUserCache) BulkGet(ids []int) (list []*User) { + return make([]*User, len(ids)) } func (mus *NullUserCache) GetUnsafe(id int) (*User, error) { return nil, ErrNoRows diff --git a/common/pages.go b/common/pages.go index 5cc2454d..de92ad60 100644 --- a/common/pages.go +++ b/common/pages.go @@ -13,7 +13,7 @@ type Header struct { Title string NoticeList []string Scripts []string - //PreloadScripts []string + //Preload []string Stylesheets []string Widgets PageWidgets Site *site @@ -32,8 +32,8 @@ func (header *Header) AddScript(name string) { header.Scripts = append(header.Scripts, name) } -/*func (header *Header) PreloadScript(name string) { - header.PreloadScripts = append(header.PreloadScripts, name) +/*func (header *Header) Preload(name string) { + header.Preload = append(header.Preload, name) }*/ func (header *Header) AddSheet(name string) { diff --git a/common/phrases.go b/common/phrases.go index 3fc9165c..280b5eb2 100644 --- a/common/phrases.go +++ b/common/phrases.go @@ -1,7 +1,7 @@ /* * * Gosora Phrase System -* Copyright Azareal 2017 - 2018 +* Copyright Azareal 2017 - 2019 * */ package common @@ -13,6 +13,7 @@ import ( "log" "os" "path/filepath" + "strings" "sync" "sync/atomic" ) @@ -34,21 +35,22 @@ type LevelPhrases struct { // ! For the sake of thread safety, you must never modify a *LanguagePack directly, but to create a copy of it and overwrite the entry in the sync.Map type LanguagePack struct { - Name string - Phrases map[string]string // Should we use a sync map or a struct for these? It would be nice, if we could keep all the phrases consistent. - Levels LevelPhrases - GlobalPerms map[string]string - LocalPerms map[string]string - SettingPhrases map[string]string - PermPresets map[string]string - Accounts map[string]string // TODO: Apply these phrases in the software proper - UserAgents map[string]string - OperatingSystems map[string]string - HumanLanguages map[string]string - Errors map[string]map[string]string // map[category]map[name]value - NoticePhrases map[string]string - PageTitles map[string]string - TmplPhrases map[string]string + Name string + Phrases map[string]string // Should we use a sync map or a struct for these? It would be nice, if we could keep all the phrases consistent. + Levels LevelPhrases + GlobalPerms map[string]string + LocalPerms map[string]string + SettingPhrases map[string]string + PermPresets map[string]string + Accounts map[string]string // TODO: Apply these phrases in the software proper + UserAgents map[string]string + OperatingSystems map[string]string + HumanLanguages map[string]string + Errors map[string]map[string]string // map[category]map[name]value + NoticePhrases map[string]string + PageTitles map[string]string + TmplPhrases map[string]string + TmplPhrasesPrefixes map[string]map[string]string // [prefix][name]phrase TmplIndicesToPhrases [][][]byte // [tmplID][index]phrase } @@ -81,6 +83,17 @@ func InitPhrases() error { return err } + // [prefix][name]phrase + langPack.TmplPhrasesPrefixes = make(map[string]map[string]string) + for name, phrase := range langPack.TmplPhrases { + prefix := strings.Split(name, ".")[0] + _, ok := langPack.TmplPhrasesPrefixes[prefix] + if !ok { + langPack.TmplPhrasesPrefixes[prefix] = make(map[string]string) + } + langPack.TmplPhrasesPrefixes[prefix][name] = phrase + } + langPack.TmplIndicesToPhrases = make([][][]byte, len(langTmplIndicesToNames)) for tmplID, phraseNames := range langTmplIndicesToNames { var phraseSet = make([][]byte, len(phraseNames)) @@ -233,6 +246,11 @@ func GetTmplPhrases() map[string]string { return currentLangPack.Load().(*LanguagePack).TmplPhrases } +func GetTmplPhrasesByPrefix(prefix string) (phrases map[string]string, ok bool) { + res, ok := currentLangPack.Load().(*LanguagePack).TmplPhrasesPrefixes[prefix] + return res, ok +} + func getPhrasePlaceholder(prefix string, suffix string) string { return "{lang." + prefix + "[" + suffix + "]}" } diff --git a/common/poll_cache.go b/common/poll_cache.go index 5c51197a..ea034f88 100644 --- a/common/poll_cache.go +++ b/common/poll_cache.go @@ -5,6 +5,7 @@ import ( "sync/atomic" ) +// PollCache is an interface which spits out polls from a fast cache rather than the database, whether from memory or from an application like Redis. Polls may not be present in the cache but may be in the database type PollCache interface { Get(id int) (*Poll, error) GetUnsafe(id int) (*Poll, error) @@ -20,6 +21,7 @@ type PollCache interface { GetCapacity() int } +// MemoryPollCache stores and pulls polls out of the current process' memory type MemoryPollCache struct { items map[int]*Poll length int64 @@ -36,6 +38,7 @@ func NewMemoryPollCache(capacity int) *MemoryPollCache { } } +// Get fetches a poll by ID. Returns ErrNoRows if not present. func (mus *MemoryPollCache) Get(id int) (*Poll, error) { mus.RLock() item, ok := mus.items[id] @@ -46,6 +49,7 @@ func (mus *MemoryPollCache) Get(id int) (*Poll, error) { return item, ErrNoRows } +// BulkGet fetches multiple polls by their IDs. Indices without polls will be set to nil, so make sure you check for those, we might want to change this behaviour to make it less confusing. func (mus *MemoryPollCache) BulkGet(ids []int) (list []*Poll) { list = make([]*Poll, len(ids)) mus.RLock() @@ -56,6 +60,7 @@ func (mus *MemoryPollCache) BulkGet(ids []int) (list []*Poll) { return list } +// GetUnsafe fetches a poll by ID. Returns ErrNoRows if not present. THIS METHOD IS NOT THREAD-SAFE. func (mus *MemoryPollCache) GetUnsafe(id int) (*Poll, error) { item, ok := mus.items[id] if ok { @@ -64,6 +69,7 @@ func (mus *MemoryPollCache) GetUnsafe(id int) (*Poll, error) { return item, ErrNoRows } +// Set overwrites the value of a poll in the cache, whether it's present or not. May return a capacity overflow error. func (mus *MemoryPollCache) Set(item *Poll) error { mus.Lock() user, ok := mus.items[item.ID] @@ -81,17 +87,21 @@ func (mus *MemoryPollCache) Set(item *Poll) error { return nil } +// Add adds a poll to the cache, similar to Set, but it's only intended for new items. This method might be deprecated in the near future, use Set. May return a capacity overflow error. +// ? Is this redundant if we have Set? Are the efficiency wins worth this? Is this even used? func (mus *MemoryPollCache) Add(item *Poll) error { + mus.Lock() if int(mus.length) >= mus.capacity { + mus.Unlock() return ErrStoreCapacityOverflow } - mus.Lock() mus.items[item.ID] = item mus.length = int64(len(mus.items)) mus.Unlock() return nil } +// AddUnsafe is the unsafe version of Add. May return a capacity overflow error. THIS METHOD IS NOT THREAD-SAFE. func (mus *MemoryPollCache) AddUnsafe(item *Poll) error { if int(mus.length) >= mus.capacity { return ErrStoreCapacityOverflow @@ -101,6 +111,7 @@ func (mus *MemoryPollCache) AddUnsafe(item *Poll) error { return nil } +// Remove removes a poll from the cache by ID, if they exist. Returns ErrNoRows if no items exist. func (mus *MemoryPollCache) Remove(id int) error { mus.Lock() _, ok := mus.items[id] @@ -114,6 +125,7 @@ func (mus *MemoryPollCache) Remove(id int) error { return nil } +// RemoveUnsafe is the unsafe version of Remove. THIS METHOD IS NOT THREAD-SAFE. func (mus *MemoryPollCache) RemoveUnsafe(id int) error { _, ok := mus.items[id] if !ok { @@ -124,6 +136,7 @@ func (mus *MemoryPollCache) RemoveUnsafe(id int) error { return nil } +// Flush removes all the polls from the cache, useful for tests. func (mus *MemoryPollCache) Flush() { mus.Lock() mus.items = make(map[int]*Poll) @@ -132,19 +145,23 @@ func (mus *MemoryPollCache) Flush() { } // ! Is this concurrent? -// Length returns the number of users in the memory cache +// Length returns the number of polls in the memory cache func (mus *MemoryPollCache) Length() int { return int(mus.length) } +// SetCapacity sets the maximum number of polls which this cache can hold func (mus *MemoryPollCache) SetCapacity(capacity int) { + // Ints are moved in a single instruction, so this should be thread-safe mus.capacity = capacity } +// GetCapacity returns the maximum number of polls this cache can hold func (mus *MemoryPollCache) GetCapacity() int { return mus.capacity } +// NullPollCache is a poll cache to be used when you don't want a cache and just want queries to passthrough to the database type NullPollCache struct { } @@ -153,50 +170,38 @@ func NewNullPollCache() *NullPollCache { return &NullPollCache{} } +// nolint func (mus *NullPollCache) Get(id int) (*Poll, error) { return nil, ErrNoRows } - func (mus *NullPollCache) BulkGet(ids []int) (list []*Poll) { - return list + return make([]*Poll, len(ids)) } - func (mus *NullPollCache) GetUnsafe(id int) (*Poll, error) { return nil, ErrNoRows } - func (mus *NullPollCache) Set(_ *Poll) error { return nil } - -func (mus *NullPollCache) Add(item *Poll) error { - _ = item +func (mus *NullPollCache) Add(_ *Poll) error { return nil } - -func (mus *NullPollCache) AddUnsafe(item *Poll) error { - _ = item +func (mus *NullPollCache) AddUnsafe(_ *Poll) error { return nil } - func (mus *NullPollCache) Remove(id int) error { return nil } - func (mus *NullPollCache) RemoveUnsafe(id int) error { return nil } - func (mus *NullPollCache) Flush() { } - func (mus *NullPollCache) Length() int { return 0 } - func (mus *NullPollCache) SetCapacity(_ int) { } - func (mus *NullPollCache) GetCapacity() int { return 0 } diff --git a/common/routes_common.go b/common/routes_common.go index bdc0a36c..d4d8ce55 100644 --- a/common/routes_common.go +++ b/common/routes_common.go @@ -149,7 +149,7 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header stats.Reports = 0 // TODO: Do the report count. Only show open threads? // TODO: Remove this as it might be counter-productive - pusher, ok := w.(http.Pusher) + /*pusher, ok := w.(http.Pusher) if ok { pusher.Push("/static/"+theme.Name+"/main.css", nil) pusher.Push("/static/"+theme.Name+"/panel.css", nil) @@ -163,7 +163,7 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header pusher.Push("/static/"+script, nil) } // TODO: Push avatars? - } + }*/ return header, stats, nil } @@ -230,7 +230,7 @@ func userCheck(w http.ResponseWriter, r *http.Request, user *User) (header *Head } } - pusher, ok := w.(http.Pusher) + /*pusher, ok := w.(http.Pusher) if ok { pusher.Push("/static/"+theme.Name+"/main.css", nil) pusher.Push("/static/global.js", nil) @@ -243,7 +243,7 @@ func userCheck(w http.ResponseWriter, r *http.Request, user *User) (header *Head pusher.Push("/static/"+script, nil) } // TODO: Push avatars? - } + }*/ return header, nil } diff --git a/common/site.go b/common/site.go index 4809a9da..9d90d12d 100644 --- a/common/site.go +++ b/common/site.go @@ -81,6 +81,8 @@ type config struct { BuildSlugs bool // TODO: Make this a setting? ServerCount int + DisableLiveTopicList bool + Noavatar string // ? - Move this into the settings table? ItemsPerPage int // ? - Move this into the settings table? MaxTopicTitleLength int diff --git a/common/template_init.go b/common/template_init.go index ab9198ed..f18eb4e0 100644 --- a/common/template_init.go +++ b/common/template_init.go @@ -122,25 +122,15 @@ var Template_ip_search_handle = func(pi IPSearchPage, w io.Writer) error { return Templates.ExecuteTemplate(w, mapping+".html", pi) } -// ? - Add template hooks? -func CompileTemplates() error { - var config tmpl.CTemplateConfig - config.Minify = Config.MinifyTemplates - config.SuperDebug = Dev.TemplateDebug - - c := tmpl.NewCTemplateSet() - c.SetConfig(config) - c.SetBaseImportMap(map[string]string{ - "io": "io", - "./common": "./common", - }) - - // Schemas to train the template compiler on what to expect - // TODO: Add support for interface{}s +func tmplInitUsers() (User, User, User) { user := User{62, BuildProfileURL("fake-user", 62), "Fake User", "compiler@localhost", 0, false, false, false, false, false, false, GuestPerms, make(map[string]bool), "", false, BuildAvatar(62, ""), "", "", "", "", 0, 0, 0, "0.0.0.0.0", 0} // TODO: Do a more accurate level calculation for this? user2 := User{1, BuildProfileURL("admin-alice", 1), "Admin Alice", "alice@localhost", 1, true, true, true, true, false, false, AllPerms, make(map[string]bool), "", true, BuildAvatar(1, ""), "", "", "", "", 58, 1000, 0, "127.0.0.1", 0} user3 := User{2, BuildProfileURL("admin-fred", 62), "Admin Fred", "fred@localhost", 1, true, true, true, true, false, false, AllPerms, make(map[string]bool), "", true, BuildAvatar(2, ""), "", "", "", "", 42, 900, 0, "::1", 0} + return user, user2, user3 +} + +func tmplInitHeaders(user User, user2 User, user3 User) (*Header, *Header, *Header) { header := &Header{ Site: Site, Settings: SettingBox.Load().(SettingMap), @@ -163,9 +153,32 @@ func CompileTemplates() error { *header3 = *header header3.CurrentUser = user3 + return header, header2, header3 +} + +// ? - Add template hooks? +func CompileTemplates() error { + var config tmpl.CTemplateConfig + config.Minify = Config.MinifyTemplates + config.Debug = Dev.DebugMode + config.SuperDebug = Dev.TemplateDebug + + c := tmpl.NewCTemplateSet() + c.SetConfig(config) + c.SetBaseImportMap(map[string]string{ + "io": "io", + "./common": "./common", + }) + c.SetBuildTags("!no_templategen") + + // Schemas to train the template compiler on what to expect + // TODO: Add support for interface{}s + user, user2, user3 := tmplInitUsers() + header, header2, _ := tmplInitHeaders(user, user2, user3) + now := time.Now() + log.Print("Compiling the templates") - var now = time.Now() poll := Poll{ID: 1, Type: 0, Options: map[int]string{0: "Nothing", 1: "Something"}, Results: map[int]int{0: 5, 1: 2}, QuickOptions: []PollOption{ PollOption{0, "Nothing"}, PollOption{1, "Something"}, @@ -213,7 +226,7 @@ func CompileTemplates() error { } var topicsList []*TopicsRow - topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, "Date", time.Now(), "Date", user3.ID, 1, "", "127.0.0.1", 0, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"}) + topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, "Date", user3.ID, 1, "", "127.0.0.1", 0, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"}) header2.Title = "Topic List" topicListPage := TopicListPage{header, topicsList, forumList, Config.DefaultForum, Paginator{[]int{1}, 1, 1}} topicListTmpl, err := c.Compile("topics.html", "templates/", "common.TopicListPage", topicListPage, varList) @@ -310,9 +323,11 @@ func CompileJSTemplates() error { log.Print("Compiling the JS templates") var config tmpl.CTemplateConfig config.Minify = Config.MinifyTemplates + config.Debug = Dev.DebugMode config.SuperDebug = Dev.TemplateDebug config.SkipHandles = true - config.SkipInitBlock = true + config.SkipTmplPtrMap = true + config.SkipInitBlock = false config.PackageName = "tmpl" c := tmpl.NewCTemplateSet() @@ -321,6 +336,11 @@ func CompileJSTemplates() error { "io": "io", "../common/alerts": "../common/alerts", }) + c.SetBuildTags("!no_templategen") + + user, user2, user3 := tmplInitUsers() + header, _, _ := tmplInitHeaders(user, user2, user3) + now := time.Now() var varList = make(map[string]tmpl.VarItem) // TODO: Check what sort of path is sent exactly and use it here @@ -330,6 +350,39 @@ func CompileJSTemplates() error { return err } + c.SetBaseImportMap(map[string]string{ + "io": "io", + "../common": "../common", + }) + // TODO: Fix the import loop so we don't have to use this hack anymore + c.SetBuildTags("!no_templategen,tmplgentopic") + + var topicsRow = &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, "Date", user3.ID, 1, "", "127.0.0.1", 0, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"} + topicListItemTmpl, err := c.Compile("topics_topic.html", "templates/", "*common.TopicsRow", topicsRow, varList) + if err != nil { + return err + } + + poll := Poll{ID: 1, Type: 0, Options: map[int]string{0: "Nothing", 1: "Something"}, Results: map[int]int{0: 5, 1: 2}, QuickOptions: []PollOption{ + PollOption{0, "Nothing"}, + PollOption{1, "Something"}, + }, VoteCount: 7} + topic := TopicUser{1, "blah", "Blah", "Hey there!", 0, false, false, now, RelativeTime(now), now, RelativeTime(now), 0, "", "127.0.0.1", 0, 1, "classname", poll.ID, "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, now, RelativeTime(now), 0, 0, "", "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""}) + + varList = make(map[string]tmpl.VarItem) + header.Title = "Topic Name" + tpage := TopicPage{header, replyList, topic, poll, 1, 1} + topicIDTmpl, err := c.Compile("topic_posts.html", "templates/", "common.TopicPage", tpage, varList) + if err != nil { + return err + } + topicIDAltTmpl, err := c.Compile("topic_alt_posts.html", "templates/", "common.TopicPage", tpage, varList) + if err != nil { + return err + } + var dirPrefix = "./tmpl_client/" var wg sync.WaitGroup var writeTemplate = func(name string, content string) { @@ -348,6 +401,9 @@ func CompileJSTemplates() error { }() } writeTemplate("alert", alertTmpl) + writeTemplate("topics_topic", topicListItemTmpl) + writeTemplate("topic_posts", topicIDTmpl) + writeTemplate("topic_alt_posts", topicIDAltTmpl) writeTemplateList(c, &wg, dirPrefix) return nil } diff --git a/common/templates/templates.go b/common/templates/templates.go index c8be011d..913d27c2 100644 --- a/common/templates/templates.go +++ b/common/templates/templates.go @@ -28,12 +28,13 @@ type VarItemReflect struct { } type CTemplateConfig struct { - Minify bool - Debug bool - SuperDebug bool - SkipHandles bool - SkipInitBlock bool - PackageName string + Minify bool + Debug bool + SuperDebug bool + SkipHandles bool + SkipTmplPtrMap bool + SkipInitBlock bool + PackageName string } // nolint @@ -58,6 +59,7 @@ type CTemplateSet struct { //tempVars map[string]string config CTemplateConfig baseImportMap map[string]string + buildTags string expectsInt interface{} } @@ -103,6 +105,10 @@ func (c *CTemplateSet) SetBaseImportMap(importMap map[string]string) { c.baseImportMap = importMap } +func (c *CTemplateSet) SetBuildTags(tags string) { + c.buildTags = tags +} + func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expectsInt interface{}, varList map[string]VarItem, imports ...string) (out string, err error) { if c.config.Debug { fmt.Println("Compiling template '" + name + "'") @@ -179,7 +185,12 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe varString += "var " + varItem.Name + " " + varItem.Type + " = " + varItem.Destination + "\n" } - fout := "// +build !no_templategen\n\n// Code generated by Gosora. More below:\n/* This file was automatically generated by the software. Please don't edit it as your changes may be overwritten at any moment. */\n" + var fout string + if c.buildTags != "" { + fout += "// +build " + c.buildTags + "\n\n" + } + + fout += "// Code generated by Gosora. More below:\n/* This file was automatically generated by the software. Please don't edit it as your changes may be overwritten at any moment. */\n" fout += "package " + c.config.PackageName + "\n" + importList + "\n" if !c.config.SkipInitBlock { @@ -194,7 +205,9 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe fout += "\tcommon.Ctemplates = append(common.Ctemplates,\"" + fname + "\")\n\tcommon.TmplPtrMap[\"" + fname + "\"] = &common.Template_" + fname + "_handle\n" } - fout += "\tcommon.TmplPtrMap[\"o_" + fname + "\"] = Template_" + fname + "\n" + if !c.config.SkipTmplPtrMap { + fout += "\tcommon.TmplPtrMap[\"o_" + fname + "\"] = Template_" + fname + "\n" + } if len(c.langIndexToName) > 0 { fout += "\t" + fname + "_tmpl_phrase_id = common.RegisterTmplPhraseNames([]string{\n" for _, name := range c.langIndexToName { @@ -805,6 +818,7 @@ func (c *CTemplateSet) compileIfVarsub(varname string, varholder string, templat out += ".(" + cur.Type().Name() + ")" } if !cur.IsValid() { + fmt.Println("cur: ", cur) panic(out + "^\n" + "Invalid value. Maybe, it doesn't exist?") } c.detail("Data Kind:", cur.Kind()) diff --git a/common/theme_list.go b/common/theme_list.go index 5aa9b23c..89f91f5c 100644 --- a/common/theme_list.go +++ b/common/theme_list.go @@ -342,7 +342,7 @@ func RunThemeTemplate(theme string, template string, pi interface{}, w io.Writer return tmplO(pi.(ErrorPage), w) case func(Page, io.Writer) error: return tmplO(pi.(Page), w) - case string: + case nil, string: mapping, ok := Themes[DefaultThemeBox.Load().(string)].TemplatesMap[template] if !ok { mapping = template @@ -370,14 +370,21 @@ func RunThemeTemplate(theme string, template string, pi interface{}, w io.Writer // GetThemeTemplate attempts to get the template for a specific theme, otherwise it falls back on the default template pointer, which if absent will fallback onto the template interpreter func GetThemeTemplate(theme string, template string) interface{} { + // TODO: Figure out why we're getting a nil pointer here when transpiled templates are disabled, I would have assumed that we would just fall back to !ok on this + // Might have something to do with it being the theme's TmplPtr map, investigate. tmpl, ok := Themes[theme].TmplPtr[template] if ok { + //fmt.Println("tmpl: ", tmpl) + //fmt.Println("exiting at Themes[theme].TmplPtr[template]") return tmpl } + tmpl, ok = TmplPtrMap[template] if ok { + //fmt.Println("exiting at TmplPtrMap[template]") return tmpl } + //fmt.Println("just passing back the template name") return template } diff --git a/common/topic.go b/common/topic.go index 2830fdc4..5d180c47 100644 --- a/common/topic.go +++ b/common/topic.go @@ -81,14 +81,15 @@ type TopicUser struct { } type TopicsRow struct { - ID int - Link string - Title string - Content string - CreatedBy int - IsClosed bool - Sticky bool - CreatedAt string + ID int + Link string + Title string + Content string + CreatedBy int + IsClosed bool + Sticky bool + CreatedAt time.Time + //RelativeCreatedAt string LastReplyAt time.Time RelativeLastReplyAt string LastReplyBy int @@ -109,6 +110,31 @@ type TopicsRow struct { ForumLink string } +type WsTopicsRow struct { + ID int + Link string + Title string + CreatedBy int + IsClosed bool + Sticky bool + CreatedAt time.Time + LastReplyAt time.Time + RelativeLastReplyAt string + LastReplyBy int + ParentID int + PostCount int + LikeCount int + ClassName string + Creator *WsJSONUser + LastUser *WsJSONUser + ForumName string + ForumLink string +} + +func (row *TopicsRow) WebSockets() *WsTopicsRow { + return &WsTopicsRow{row.ID, row.Link, row.Title, row.CreatedBy, row.IsClosed, row.Sticky, row.CreatedAt, row.LastReplyAt, row.RelativeLastReplyAt, row.LastReplyBy, row.ParentID, row.PostCount, row.LikeCount, row.ClassName, row.Creator.WebSockets(), row.LastUser.WebSockets(), row.ForumName, row.ForumLink} +} + type TopicStmts struct { addRepliesToTopic *sql.Stmt lock *sql.Stmt @@ -302,6 +328,7 @@ func (topic *Topic) Copy() Topic { return *topic } +// TODO: Load LastReplyAt? func TopicByReplyID(rid int) (*Topic, error) { topic := Topic{ID: 0} err := topicStmts.getByReplyID.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.Poll, &topic.Data) @@ -310,6 +337,7 @@ func TopicByReplyID(rid int) (*Topic, error) { } // TODO: Refactor the caller to take a Topic and a User rather than a combined TopicUser +// TODO: Load LastReplyAt everywhere in here? func GetTopicUser(tid int) (TopicUser, error) { tcache := Topics.GetCache() ucache := Users.GetCache() diff --git a/common/topic_list.go b/common/topic_list.go index 436adfcc..6c120baa 100644 --- a/common/topic_list.go +++ b/common/topic_list.go @@ -3,6 +3,7 @@ package common import ( "strconv" "sync" + "sync/atomic" "../query_gen/lib" ) @@ -16,119 +17,82 @@ type TopicListHolder struct { } type TopicListInt interface { + GetListByCanSee(canSee []int, page int) (topicList []*TopicsRow, forumList []Forum, paginator Paginator, err error) GetListByGroup(group *Group, page int) (topicList []*TopicsRow, forumList []Forum, paginator Paginator, err error) GetList(page int) (topicList []*TopicsRow, forumList []Forum, paginator Paginator, err error) + + RebuildPermTree() error } type DefaultTopicList struct { + // TODO: Rewrite this to put permTree as the primary and put canSeeStr on each group? oddGroups map[int]*TopicListHolder evenGroups map[int]*TopicListHolder oddLock sync.RWMutex evenLock sync.RWMutex - groupList []int // TODO: Use an atomic.Value instead to allow this to be updated on long ticks + permTree atomic.Value // [string(canSee)]canSee + //permTree map[string][]int // [string(canSee)]canSee } +// We've removed the topic list cache cap as admins really shouldn't be abusing groups like this with plugin_guilds around and it was extremely fiddly. +// If this becomes a problem later on, then we can revisit this with a fresh perspective, particularly with regards to what people expect a group to really be +// Also, keep in mind that as-long as the groups don't all have unique sets of forums they can see, then we can optimise a large portion of the work away. func NewDefaultTopicList() (*DefaultTopicList, error) { tList := &DefaultTopicList{ oddGroups: make(map[int]*TopicListHolder), evenGroups: make(map[int]*TopicListHolder), } - var slots = make([]int, 8) // Only cache the topic list for eight groups - - // TODO: Do something more efficient than this - allGroups, err := Groups.GetAll() + err := tList.RebuildPermTree() if err != nil { return nil, err } - - if len(allGroups) > 0 { - var stopHere int - if len(allGroups) <= 8 { - stopHere = len(allGroups) - } else { - stopHere = 8 - } - - var lowest = allGroups[0].UserCount - for i := 0; i < stopHere; i++ { - slots[i] = i - if allGroups[i].UserCount < lowest { - lowest = allGroups[i].UserCount - } - } - - var findNewLowest = func() { - for _, slot := range slots { - if allGroups[slot].UserCount < lowest { - lowest = allGroups[slot].UserCount - } - } - } - - for i := 8; i < len(allGroups); i++ { - if allGroups[i].UserCount > lowest { - for ii, slot := range slots { - if allGroups[i].UserCount > slot { - slots[ii] = i - lowest = allGroups[i].UserCount - findNewLowest() - break - } - } - } - } - - tList.groupList = slots - } - err = tList.Tick() if err != nil { return nil, err } + AddScheduledHalfSecondTask(tList.Tick) //AddScheduledSecondTask(tList.GroupCountTick) // TODO: Dynamically change the groups in the short list to be optimised every second return tList, nil } -// TODO: Add support for groups other than the guest group func (tList *DefaultTopicList) Tick() error { var oddLists = make(map[int]*TopicListHolder) var evenLists = make(map[int]*TopicListHolder) - var addList = func(gid int, topicList []*TopicsRow, forumList []Forum, paginator Paginator) { + var addList = func(gid int, holder *TopicListHolder) { if gid%2 == 0 { - evenLists[gid] = &TopicListHolder{topicList, forumList, paginator} + evenLists[gid] = holder } else { - oddLists[gid] = &TopicListHolder{topicList, forumList, paginator} + oddLists[gid] = holder } } - guestGroup, err := Groups.Get(GuestUser.Group) - if err != nil { - return err - } - topicList, forumList, paginator, err := tList.getListByGroup(guestGroup, 1) - if err != nil { - return err - } - addList(guestGroup.ID, topicList, forumList, paginator) - - for _, gid := range tList.groupList { - group, err := Groups.Get(gid) // TODO: Bulk load the groups? + var canSeeHolders = make(map[string]*TopicListHolder) + for name, canSee := range tList.permTree.Load().(map[string][]int) { + topicList, forumList, paginator, err := tList.GetListByCanSee(canSee, 1) if err != nil { return err } - if group.UserCount == 0 { + canSeeHolders[name] = &TopicListHolder{topicList, forumList, paginator} + } + + allGroups, err := Groups.GetAll() + if err != nil { + return err + } + for _, group := range allGroups { + // ? - Move the user count check to instance initialisation? Might require more book-keeping, particularly when a user moves into a zero user group + if group.UserCount == 0 && group.ID != GuestUser.Group { continue } - - topicList, forumList, paginator, err := tList.getListByGroup(group, 1) - if err != nil { - return err + var canSee = make([]byte, len(group.CanSee)) + for i, item := range group.CanSee { + canSee[i] = byte(item) } - addList(group.ID, topicList, forumList, paginator) + addList(group.ID, canSeeHolders[string(canSee)]) } tList.oddLock.Lock() @@ -142,6 +106,28 @@ func (tList *DefaultTopicList) Tick() error { return nil } +func (tList *DefaultTopicList) RebuildPermTree() error { + // TODO: Do something more efficient than this + allGroups, err := Groups.GetAll() + if err != nil { + return err + } + + var permTree = make(map[string][]int) // [string(canSee)]canSee + for _, group := range allGroups { + var canSee = make([]byte, len(group.CanSee)) + for i, item := range group.CanSee { + canSee[i] = byte(item) + } + var canSeeInt = make([]int, len(canSee)) + copy(canSeeInt, group.CanSee) + permTree[string(canSee)] = canSeeInt + } + tList.permTree.Store(permTree) + + return nil +} + func (tList *DefaultTopicList) GetListByGroup(group *Group, page int) (topicList []*TopicsRow, forumList []Forum, paginator Paginator, err error) { // TODO: Cache the first three pages not just the first along with all the topics on this beaten track if page == 1 { @@ -161,13 +147,11 @@ func (tList *DefaultTopicList) GetListByGroup(group *Group, page int) (topicList } } - return tList.getListByGroup(group, page) + // TODO: Make CanSee a method on *Group with a canSee field? Have a CanSee method on *User to cover the case of superadmins? + return tList.GetListByCanSee(group.CanSee, page) } -func (tList *DefaultTopicList) getListByGroup(group *Group, page int) (topicList []*TopicsRow, forumList []Forum, paginator Paginator, err error) { - // TODO: Make CanSee a method on *Group with a canSee field? Have a CanSee method on *User to cover the case of superadmins? - canSee := group.CanSee - +func (tList *DefaultTopicList) GetListByCanSee(canSee []int, page int) (topicList []*TopicsRow, forumList []Forum, paginator Paginator, err error) { // We need a list of the visible forums for Quick Topic // ? - Would it be useful, if we could post in social groups from /topics/? for _, fid := range canSee { @@ -252,11 +236,12 @@ func (tList *DefaultTopicList) getList(page int, argList []interface{}, qlist st } topicItem.Link = BuildTopicURL(NameToSlug(topicItem.Title), topicItem.ID) + // TODO: Pass forum to something like topicItem.Forum and use that instead of these two properties? Could be more flexible. forum := Forums.DirtyGet(topicItem.ParentID) topicItem.ForumName = forum.Name topicItem.ForumLink = forum.Link - //topicItem.CreatedAt = RelativeTime(topicItem.CreatedAt) + //topicItem.RelativeCreatedAt = RelativeTime(topicItem.CreatedAt) topicItem.RelativeLastReplyAt = RelativeTime(topicItem.LastReplyAt) // TODO: Rename this Vhook to better reflect moving the topic list from /routes/ to /common/ diff --git a/common/user.go b/common/user.go index 5fe59af9..37e0a46c 100644 --- a/common/user.go +++ b/common/user.go @@ -53,6 +53,28 @@ type User struct { TempGroup int } +func (user *User) WebSockets() *WsJSONUser { + var groupID = user.Group + if user.TempGroup != 0 { + groupID = user.TempGroup + } + // TODO: Do we want to leak the user's permissions? Users will probably be able to see their status from the group tags, but still + return &WsJSONUser{user.ID, user.Link, user.Name, groupID, user.IsMod, user.Avatar, user.Level, user.Score, user.Liked} +} + +// Use struct tags to avoid having to define this? It really depends on the circumstances, sometimes we want the whole thing, sometimes... not. +type WsJSONUser struct { + ID int + Link string + Name string + Group int // Be sure to mask with TempGroup + IsMod bool + Avatar string + Level int + Score int + Liked int +} + type UserStmts struct { activate *sql.Stmt changeGroup *sql.Stmt diff --git a/common/websockets.go b/common/websockets.go index c96f7ef8..55fee2fa 100644 --- a/common/websockets.go +++ b/common/websockets.go @@ -10,6 +10,7 @@ package common import ( "bytes" + "encoding/json" "errors" "fmt" "net/http" @@ -28,54 +29,218 @@ type WSUser struct { User *User } -type WSHub struct { +// TODO: Make this an interface? +type WsHubImpl struct { + // TODO: Shard this map OnlineUsers map[int]*WSUser OnlineGuests map[*WSUser]bool GuestLock sync.RWMutex UserLock sync.RWMutex + + lastTick time.Time + lastTopicList []*TopicsRow } // TODO: Disable WebSockets on high load? Add a Control Panel interface for disabling it? var EnableWebsockets = true // Put this in caps for consistency with the other constants? -var WsHub WSHub +// TODO: Rename this to WebSockets? +var WsHub WsHubImpl var wsUpgrader = websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024} var errWsNouser = errors.New("This user isn't connected via WebSockets") func init() { adminStatsWatchers = make(map[*WSUser]bool) - WsHub = WSHub{ + topicListWatchers = make(map[*WSUser]bool) + // TODO: Do we really want to initialise this here instead of in main.go / general_test.go like the other things? + WsHub = WsHubImpl{ OnlineUsers: make(map[int]*WSUser), OnlineGuests: make(map[*WSUser]bool), } } -func (hub *WSHub) GuestCount() int { +func (hub *WsHubImpl) Start() { + //fmt.Println("running hub.Start") + if Config.DisableLiveTopicList { + return + } + hub.lastTick = time.Now() + AddScheduledSecondTask(hub.Tick) +} + +type WsTopicList struct { + Topics []*WsTopicsRow +} + +// This Tick is seperate from the admin one, as we want to process that in parallel with this due to the blocking calls to gopsutil +func (hub *WsHubImpl) Tick() error { + //fmt.Println("running hub.Tick") + + // Don't waste CPU time if nothing has happened + // TODO: Get a topic list method which strips stickies? + tList, _, _, err := TopicList.GetList(1) + if err != nil { + hub.lastTick = time.Now() + return err // TODO: Do we get ErrNoRows here? + } + defer func() { + hub.lastTick = time.Now() + hub.lastTopicList = tList + }() + if len(tList) == 0 { + return nil + } + + //fmt.Println("checking for changes") + // TODO: Optimise this by only sniffing the top non-sticky + if len(tList) == len(hub.lastTopicList) { + var hasItem = false + for j, tItem := range tList { + if !tItem.Sticky { + if tItem.ID != hub.lastTopicList[j].ID { + hasItem = true + } + } + } + if !hasItem { + return nil + } + } + + // TODO: Implement this for guests too? Should be able to optimise it far better there due to them sharing the same permission set + // TODO: Be less aggressive with the locking, maybe use an array of sorts instead of hitting the main map every-time + topicListMutex.RLock() + if len(topicListWatchers) == 0 { + //fmt.Println("no watchers") + topicListMutex.RUnlock() + return nil + } + //fmt.Println("found changes") + + // Copy these over so we close this loop as fast as possible so we can release the read lock, especially if the group gets are backed by calls to the database + var groupIDs = make(map[int]bool) + var currentWatchers = make([]*WSUser, len(topicListWatchers)) + var i = 0 + for wsUser, _ := range topicListWatchers { + currentWatchers[i] = wsUser + groupIDs[wsUser.User.Group] = true + i++ + } + topicListMutex.RUnlock() + + var groups = make(map[int]*Group) + var canSeeMap = make(map[string][]int) + for groupID, _ := range groupIDs { + group, err := Groups.Get(groupID) + if err != nil { + // TODO: Do we really want to halt all pushes for what is possibly just one user? + return err + } + groups[group.ID] = group + + var canSee = make([]byte, len(group.CanSee)) + for i, item := range group.CanSee { + canSee[i] = byte(item) + } + canSeeMap[string(canSee)] = group.CanSee + } + + var canSeeRenders = make(map[string][]byte) + for name, canSee := range canSeeMap { + topicList, forumList, _, err := TopicList.GetListByCanSee(canSee, 1) + if err != nil { + return err // TODO: Do we get ErrNoRows here? + } + if len(topicList) == 0 { + continue + } + _ = forumList // Might use this later after we get the base feature working + + //fmt.Println("canSeeItem") + if topicList[0].Sticky { + var lastSticky = 0 + for i, row := range topicList { + if !row.Sticky { + lastSticky = i + break + } + } + if lastSticky == 0 { + continue + } + //fmt.Println("lastSticky: ", lastSticky) + //fmt.Println("before topicList: ", topicList) + topicList = topicList[lastSticky:] + //fmt.Println("after topicList: ", topicList) + } + + // TODO: Compare to previous tick to eliminate unnecessary work and data + var wsTopicList = make([]*WsTopicsRow, len(topicList)) + for i, topicRow := range topicList { + wsTopicList[i] = topicRow.WebSockets() + } + + outBytes, err := json.Marshal(&WsTopicList{wsTopicList}) + if err != nil { + return err + } + canSeeRenders[name] = outBytes + } + + // TODO: Use MessagePack for additional speed? + //fmt.Println("writing to the clients") + for _, wsUser := range currentWatchers { + group := groups[wsUser.User.Group] + var canSee = make([]byte, len(group.CanSee)) + for i, item := range group.CanSee { + canSee[i] = byte(item) + } + + w, err := wsUser.conn.NextWriter(websocket.TextMessage) + if err != nil { + //fmt.Printf("werr for #%d: %s\n", wsUser.User.ID, err) + topicListMutex.Lock() + delete(topicListWatchers, wsUser) + topicListMutex.Unlock() + continue + } + + //fmt.Println("writing to user #", wsUser.User.ID) + outBytes := canSeeRenders[string(canSee)] + //fmt.Println("outBytes: ", string(outBytes)) + w.Write(outBytes) + w.Close() + } + return nil +} + +func (hub *WsHubImpl) GuestCount() int { defer hub.GuestLock.RUnlock() hub.GuestLock.RLock() return len(hub.OnlineGuests) } -func (hub *WSHub) UserCount() int { +func (hub *WsHubImpl) UserCount() int { defer hub.UserLock.RUnlock() hub.UserLock.RLock() return len(hub.OnlineUsers) } -func (hub *WSHub) broadcastMessage(msg string) error { +func (hub *WsHubImpl) broadcastMessage(msg string) error { hub.UserLock.RLock() + defer hub.UserLock.RUnlock() for _, wsUser := range hub.OnlineUsers { w, err := wsUser.conn.NextWriter(websocket.TextMessage) if err != nil { return err } _, _ = w.Write([]byte(msg)) + w.Close() } - hub.UserLock.RUnlock() return nil } -func (hub *WSHub) pushMessage(targetUser int, msg string) error { +func (hub *WsHubImpl) pushMessage(targetUser int, msg string) error { hub.UserLock.RLock() wsUser, ok := hub.OnlineUsers[targetUser] hub.UserLock.RUnlock() @@ -93,7 +258,7 @@ func (hub *WSHub) pushMessage(targetUser int, msg string) error { return nil } -func (hub *WSHub) pushAlert(targetUser int, asid int, event string, elementType string, actorID int, targetUserID int, elementID int) error { +func (hub *WsHubImpl) pushAlert(targetUser int, asid int, event string, elementType string, actorID int, targetUserID int, elementID int) error { //log.Print("In pushAlert") hub.UserLock.RLock() wsUser, ok := hub.OnlineUsers[targetUser] @@ -119,7 +284,7 @@ func (hub *WSHub) pushAlert(targetUser int, asid int, event string, elementType return nil } -func (hub *WSHub) pushAlerts(users []int, asid int, event string, elementType string, actorID int, targetUserID int, elementID int) error { +func (hub *WsHubImpl) pushAlerts(users []int, asid int, event string, elementType string, actorID int, targetUserID int, elementID int) error { var wsUsers []*WSUser hub.UserLock.RLock() // We don't want to keep a lock on this for too long, so we'll accept some nil pointers @@ -193,6 +358,7 @@ func RouteWebsockets(w http.ResponseWriter, r *http.Request, user User) RouteErr delete(WsHub.OnlineGuests, wsUser) WsHub.GuestLock.Unlock() } else { + // TODO: Make sure the admin is removed from the admin stats list in the case that an error happens WsHub.UserLock.Lock() delete(WsHub.OnlineUsers, user.ID) WsHub.UserLock.Unlock() @@ -229,26 +395,17 @@ func RouteWebsockets(w http.ResponseWriter, r *http.Request, user User) RouteErr return nil } +// TODO: Use a map instead of a switch to make this more modular? func wsPageResponses(wsUser *WSUser, page []byte) { + //fmt.Println("entering page: ", string(page)) switch string(page) { + // Live Topic List is an experimental feature + // TODO: Optimise this to reduce the amount of contention + case "/topics/": + topicListMutex.Lock() + topicListWatchers[wsUser] = true + topicListMutex.Unlock() case "/panel/": - //log.Print("/panel/ WS Route") - /*w, err := wsUser.conn.NextWriter(websocket.TextMessage) - if err != nil { - //log.Print(err.Error()) - return - } - - log.Print(WsHub.online_users) - uonline := WsHub.UserCount() - gonline := WsHub.GuestCount() - totonline := uonline + gonline - - w.Write([]byte("set #dash-totonline " + strconv.Itoa(totonline) + " online\r")) - w.Write([]byte("set #dash-gonline " + strconv.Itoa(gonline) + " guests online\r")) - w.Write([]byte("set #dash-uonline " + strconv.Itoa(uonline) + " users online\r")) - w.Close()*/ - // Listen for changes and inform the admins... adminStatsMutex.Lock() watchers := len(adminStatsWatchers) @@ -260,8 +417,15 @@ func wsPageResponses(wsUser *WSUser, page []byte) { } } +// TODO: Use a map instead of a switch to make this more modular? func wsLeavePage(wsUser *WSUser, page []byte) { + //fmt.Println("leaving page: ", string(page)) switch string(page) { + // Live Topic List is an experimental feature + case "/topics/": + topicListMutex.Lock() + delete(topicListWatchers, wsUser) + topicListMutex.Unlock() case "/panel/": adminStatsMutex.Lock() delete(adminStatsWatchers, wsUser) @@ -269,6 +433,10 @@ func wsLeavePage(wsUser *WSUser, page []byte) { } } +// TODO: Abstract this +// TODO: Use odd-even sharding +var topicListWatchers map[*WSUser]bool +var topicListMutex sync.RWMutex var adminStatsWatchers map[*WSUser]bool var adminStatsMutex sync.RWMutex @@ -385,20 +553,18 @@ AdminStatLoop: } } - adminStatsMutex.RLock() - watchers := adminStatsWatchers - adminStatsMutex.RUnlock() - - for watcher := range watchers { + // Acquire a write lock for now, so we can handle the delete() case below and the read one simultaneously + // TODO: Stop taking a write lock here if it isn't necessary + adminStatsMutex.Lock() + for watcher := range adminStatsWatchers { w, err := watcher.conn.NextWriter(websocket.TextMessage) if err != nil { - adminStatsMutex.Lock() delete(adminStatsWatchers, watcher) - adminStatsMutex.Unlock() continue } // nolint + // TODO: Use JSON for this to make things more portable and easier to convert to MessagePack, if need be? if !noStatUpdates { w.Write([]byte("set #dash-totonline " + strconv.Itoa(totonline) + totunit + " online\r")) w.Write([]byte("set #dash-gonline " + strconv.Itoa(gonline) + gunit + " guests online\r")) @@ -421,6 +587,7 @@ AdminStatLoop: w.Close() } + adminStatsMutex.Unlock() lastUonline = uonline lastGonline = gonline diff --git a/gen_router.go b/gen_router.go index 5707a637..1a9d1434 100644 --- a/gen_router.go +++ b/gen_router.go @@ -20,7 +20,6 @@ import ( var ErrNoRoute = errors.New("That route doesn't exist.") // TODO: What about the /uploads/ route? x.x var RouteMap = map[string]interface{}{ - "routeAPI": routeAPI, "routes.Overview": routes.Overview, "routes.CustomPage": routes.CustomPage, "routes.ForumList": routes.ForumList, @@ -28,6 +27,8 @@ var RouteMap = map[string]interface{}{ "routes.ChangeTheme": routes.ChangeTheme, "routes.ShowAttachment": routes.ShowAttachment, "common.RouteWebsockets": common.RouteWebsockets, + "routeAPIPhrases": routeAPIPhrases, + "routeAPI": routeAPI, "routes.ReportSubmit": routes.ReportSubmit, "routes.CreateTopic": routes.CreateTopic, "routes.TopicList": routes.TopicList, @@ -147,254 +148,256 @@ var RouteMap = map[string]interface{}{ // ! NEVER RELY ON THESE REMAINING THE SAME BETWEEN COMMITS var routeMapEnum = map[string]int{ - "routeAPI": 0, - "routes.Overview": 1, - "routes.CustomPage": 2, - "routes.ForumList": 3, - "routes.ViewForum": 4, - "routes.ChangeTheme": 5, - "routes.ShowAttachment": 6, - "common.RouteWebsockets": 7, - "routes.ReportSubmit": 8, - "routes.CreateTopic": 9, - "routes.TopicList": 10, - "panel.Forums": 11, - "panel.ForumsCreateSubmit": 12, - "panel.ForumsDelete": 13, - "panel.ForumsDeleteSubmit": 14, - "panel.ForumsEdit": 15, - "panel.ForumsEditSubmit": 16, - "panel.ForumsEditPermsSubmit": 17, - "panel.ForumsEditPermsAdvance": 18, - "panel.ForumsEditPermsAdvanceSubmit": 19, - "panel.Settings": 20, - "panel.SettingEdit": 21, - "panel.SettingEditSubmit": 22, - "routePanelWordFilters": 23, - "routePanelWordFiltersCreateSubmit": 24, - "routePanelWordFiltersEdit": 25, - "routePanelWordFiltersEditSubmit": 26, - "routePanelWordFiltersDeleteSubmit": 27, - "panel.Pages": 28, - "panel.PagesCreateSubmit": 29, - "panel.PagesEdit": 30, - "panel.PagesEditSubmit": 31, - "panel.PagesDeleteSubmit": 32, - "routePanelThemes": 33, - "routePanelThemesSetDefault": 34, - "routePanelThemesMenus": 35, - "routePanelThemesMenusEdit": 36, - "routePanelThemesMenuItemEdit": 37, - "routePanelThemesMenuItemEditSubmit": 38, - "routePanelThemesMenuItemCreateSubmit": 39, - "routePanelThemesMenuItemDeleteSubmit": 40, - "routePanelThemesMenuItemOrderSubmit": 41, - "routePanelPlugins": 42, - "routePanelPluginsActivate": 43, - "routePanelPluginsDeactivate": 44, - "routePanelPluginsInstall": 45, - "panel.Users": 46, - "panel.UsersEdit": 47, - "panel.UsersEditSubmit": 48, - "panel.AnalyticsViews": 49, - "panel.AnalyticsRoutes": 50, - "panel.AnalyticsAgents": 51, - "panel.AnalyticsSystems": 52, - "panel.AnalyticsLanguages": 53, - "panel.AnalyticsReferrers": 54, - "panel.AnalyticsRouteViews": 55, - "panel.AnalyticsAgentViews": 56, - "panel.AnalyticsForumViews": 57, - "panel.AnalyticsSystemViews": 58, - "panel.AnalyticsLanguageViews": 59, - "panel.AnalyticsReferrerViews": 60, - "panel.AnalyticsPosts": 61, - "panel.AnalyticsTopics": 62, - "panel.AnalyticsForums": 63, - "routePanelGroups": 64, - "routePanelGroupsEdit": 65, - "routePanelGroupsEditPerms": 66, - "routePanelGroupsEditSubmit": 67, - "routePanelGroupsEditPermsSubmit": 68, - "routePanelGroupsCreateSubmit": 69, - "panel.Backups": 70, - "panel.LogsRegs": 71, - "panel.LogsMod": 72, - "panel.Debug": 73, - "routePanelDashboard": 74, - "routes.AccountEdit": 75, - "routes.AccountEditPassword": 76, - "routes.AccountEditPasswordSubmit": 77, - "routes.AccountEditAvatarSubmit": 78, - "routes.AccountEditUsernameSubmit": 79, - "routes.AccountEditMFA": 80, - "routes.AccountEditMFASetup": 81, - "routes.AccountEditMFASetupSubmit": 82, - "routes.AccountEditMFADisableSubmit": 83, - "routes.AccountEditEmail": 84, - "routes.AccountEditEmailTokenSubmit": 85, - "routes.ViewProfile": 86, - "routes.BanUserSubmit": 87, - "routes.UnbanUser": 88, - "routes.ActivateUser": 89, - "routes.IPSearch": 90, - "routes.CreateTopicSubmit": 91, - "routes.EditTopicSubmit": 92, - "routes.DeleteTopicSubmit": 93, - "routes.StickTopicSubmit": 94, - "routes.UnstickTopicSubmit": 95, - "routes.LockTopicSubmit": 96, - "routes.UnlockTopicSubmit": 97, - "routes.MoveTopicSubmit": 98, - "routes.LikeTopicSubmit": 99, - "routes.ViewTopic": 100, - "routes.CreateReplySubmit": 101, - "routes.ReplyEditSubmit": 102, - "routes.ReplyDeleteSubmit": 103, - "routes.ReplyLikeSubmit": 104, - "routes.ProfileReplyCreateSubmit": 105, - "routes.ProfileReplyEditSubmit": 106, - "routes.ProfileReplyDeleteSubmit": 107, - "routes.PollVote": 108, - "routes.PollResults": 109, - "routes.AccountLogin": 110, - "routes.AccountRegister": 111, - "routes.AccountLogout": 112, - "routes.AccountLoginSubmit": 113, - "routes.AccountLoginMFAVerify": 114, - "routes.AccountLoginMFAVerifySubmit": 115, - "routes.AccountRegisterSubmit": 116, - "routes.DynamicRoute": 117, - "routes.UploadedFile": 118, - "routes.StaticFile": 119, - "routes.RobotsTxt": 120, - "routes.SitemapXml": 121, - "routes.BadRoute": 122, + "routes.Overview": 0, + "routes.CustomPage": 1, + "routes.ForumList": 2, + "routes.ViewForum": 3, + "routes.ChangeTheme": 4, + "routes.ShowAttachment": 5, + "common.RouteWebsockets": 6, + "routeAPIPhrases": 7, + "routeAPI": 8, + "routes.ReportSubmit": 9, + "routes.CreateTopic": 10, + "routes.TopicList": 11, + "panel.Forums": 12, + "panel.ForumsCreateSubmit": 13, + "panel.ForumsDelete": 14, + "panel.ForumsDeleteSubmit": 15, + "panel.ForumsEdit": 16, + "panel.ForumsEditSubmit": 17, + "panel.ForumsEditPermsSubmit": 18, + "panel.ForumsEditPermsAdvance": 19, + "panel.ForumsEditPermsAdvanceSubmit": 20, + "panel.Settings": 21, + "panel.SettingEdit": 22, + "panel.SettingEditSubmit": 23, + "routePanelWordFilters": 24, + "routePanelWordFiltersCreateSubmit": 25, + "routePanelWordFiltersEdit": 26, + "routePanelWordFiltersEditSubmit": 27, + "routePanelWordFiltersDeleteSubmit": 28, + "panel.Pages": 29, + "panel.PagesCreateSubmit": 30, + "panel.PagesEdit": 31, + "panel.PagesEditSubmit": 32, + "panel.PagesDeleteSubmit": 33, + "routePanelThemes": 34, + "routePanelThemesSetDefault": 35, + "routePanelThemesMenus": 36, + "routePanelThemesMenusEdit": 37, + "routePanelThemesMenuItemEdit": 38, + "routePanelThemesMenuItemEditSubmit": 39, + "routePanelThemesMenuItemCreateSubmit": 40, + "routePanelThemesMenuItemDeleteSubmit": 41, + "routePanelThemesMenuItemOrderSubmit": 42, + "routePanelPlugins": 43, + "routePanelPluginsActivate": 44, + "routePanelPluginsDeactivate": 45, + "routePanelPluginsInstall": 46, + "panel.Users": 47, + "panel.UsersEdit": 48, + "panel.UsersEditSubmit": 49, + "panel.AnalyticsViews": 50, + "panel.AnalyticsRoutes": 51, + "panel.AnalyticsAgents": 52, + "panel.AnalyticsSystems": 53, + "panel.AnalyticsLanguages": 54, + "panel.AnalyticsReferrers": 55, + "panel.AnalyticsRouteViews": 56, + "panel.AnalyticsAgentViews": 57, + "panel.AnalyticsForumViews": 58, + "panel.AnalyticsSystemViews": 59, + "panel.AnalyticsLanguageViews": 60, + "panel.AnalyticsReferrerViews": 61, + "panel.AnalyticsPosts": 62, + "panel.AnalyticsTopics": 63, + "panel.AnalyticsForums": 64, + "routePanelGroups": 65, + "routePanelGroupsEdit": 66, + "routePanelGroupsEditPerms": 67, + "routePanelGroupsEditSubmit": 68, + "routePanelGroupsEditPermsSubmit": 69, + "routePanelGroupsCreateSubmit": 70, + "panel.Backups": 71, + "panel.LogsRegs": 72, + "panel.LogsMod": 73, + "panel.Debug": 74, + "routePanelDashboard": 75, + "routes.AccountEdit": 76, + "routes.AccountEditPassword": 77, + "routes.AccountEditPasswordSubmit": 78, + "routes.AccountEditAvatarSubmit": 79, + "routes.AccountEditUsernameSubmit": 80, + "routes.AccountEditMFA": 81, + "routes.AccountEditMFASetup": 82, + "routes.AccountEditMFASetupSubmit": 83, + "routes.AccountEditMFADisableSubmit": 84, + "routes.AccountEditEmail": 85, + "routes.AccountEditEmailTokenSubmit": 86, + "routes.ViewProfile": 87, + "routes.BanUserSubmit": 88, + "routes.UnbanUser": 89, + "routes.ActivateUser": 90, + "routes.IPSearch": 91, + "routes.CreateTopicSubmit": 92, + "routes.EditTopicSubmit": 93, + "routes.DeleteTopicSubmit": 94, + "routes.StickTopicSubmit": 95, + "routes.UnstickTopicSubmit": 96, + "routes.LockTopicSubmit": 97, + "routes.UnlockTopicSubmit": 98, + "routes.MoveTopicSubmit": 99, + "routes.LikeTopicSubmit": 100, + "routes.ViewTopic": 101, + "routes.CreateReplySubmit": 102, + "routes.ReplyEditSubmit": 103, + "routes.ReplyDeleteSubmit": 104, + "routes.ReplyLikeSubmit": 105, + "routes.ProfileReplyCreateSubmit": 106, + "routes.ProfileReplyEditSubmit": 107, + "routes.ProfileReplyDeleteSubmit": 108, + "routes.PollVote": 109, + "routes.PollResults": 110, + "routes.AccountLogin": 111, + "routes.AccountRegister": 112, + "routes.AccountLogout": 113, + "routes.AccountLoginSubmit": 114, + "routes.AccountLoginMFAVerify": 115, + "routes.AccountLoginMFAVerifySubmit": 116, + "routes.AccountRegisterSubmit": 117, + "routes.DynamicRoute": 118, + "routes.UploadedFile": 119, + "routes.StaticFile": 120, + "routes.RobotsTxt": 121, + "routes.SitemapXml": 122, + "routes.BadRoute": 123, } var reverseRouteMapEnum = map[int]string{ - 0: "routeAPI", - 1: "routes.Overview", - 2: "routes.CustomPage", - 3: "routes.ForumList", - 4: "routes.ViewForum", - 5: "routes.ChangeTheme", - 6: "routes.ShowAttachment", - 7: "common.RouteWebsockets", - 8: "routes.ReportSubmit", - 9: "routes.CreateTopic", - 10: "routes.TopicList", - 11: "panel.Forums", - 12: "panel.ForumsCreateSubmit", - 13: "panel.ForumsDelete", - 14: "panel.ForumsDeleteSubmit", - 15: "panel.ForumsEdit", - 16: "panel.ForumsEditSubmit", - 17: "panel.ForumsEditPermsSubmit", - 18: "panel.ForumsEditPermsAdvance", - 19: "panel.ForumsEditPermsAdvanceSubmit", - 20: "panel.Settings", - 21: "panel.SettingEdit", - 22: "panel.SettingEditSubmit", - 23: "routePanelWordFilters", - 24: "routePanelWordFiltersCreateSubmit", - 25: "routePanelWordFiltersEdit", - 26: "routePanelWordFiltersEditSubmit", - 27: "routePanelWordFiltersDeleteSubmit", - 28: "panel.Pages", - 29: "panel.PagesCreateSubmit", - 30: "panel.PagesEdit", - 31: "panel.PagesEditSubmit", - 32: "panel.PagesDeleteSubmit", - 33: "routePanelThemes", - 34: "routePanelThemesSetDefault", - 35: "routePanelThemesMenus", - 36: "routePanelThemesMenusEdit", - 37: "routePanelThemesMenuItemEdit", - 38: "routePanelThemesMenuItemEditSubmit", - 39: "routePanelThemesMenuItemCreateSubmit", - 40: "routePanelThemesMenuItemDeleteSubmit", - 41: "routePanelThemesMenuItemOrderSubmit", - 42: "routePanelPlugins", - 43: "routePanelPluginsActivate", - 44: "routePanelPluginsDeactivate", - 45: "routePanelPluginsInstall", - 46: "panel.Users", - 47: "panel.UsersEdit", - 48: "panel.UsersEditSubmit", - 49: "panel.AnalyticsViews", - 50: "panel.AnalyticsRoutes", - 51: "panel.AnalyticsAgents", - 52: "panel.AnalyticsSystems", - 53: "panel.AnalyticsLanguages", - 54: "panel.AnalyticsReferrers", - 55: "panel.AnalyticsRouteViews", - 56: "panel.AnalyticsAgentViews", - 57: "panel.AnalyticsForumViews", - 58: "panel.AnalyticsSystemViews", - 59: "panel.AnalyticsLanguageViews", - 60: "panel.AnalyticsReferrerViews", - 61: "panel.AnalyticsPosts", - 62: "panel.AnalyticsTopics", - 63: "panel.AnalyticsForums", - 64: "routePanelGroups", - 65: "routePanelGroupsEdit", - 66: "routePanelGroupsEditPerms", - 67: "routePanelGroupsEditSubmit", - 68: "routePanelGroupsEditPermsSubmit", - 69: "routePanelGroupsCreateSubmit", - 70: "panel.Backups", - 71: "panel.LogsRegs", - 72: "panel.LogsMod", - 73: "panel.Debug", - 74: "routePanelDashboard", - 75: "routes.AccountEdit", - 76: "routes.AccountEditPassword", - 77: "routes.AccountEditPasswordSubmit", - 78: "routes.AccountEditAvatarSubmit", - 79: "routes.AccountEditUsernameSubmit", - 80: "routes.AccountEditMFA", - 81: "routes.AccountEditMFASetup", - 82: "routes.AccountEditMFASetupSubmit", - 83: "routes.AccountEditMFADisableSubmit", - 84: "routes.AccountEditEmail", - 85: "routes.AccountEditEmailTokenSubmit", - 86: "routes.ViewProfile", - 87: "routes.BanUserSubmit", - 88: "routes.UnbanUser", - 89: "routes.ActivateUser", - 90: "routes.IPSearch", - 91: "routes.CreateTopicSubmit", - 92: "routes.EditTopicSubmit", - 93: "routes.DeleteTopicSubmit", - 94: "routes.StickTopicSubmit", - 95: "routes.UnstickTopicSubmit", - 96: "routes.LockTopicSubmit", - 97: "routes.UnlockTopicSubmit", - 98: "routes.MoveTopicSubmit", - 99: "routes.LikeTopicSubmit", - 100: "routes.ViewTopic", - 101: "routes.CreateReplySubmit", - 102: "routes.ReplyEditSubmit", - 103: "routes.ReplyDeleteSubmit", - 104: "routes.ReplyLikeSubmit", - 105: "routes.ProfileReplyCreateSubmit", - 106: "routes.ProfileReplyEditSubmit", - 107: "routes.ProfileReplyDeleteSubmit", - 108: "routes.PollVote", - 109: "routes.PollResults", - 110: "routes.AccountLogin", - 111: "routes.AccountRegister", - 112: "routes.AccountLogout", - 113: "routes.AccountLoginSubmit", - 114: "routes.AccountLoginMFAVerify", - 115: "routes.AccountLoginMFAVerifySubmit", - 116: "routes.AccountRegisterSubmit", - 117: "routes.DynamicRoute", - 118: "routes.UploadedFile", - 119: "routes.StaticFile", - 120: "routes.RobotsTxt", - 121: "routes.SitemapXml", - 122: "routes.BadRoute", + 0: "routes.Overview", + 1: "routes.CustomPage", + 2: "routes.ForumList", + 3: "routes.ViewForum", + 4: "routes.ChangeTheme", + 5: "routes.ShowAttachment", + 6: "common.RouteWebsockets", + 7: "routeAPIPhrases", + 8: "routeAPI", + 9: "routes.ReportSubmit", + 10: "routes.CreateTopic", + 11: "routes.TopicList", + 12: "panel.Forums", + 13: "panel.ForumsCreateSubmit", + 14: "panel.ForumsDelete", + 15: "panel.ForumsDeleteSubmit", + 16: "panel.ForumsEdit", + 17: "panel.ForumsEditSubmit", + 18: "panel.ForumsEditPermsSubmit", + 19: "panel.ForumsEditPermsAdvance", + 20: "panel.ForumsEditPermsAdvanceSubmit", + 21: "panel.Settings", + 22: "panel.SettingEdit", + 23: "panel.SettingEditSubmit", + 24: "routePanelWordFilters", + 25: "routePanelWordFiltersCreateSubmit", + 26: "routePanelWordFiltersEdit", + 27: "routePanelWordFiltersEditSubmit", + 28: "routePanelWordFiltersDeleteSubmit", + 29: "panel.Pages", + 30: "panel.PagesCreateSubmit", + 31: "panel.PagesEdit", + 32: "panel.PagesEditSubmit", + 33: "panel.PagesDeleteSubmit", + 34: "routePanelThemes", + 35: "routePanelThemesSetDefault", + 36: "routePanelThemesMenus", + 37: "routePanelThemesMenusEdit", + 38: "routePanelThemesMenuItemEdit", + 39: "routePanelThemesMenuItemEditSubmit", + 40: "routePanelThemesMenuItemCreateSubmit", + 41: "routePanelThemesMenuItemDeleteSubmit", + 42: "routePanelThemesMenuItemOrderSubmit", + 43: "routePanelPlugins", + 44: "routePanelPluginsActivate", + 45: "routePanelPluginsDeactivate", + 46: "routePanelPluginsInstall", + 47: "panel.Users", + 48: "panel.UsersEdit", + 49: "panel.UsersEditSubmit", + 50: "panel.AnalyticsViews", + 51: "panel.AnalyticsRoutes", + 52: "panel.AnalyticsAgents", + 53: "panel.AnalyticsSystems", + 54: "panel.AnalyticsLanguages", + 55: "panel.AnalyticsReferrers", + 56: "panel.AnalyticsRouteViews", + 57: "panel.AnalyticsAgentViews", + 58: "panel.AnalyticsForumViews", + 59: "panel.AnalyticsSystemViews", + 60: "panel.AnalyticsLanguageViews", + 61: "panel.AnalyticsReferrerViews", + 62: "panel.AnalyticsPosts", + 63: "panel.AnalyticsTopics", + 64: "panel.AnalyticsForums", + 65: "routePanelGroups", + 66: "routePanelGroupsEdit", + 67: "routePanelGroupsEditPerms", + 68: "routePanelGroupsEditSubmit", + 69: "routePanelGroupsEditPermsSubmit", + 70: "routePanelGroupsCreateSubmit", + 71: "panel.Backups", + 72: "panel.LogsRegs", + 73: "panel.LogsMod", + 74: "panel.Debug", + 75: "routePanelDashboard", + 76: "routes.AccountEdit", + 77: "routes.AccountEditPassword", + 78: "routes.AccountEditPasswordSubmit", + 79: "routes.AccountEditAvatarSubmit", + 80: "routes.AccountEditUsernameSubmit", + 81: "routes.AccountEditMFA", + 82: "routes.AccountEditMFASetup", + 83: "routes.AccountEditMFASetupSubmit", + 84: "routes.AccountEditMFADisableSubmit", + 85: "routes.AccountEditEmail", + 86: "routes.AccountEditEmailTokenSubmit", + 87: "routes.ViewProfile", + 88: "routes.BanUserSubmit", + 89: "routes.UnbanUser", + 90: "routes.ActivateUser", + 91: "routes.IPSearch", + 92: "routes.CreateTopicSubmit", + 93: "routes.EditTopicSubmit", + 94: "routes.DeleteTopicSubmit", + 95: "routes.StickTopicSubmit", + 96: "routes.UnstickTopicSubmit", + 97: "routes.LockTopicSubmit", + 98: "routes.UnlockTopicSubmit", + 99: "routes.MoveTopicSubmit", + 100: "routes.LikeTopicSubmit", + 101: "routes.ViewTopic", + 102: "routes.CreateReplySubmit", + 103: "routes.ReplyEditSubmit", + 104: "routes.ReplyDeleteSubmit", + 105: "routes.ReplyLikeSubmit", + 106: "routes.ProfileReplyCreateSubmit", + 107: "routes.ProfileReplyEditSubmit", + 108: "routes.ProfileReplyDeleteSubmit", + 109: "routes.PollVote", + 110: "routes.PollResults", + 111: "routes.AccountLogin", + 112: "routes.AccountRegister", + 113: "routes.AccountLogout", + 114: "routes.AccountLoginSubmit", + 115: "routes.AccountLoginMFAVerify", + 116: "routes.AccountLoginMFAVerifySubmit", + 117: "routes.AccountRegisterSubmit", + 118: "routes.DynamicRoute", + 119: "routes.UploadedFile", + 120: "routes.StaticFile", + 121: "routes.RobotsTxt", + 122: "routes.SitemapXml", + 123: "routes.BadRoute", } var osMapEnum = map[string]int{ "unknown": 0, @@ -661,14 +664,14 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { // TODO: Cover more suspicious strings and at a lower layer than this for _, char := range req.URL.Path { if char != '&' && !(char > 44 && char < 58) && char != '=' && char != '?' && !(char > 64 && char < 91) && char != '\\' && char != '_' && !(char > 96 && char < 123) { - router.SuspiciousRequest(req,"") + router.SuspiciousRequest(req,"Bad char in path") break } } lowerPath := strings.ToLower(req.URL.Path) // TODO: Flag any requests which has a dot with anything but a number after that if strings.Contains(req.URL.Path,"..") || strings.Contains(req.URL.Path,"--") || strings.Contains(lowerPath,".php") || strings.Contains(lowerPath,".asp") || strings.Contains(lowerPath,".cgi") || strings.Contains(lowerPath,".py") || strings.Contains(lowerPath,".sql") || strings.Contains(lowerPath,".action") { - router.SuspiciousRequest(req,"") + router.SuspiciousRequest(req,"Bad snippet in path") } // Indirect the default route onto a different one @@ -690,7 +693,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { counters.GlobalViewCounter.Bump() if prefix == "/static" { - counters.RouteViewCounter.Bump(119) + counters.RouteViewCounter.Bump(120) req.URL.Path += extraData routes.StaticFile(w, req) return @@ -742,7 +745,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { // TODO: Test this items = items[:0] indices = indices[:0] - router.SuspiciousRequest(req,"") + router.SuspiciousRequest(req,"Illegal char in UA") router.requestLogger.Print("UA Buffer: ", buffer) router.requestLogger.Print("UA Buffer String: ", string(buffer)) break @@ -856,32 +859,26 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { var err common.RouteError switch(prefix) { - case "/api": - counters.RouteViewCounter.Bump(0) - err = routeAPI(w,req,user) - if err != nil { - router.handleError(err,w,req,user) - } case "/overview": - counters.RouteViewCounter.Bump(1) + counters.RouteViewCounter.Bump(0) err = routes.Overview(w,req,user) if err != nil { router.handleError(err,w,req,user) } case "/pages": - counters.RouteViewCounter.Bump(2) + counters.RouteViewCounter.Bump(1) err = routes.CustomPage(w,req,user,extraData) if err != nil { router.handleError(err,w,req,user) } case "/forums": - counters.RouteViewCounter.Bump(3) + counters.RouteViewCounter.Bump(2) err = routes.ForumList(w,req,user) if err != nil { router.handleError(err,w,req,user) } case "/forum": - counters.RouteViewCounter.Bump(4) + counters.RouteViewCounter.Bump(3) err = routes.ViewForum(w,req,user,extraData) if err != nil { router.handleError(err,w,req,user) @@ -893,7 +890,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(5) + counters.RouteViewCounter.Bump(4) err = routes.ChangeTheme(w,req,user) if err != nil { router.handleError(err,w,req,user) @@ -905,18 +902,30 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(6) + counters.RouteViewCounter.Bump(5) err = routes.ShowAttachment(w,req,user,extraData) if err != nil { router.handleError(err,w,req,user) } case "/ws": req.URL.Path += extraData - counters.RouteViewCounter.Bump(7) + counters.RouteViewCounter.Bump(6) err = common.RouteWebsockets(w,req,user) if err != nil { router.handleError(err,w,req,user) } + case "/api": + switch(req.URL.Path) { + case "/api/phrases/": + counters.RouteViewCounter.Bump(7) + err = routeAPIPhrases(w,req,user) + default: + counters.RouteViewCounter.Bump(8) + err = routeAPI(w,req,user) + } + if err != nil { + router.handleError(err,w,req,user) + } case "/report": err = common.NoBanned(w,req,user) if err != nil { @@ -938,7 +947,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(8) + counters.RouteViewCounter.Bump(9) err = routes.ReportSubmit(w,req,user,extraData) } if err != nil { @@ -953,10 +962,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(9) + counters.RouteViewCounter.Bump(10) err = routes.CreateTopic(w,req,user,extraData) default: - counters.RouteViewCounter.Bump(10) + counters.RouteViewCounter.Bump(11) err = routes.TopicList(w,req,user) } if err != nil { @@ -971,7 +980,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { switch(req.URL.Path) { case "/panel/forums/": - counters.RouteViewCounter.Bump(11) + counters.RouteViewCounter.Bump(12) err = panel.Forums(w,req,user) case "/panel/forums/create/": err = common.NoSessionMismatch(w,req,user) @@ -980,7 +989,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(12) + counters.RouteViewCounter.Bump(13) err = panel.ForumsCreateSubmit(w,req,user) case "/panel/forums/delete/": err = common.NoSessionMismatch(w,req,user) @@ -989,7 +998,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(13) + counters.RouteViewCounter.Bump(14) err = panel.ForumsDelete(w,req,user,extraData) case "/panel/forums/delete/submit/": err = common.NoSessionMismatch(w,req,user) @@ -998,10 +1007,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(14) + counters.RouteViewCounter.Bump(15) err = panel.ForumsDeleteSubmit(w,req,user,extraData) case "/panel/forums/edit/": - counters.RouteViewCounter.Bump(15) + counters.RouteViewCounter.Bump(16) err = panel.ForumsEdit(w,req,user,extraData) case "/panel/forums/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1010,7 +1019,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(16) + counters.RouteViewCounter.Bump(17) err = panel.ForumsEditSubmit(w,req,user,extraData) case "/panel/forums/edit/perms/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1019,10 +1028,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(17) + counters.RouteViewCounter.Bump(18) err = panel.ForumsEditPermsSubmit(w,req,user,extraData) case "/panel/forums/edit/perms/": - counters.RouteViewCounter.Bump(18) + counters.RouteViewCounter.Bump(19) err = panel.ForumsEditPermsAdvance(w,req,user,extraData) case "/panel/forums/edit/perms/adv/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1031,13 +1040,13 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(19) + counters.RouteViewCounter.Bump(20) err = panel.ForumsEditPermsAdvanceSubmit(w,req,user,extraData) case "/panel/settings/": - counters.RouteViewCounter.Bump(20) + counters.RouteViewCounter.Bump(21) err = panel.Settings(w,req,user) case "/panel/settings/edit/": - counters.RouteViewCounter.Bump(21) + counters.RouteViewCounter.Bump(22) err = panel.SettingEdit(w,req,user,extraData) case "/panel/settings/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1046,10 +1055,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(22) + counters.RouteViewCounter.Bump(23) err = panel.SettingEditSubmit(w,req,user,extraData) case "/panel/settings/word-filters/": - counters.RouteViewCounter.Bump(23) + counters.RouteViewCounter.Bump(24) err = routePanelWordFilters(w,req,user) case "/panel/settings/word-filters/create/": err = common.NoSessionMismatch(w,req,user) @@ -1058,10 +1067,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(24) + counters.RouteViewCounter.Bump(25) err = routePanelWordFiltersCreateSubmit(w,req,user) case "/panel/settings/word-filters/edit/": - counters.RouteViewCounter.Bump(25) + counters.RouteViewCounter.Bump(26) err = routePanelWordFiltersEdit(w,req,user,extraData) case "/panel/settings/word-filters/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1070,7 +1079,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(26) + counters.RouteViewCounter.Bump(27) err = routePanelWordFiltersEditSubmit(w,req,user,extraData) case "/panel/settings/word-filters/delete/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1079,7 +1088,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(27) + counters.RouteViewCounter.Bump(28) err = routePanelWordFiltersDeleteSubmit(w,req,user,extraData) case "/panel/pages/": err = common.AdminOnly(w,req,user) @@ -1088,7 +1097,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(28) + counters.RouteViewCounter.Bump(29) err = panel.Pages(w,req,user) case "/panel/pages/create/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1103,7 +1112,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(29) + counters.RouteViewCounter.Bump(30) err = panel.PagesCreateSubmit(w,req,user) case "/panel/pages/edit/": err = common.AdminOnly(w,req,user) @@ -1112,7 +1121,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(30) + counters.RouteViewCounter.Bump(31) err = panel.PagesEdit(w,req,user,extraData) case "/panel/pages/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1127,7 +1136,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(31) + counters.RouteViewCounter.Bump(32) err = panel.PagesEditSubmit(w,req,user,extraData) case "/panel/pages/delete/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1142,10 +1151,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(32) + counters.RouteViewCounter.Bump(33) err = panel.PagesDeleteSubmit(w,req,user,extraData) case "/panel/themes/": - counters.RouteViewCounter.Bump(33) + counters.RouteViewCounter.Bump(34) err = routePanelThemes(w,req,user) case "/panel/themes/default/": err = common.NoSessionMismatch(w,req,user) @@ -1154,16 +1163,16 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(34) + counters.RouteViewCounter.Bump(35) err = routePanelThemesSetDefault(w,req,user,extraData) case "/panel/themes/menus/": - counters.RouteViewCounter.Bump(35) + counters.RouteViewCounter.Bump(36) err = routePanelThemesMenus(w,req,user) case "/panel/themes/menus/edit/": - counters.RouteViewCounter.Bump(36) + counters.RouteViewCounter.Bump(37) err = routePanelThemesMenusEdit(w,req,user,extraData) case "/panel/themes/menus/item/edit/": - counters.RouteViewCounter.Bump(37) + counters.RouteViewCounter.Bump(38) err = routePanelThemesMenuItemEdit(w,req,user,extraData) case "/panel/themes/menus/item/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1172,7 +1181,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(38) + counters.RouteViewCounter.Bump(39) err = routePanelThemesMenuItemEditSubmit(w,req,user,extraData) case "/panel/themes/menus/item/create/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1181,7 +1190,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(39) + counters.RouteViewCounter.Bump(40) err = routePanelThemesMenuItemCreateSubmit(w,req,user) case "/panel/themes/menus/item/delete/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1190,7 +1199,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(40) + counters.RouteViewCounter.Bump(41) err = routePanelThemesMenuItemDeleteSubmit(w,req,user,extraData) case "/panel/themes/menus/item/order/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1199,10 +1208,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(41) + counters.RouteViewCounter.Bump(42) err = routePanelThemesMenuItemOrderSubmit(w,req,user,extraData) case "/panel/plugins/": - counters.RouteViewCounter.Bump(42) + counters.RouteViewCounter.Bump(43) err = routePanelPlugins(w,req,user) case "/panel/plugins/activate/": err = common.NoSessionMismatch(w,req,user) @@ -1211,7 +1220,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(43) + counters.RouteViewCounter.Bump(44) err = routePanelPluginsActivate(w,req,user,extraData) case "/panel/plugins/deactivate/": err = common.NoSessionMismatch(w,req,user) @@ -1220,7 +1229,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(44) + counters.RouteViewCounter.Bump(45) err = routePanelPluginsDeactivate(w,req,user,extraData) case "/panel/plugins/install/": err = common.NoSessionMismatch(w,req,user) @@ -1229,13 +1238,13 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(45) + counters.RouteViewCounter.Bump(46) err = routePanelPluginsInstall(w,req,user,extraData) case "/panel/users/": - counters.RouteViewCounter.Bump(46) + counters.RouteViewCounter.Bump(47) err = panel.Users(w,req,user) case "/panel/users/edit/": - counters.RouteViewCounter.Bump(47) + counters.RouteViewCounter.Bump(48) err = panel.UsersEdit(w,req,user,extraData) case "/panel/users/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1244,7 +1253,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(48) + counters.RouteViewCounter.Bump(49) err = panel.UsersEditSubmit(w,req,user,extraData) case "/panel/analytics/views/": err = common.ParseForm(w,req,user) @@ -1253,7 +1262,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(49) + counters.RouteViewCounter.Bump(50) err = panel.AnalyticsViews(w,req,user) case "/panel/analytics/routes/": err = common.ParseForm(w,req,user) @@ -1262,7 +1271,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(50) + counters.RouteViewCounter.Bump(51) err = panel.AnalyticsRoutes(w,req,user) case "/panel/analytics/agents/": err = common.ParseForm(w,req,user) @@ -1271,7 +1280,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(51) + counters.RouteViewCounter.Bump(52) err = panel.AnalyticsAgents(w,req,user) case "/panel/analytics/systems/": err = common.ParseForm(w,req,user) @@ -1280,7 +1289,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(52) + counters.RouteViewCounter.Bump(53) err = panel.AnalyticsSystems(w,req,user) case "/panel/analytics/langs/": err = common.ParseForm(w,req,user) @@ -1289,7 +1298,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(53) + counters.RouteViewCounter.Bump(54) err = panel.AnalyticsLanguages(w,req,user) case "/panel/analytics/referrers/": err = common.ParseForm(w,req,user) @@ -1298,25 +1307,25 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(54) + counters.RouteViewCounter.Bump(55) err = panel.AnalyticsReferrers(w,req,user) case "/panel/analytics/route/": - counters.RouteViewCounter.Bump(55) + counters.RouteViewCounter.Bump(56) err = panel.AnalyticsRouteViews(w,req,user,extraData) case "/panel/analytics/agent/": - counters.RouteViewCounter.Bump(56) + counters.RouteViewCounter.Bump(57) err = panel.AnalyticsAgentViews(w,req,user,extraData) case "/panel/analytics/forum/": - counters.RouteViewCounter.Bump(57) + counters.RouteViewCounter.Bump(58) err = panel.AnalyticsForumViews(w,req,user,extraData) case "/panel/analytics/system/": - counters.RouteViewCounter.Bump(58) + counters.RouteViewCounter.Bump(59) err = panel.AnalyticsSystemViews(w,req,user,extraData) case "/panel/analytics/lang/": - counters.RouteViewCounter.Bump(59) + counters.RouteViewCounter.Bump(60) err = panel.AnalyticsLanguageViews(w,req,user,extraData) case "/panel/analytics/referrer/": - counters.RouteViewCounter.Bump(60) + counters.RouteViewCounter.Bump(61) err = panel.AnalyticsReferrerViews(w,req,user,extraData) case "/panel/analytics/posts/": err = common.ParseForm(w,req,user) @@ -1325,7 +1334,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(61) + counters.RouteViewCounter.Bump(62) err = panel.AnalyticsPosts(w,req,user) case "/panel/analytics/topics/": err = common.ParseForm(w,req,user) @@ -1334,7 +1343,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(62) + counters.RouteViewCounter.Bump(63) err = panel.AnalyticsTopics(w,req,user) case "/panel/analytics/forums/": err = common.ParseForm(w,req,user) @@ -1343,16 +1352,16 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(63) + counters.RouteViewCounter.Bump(64) err = panel.AnalyticsForums(w,req,user) case "/panel/groups/": - counters.RouteViewCounter.Bump(64) + counters.RouteViewCounter.Bump(65) err = routePanelGroups(w,req,user) case "/panel/groups/edit/": - counters.RouteViewCounter.Bump(65) + counters.RouteViewCounter.Bump(66) err = routePanelGroupsEdit(w,req,user,extraData) case "/panel/groups/edit/perms/": - counters.RouteViewCounter.Bump(66) + counters.RouteViewCounter.Bump(67) err = routePanelGroupsEditPerms(w,req,user,extraData) case "/panel/groups/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1361,7 +1370,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(67) + counters.RouteViewCounter.Bump(68) err = routePanelGroupsEditSubmit(w,req,user,extraData) case "/panel/groups/edit/perms/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1370,7 +1379,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(68) + counters.RouteViewCounter.Bump(69) err = routePanelGroupsEditPermsSubmit(w,req,user,extraData) case "/panel/groups/create/": err = common.NoSessionMismatch(w,req,user) @@ -1379,7 +1388,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(69) + counters.RouteViewCounter.Bump(70) err = routePanelGroupsCreateSubmit(w,req,user) case "/panel/backups/": err = common.SuperAdminOnly(w,req,user) @@ -1388,13 +1397,13 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(70) + counters.RouteViewCounter.Bump(71) err = panel.Backups(w,req,user,extraData) case "/panel/logs/regs/": - counters.RouteViewCounter.Bump(71) + counters.RouteViewCounter.Bump(72) err = panel.LogsRegs(w,req,user) case "/panel/logs/mod/": - counters.RouteViewCounter.Bump(72) + counters.RouteViewCounter.Bump(73) err = panel.LogsMod(w,req,user) case "/panel/debug/": err = common.AdminOnly(w,req,user) @@ -1403,10 +1412,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(73) + counters.RouteViewCounter.Bump(74) err = panel.Debug(w,req,user) default: - counters.RouteViewCounter.Bump(74) + counters.RouteViewCounter.Bump(75) err = routePanelDashboard(w,req,user) } if err != nil { @@ -1421,7 +1430,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(75) + counters.RouteViewCounter.Bump(76) err = routes.AccountEdit(w,req,user) case "/user/edit/password/": err = common.MemberOnly(w,req,user) @@ -1430,7 +1439,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(76) + counters.RouteViewCounter.Bump(77) err = routes.AccountEditPassword(w,req,user) case "/user/edit/password/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1445,7 +1454,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(77) + counters.RouteViewCounter.Bump(78) err = routes.AccountEditPasswordSubmit(w,req,user) case "/user/edit/avatar/submit/": err = common.MemberOnly(w,req,user) @@ -1465,7 +1474,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(78) + counters.RouteViewCounter.Bump(79) err = routes.AccountEditAvatarSubmit(w,req,user) case "/user/edit/username/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1480,7 +1489,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(79) + counters.RouteViewCounter.Bump(80) err = routes.AccountEditUsernameSubmit(w,req,user) case "/user/edit/mfa/": err = common.MemberOnly(w,req,user) @@ -1489,7 +1498,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(80) + counters.RouteViewCounter.Bump(81) err = routes.AccountEditMFA(w,req,user) case "/user/edit/mfa/setup/": err = common.MemberOnly(w,req,user) @@ -1498,7 +1507,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(81) + counters.RouteViewCounter.Bump(82) err = routes.AccountEditMFASetup(w,req,user) case "/user/edit/mfa/setup/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1513,7 +1522,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(82) + counters.RouteViewCounter.Bump(83) err = routes.AccountEditMFASetupSubmit(w,req,user) case "/user/edit/mfa/disable/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1528,7 +1537,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(83) + counters.RouteViewCounter.Bump(84) err = routes.AccountEditMFADisableSubmit(w,req,user) case "/user/edit/email/": err = common.MemberOnly(w,req,user) @@ -1537,7 +1546,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(84) + counters.RouteViewCounter.Bump(85) err = routes.AccountEditEmail(w,req,user) case "/user/edit/token/": err = common.NoSessionMismatch(w,req,user) @@ -1552,11 +1561,11 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(85) + counters.RouteViewCounter.Bump(86) err = routes.AccountEditEmailTokenSubmit(w,req,user,extraData) default: req.URL.Path += extraData - counters.RouteViewCounter.Bump(86) + counters.RouteViewCounter.Bump(87) err = routes.ViewProfile(w,req,user) } if err != nil { @@ -1577,7 +1586,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(87) + counters.RouteViewCounter.Bump(88) err = routes.BanUserSubmit(w,req,user,extraData) case "/users/unban/": err = common.NoSessionMismatch(w,req,user) @@ -1592,7 +1601,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(88) + counters.RouteViewCounter.Bump(89) err = routes.UnbanUser(w,req,user,extraData) case "/users/activate/": err = common.NoSessionMismatch(w,req,user) @@ -1607,7 +1616,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(89) + counters.RouteViewCounter.Bump(90) err = routes.ActivateUser(w,req,user,extraData) case "/users/ips/": err = common.MemberOnly(w,req,user) @@ -1616,7 +1625,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(90) + counters.RouteViewCounter.Bump(91) err = routes.IPSearch(w,req,user) } if err != nil { @@ -1642,7 +1651,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(91) + counters.RouteViewCounter.Bump(92) err = routes.CreateTopicSubmit(w,req,user) case "/topic/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1657,7 +1666,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(92) + counters.RouteViewCounter.Bump(93) err = routes.EditTopicSubmit(w,req,user,extraData) case "/topic/delete/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1673,7 +1682,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } req.URL.Path += extraData - counters.RouteViewCounter.Bump(93) + counters.RouteViewCounter.Bump(94) err = routes.DeleteTopicSubmit(w,req,user) case "/topic/stick/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1688,7 +1697,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(94) + counters.RouteViewCounter.Bump(95) err = routes.StickTopicSubmit(w,req,user,extraData) case "/topic/unstick/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1703,7 +1712,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(95) + counters.RouteViewCounter.Bump(96) err = routes.UnstickTopicSubmit(w,req,user,extraData) case "/topic/lock/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1719,7 +1728,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } req.URL.Path += extraData - counters.RouteViewCounter.Bump(96) + counters.RouteViewCounter.Bump(97) err = routes.LockTopicSubmit(w,req,user) case "/topic/unlock/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1734,7 +1743,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(97) + counters.RouteViewCounter.Bump(98) err = routes.UnlockTopicSubmit(w,req,user,extraData) case "/topic/move/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1749,7 +1758,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(98) + counters.RouteViewCounter.Bump(99) err = routes.MoveTopicSubmit(w,req,user,extraData) case "/topic/like/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1770,10 +1779,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(99) + counters.RouteViewCounter.Bump(100) err = routes.LikeTopicSubmit(w,req,user,extraData) default: - counters.RouteViewCounter.Bump(100) + counters.RouteViewCounter.Bump(101) err = routes.ViewTopic(w,req,user, extraData) } if err != nil { @@ -1799,7 +1808,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(101) + counters.RouteViewCounter.Bump(102) err = routes.CreateReplySubmit(w,req,user) case "/reply/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1814,7 +1823,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(102) + counters.RouteViewCounter.Bump(103) err = routes.ReplyEditSubmit(w,req,user,extraData) case "/reply/delete/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1829,7 +1838,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(103) + counters.RouteViewCounter.Bump(104) err = routes.ReplyDeleteSubmit(w,req,user,extraData) case "/reply/like/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1850,7 +1859,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(104) + counters.RouteViewCounter.Bump(105) err = routes.ReplyLikeSubmit(w,req,user,extraData) } if err != nil { @@ -1871,7 +1880,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(105) + counters.RouteViewCounter.Bump(106) err = routes.ProfileReplyCreateSubmit(w,req,user) case "/profile/reply/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1886,7 +1895,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(106) + counters.RouteViewCounter.Bump(107) err = routes.ProfileReplyEditSubmit(w,req,user,extraData) case "/profile/reply/delete/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1901,7 +1910,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(107) + counters.RouteViewCounter.Bump(108) err = routes.ProfileReplyDeleteSubmit(w,req,user,extraData) } if err != nil { @@ -1922,10 +1931,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(108) + counters.RouteViewCounter.Bump(109) err = routes.PollVote(w,req,user,extraData) case "/poll/results/": - counters.RouteViewCounter.Bump(109) + counters.RouteViewCounter.Bump(110) err = routes.PollResults(w,req,user,extraData) } if err != nil { @@ -1934,10 +1943,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { case "/accounts": switch(req.URL.Path) { case "/accounts/login/": - counters.RouteViewCounter.Bump(110) + counters.RouteViewCounter.Bump(111) err = routes.AccountLogin(w,req,user) case "/accounts/create/": - counters.RouteViewCounter.Bump(111) + counters.RouteViewCounter.Bump(112) err = routes.AccountRegister(w,req,user) case "/accounts/logout/": err = common.NoSessionMismatch(w,req,user) @@ -1952,7 +1961,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(112) + counters.RouteViewCounter.Bump(113) err = routes.AccountLogout(w,req,user) case "/accounts/login/submit/": err = common.ParseForm(w,req,user) @@ -1961,10 +1970,10 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(113) + counters.RouteViewCounter.Bump(114) err = routes.AccountLoginSubmit(w,req,user) case "/accounts/mfa_verify/": - counters.RouteViewCounter.Bump(114) + counters.RouteViewCounter.Bump(115) err = routes.AccountLoginMFAVerify(w,req,user) case "/accounts/mfa_verify/submit/": err = common.ParseForm(w,req,user) @@ -1973,7 +1982,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(115) + counters.RouteViewCounter.Bump(116) err = routes.AccountLoginMFAVerifySubmit(w,req,user) case "/accounts/create/submit/": err = common.ParseForm(w,req,user) @@ -1982,7 +1991,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - counters.RouteViewCounter.Bump(116) + counters.RouteViewCounter.Bump(117) err = routes.AccountRegisterSubmit(w,req,user) } if err != nil { @@ -1999,7 +2008,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { common.NotFound(w,req,nil) return } - counters.RouteViewCounter.Bump(118) + counters.RouteViewCounter.Bump(119) req.URL.Path += extraData // TODO: Find a way to propagate errors up from this? router.UploadHandler(w,req) // TODO: Count these views @@ -2008,14 +2017,14 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { // TODO: Add support for favicons and robots.txt files switch(extraData) { case "robots.txt": - counters.RouteViewCounter.Bump(120) + counters.RouteViewCounter.Bump(121) err = routes.RobotsTxt(w,req) if err != nil { router.handleError(err,w,req,user) } return /*case "sitemap.xml": - counters.RouteViewCounter.Bump(121) + counters.RouteViewCounter.Bump(122) err = routes.SitemapXml(w,req) if err != nil { router.handleError(err,w,req,user) @@ -2031,7 +2040,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { router.RUnlock() if ok { - counters.RouteViewCounter.Bump(117) // TODO: Be more specific about *which* dynamic route it is + counters.RouteViewCounter.Bump(118) // TODO: Be more specific about *which* dynamic route it is req.URL.Path += extraData err = handle(w,req,user) if err != nil { @@ -2046,7 +2055,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { } else { router.DumpRequest(req,"Bad Route") } - counters.RouteViewCounter.Bump(122) + counters.RouteViewCounter.Bump(123) common.NotFound(w,req,nil) } } diff --git a/langs/english.json b/langs/english.json index c6d78444..dc099c07 100644 --- a/langs/english.json +++ b/langs/english.json @@ -285,20 +285,20 @@ "topics_likes_suffix":"likes", "topics_last":"Last", "topics_starter":"Starter", - "topic_like_count_suffix":" likes", - "topic_plus":"+", - "topic_plus_one":"+1", - "topic_gap_up":" up", - "topic_level":"Level", - "topic_edit_button_text":"Edit", - "topic_delete_button_text":"Delete", - "topic_ip_button_text":"IP", - "topic_lock_button_text":"Lock", - "topic_unlock_button_text":"Unlock", - "topic_pin_button_text":"Pin", - "topic_unpin_button_text":"Unpin", - "topic_report_button_text":"Report", - "topic_flag_button_text":"Flag", + "topic.like_count_suffix":" likes", + "topic.plus":"+", + "topic.plus_one":"+1", + "topic.gap_up":" up", + "topic.level":"Level", + "topic.edit_button_text":"Edit", + "topic.delete_button_text":"Delete", + "topic.ip_button_text":"IP", + "topic.lock_button_text":"Lock", + "topic.unlock_button_text":"Unlock", + "topic.pin_button_text":"Pin", + "topic.unpin_button_text":"Unpin", + "topic.report_button_text":"Report", + "topic.flag_button_text":"Flag", "panel_rank_admins":"Admins", "panel_rank_mods":"Mods", @@ -413,16 +413,16 @@ "create_topic_create_topic_button":"Create Topic", "create_topic_add_file_button":"Add File", - "quick_topic_aria":"Quick Topic Form", - "quick_topic_avatar_tooltip":"Your Avatar", - "quick_topic_avatar_alt":"Your Avatar", - "quick_topic_whatsup":"What's up?", - "quick_topic_content_placeholder":"Insert post here", - "quick_topic_add_poll_option":"Add new poll option", - "quick_topic_create_topic_button":"Create Topic", - "quick_topic_add_poll_button":"Add Poll", - "quick_topic_add_file_button":"Add File", - "quick_topic_cancel_button":"Cancel", + "quick_topic.aria":"Quick Topic Form", + "quick_topic.avatar_tooltip":"Your Avatar", + "quick_topic.avatar_alt":"Your Avatar", + "quick_topic.whatsup":"What's up?", + "quick_topic.content_placeholder":"Insert post here", + "quick_topic.add_poll_option":"Add new poll option", + "quick_topic.create_topic_button":"Create Topic", + "quick_topic.add_poll_button":"Add Poll", + "quick_topic.add_file_button":"Add File", + "quick_topic.cancel_button":"Cancel", "topic_list_create_topic_tooltip":"Create Topic", "topic_list_create_topic_aria":"Create a topic", @@ -435,8 +435,8 @@ "topic_list_moderate_run":"Run", "topic_list_move_head":"Move these topics to?", "topic_list_move_button":"Move Topics", - "status_closed_tooltip":"Status: Closed", - "status_pinned_tooltip":"Status: Pinned", + "status.closed_tooltip":"Status: Closed", + "status.pinned_tooltip":"Status: Pinned", "topics_head":"All Topics", "topics_locked_tooltip":"You don't have the permissions needed to create a topic", @@ -456,68 +456,68 @@ "forums_none":"None", "forums_no_forums":"You don't have access to any forums.", - "topic_opening_post_aria":"The opening post for this topic", - "topic_status_closed_aria":"This topic has been locked", - "topic_title_input_aria":"Topic Title Input", - "topic_update_button":"Update", - "topic_userinfo_aria":"The information on the poster", - "topic_poll_aria":"The main poll for this topic", - "topic_poll_vote":"Vote", - "topic_poll_results":"Results", - "topic_poll_cancel":"Cancel", - "topic_post_controls_aria":"Controls and Author Information", - "topic_unlike_tooltip":"Unlike", - "topic_unlike_aria":"Unlike this topic", - "topic_like_tooltip":"Like", - "topic_like_aria":"Like this topic", - "topic_edit_tooltip":"Edit Topic", - "topic_edit_aria":"Edit this topic", - "topic_delete_tooltip":"Delete Topic", - "topic_delete_aria":"Delete this topic", - "topic_unlock_tooltip":"Unlock Topic", - "topic_unlock_aria":"Unlock this topic", - "topic_lock_tooltip":"Lock Topic", - "topic_lock_aria":"Lock this topic", - "topic_unpin_tooltip":"Unpin Topic", - "topic_unpin_aria":"Unpin this topic", - "topic_pin_tooltip":"Pin Topic", - "topic_pin_aria":"Pin this topic", - "topic_ip_tooltip":"View IP", - "topic_ip_full_tooltip":"IP Address", - "topic_ip_full_aria":"This user's IP Address", - "topic_flag_tooltip":"Flag this topic", - "topic_flag_aria":"Flag this topic", - "topic_report_tooltip":"Report this topic", - "topic_report_aria":"Report this topic", - "topic_like_count_aria":"The number of likes on this topic", - "topic_like_count_tooltip":"Like Count", - "topic_level_aria":"The poster's level", - "topic_level_tooltip":"Level", - "topic_current_page_aria":"The current page for this topic", - "topic_post_like_tooltip":"Like this", - "topic_post_like_aria":"Like this post", - "topic_post_unlike_tooltip":"Unlike this", - "topic_post_unlike_aria":"Unlike this post", - "topic_post_edit_tooltip":"Edit Reply", - "topic_post_edit_aria":"Edit this post", - "topic_post_delete_tooltip":"Delete Reply", - "topic_post_delete_aria":"Delete this post", - "topic_post_ip_tooltip":"View IP", - "topic_post_flag_tooltip":"Flag this reply", - "topic_post_flag_aria":"Flag this reply", - "topic_post_like_count_tooltip":"Like Count", - "topic_post_level_aria":"The poster's level", - "topic_post_level_tooltip":"Level", - "topic_reply_aria":"The quick reply form", - "topic_reply_content":"Insert reply here", - "topic_reply_content_alt":"What do you think?", - "topic_reply_add_poll_option":"Add new poll option", - "topic_reply_button":"Create Reply", - "topic_reply_add_poll_button":"Add Poll", - "topic_reply_add_file_button":"Add File", + "topic.opening_post_aria":"The opening post for this topic", + "topic.status_closed_aria":"This topic has been locked", + "topic.title_input_aria":"Topic Title Input", + "topic.update_button":"Update", + "topic.userinfo_aria":"The information on the poster", + "topic.poll_aria":"The main poll for this topic", + "topic.poll_vote":"Vote", + "topic.poll_results":"Results", + "topic.poll_cancel":"Cancel", + "topic.post_controls_aria":"Controls and Author Information", + "topic.unlike_tooltip":"Unlike", + "topic.unlike_aria":"Unlike this topic", + "topic.like_tooltip":"Like", + "topic.like_aria":"Like this topic", + "topic.edit_tooltip":"Edit Topic", + "topic.edit_aria":"Edit this topic", + "topic.delete_tooltip":"Delete Topic", + "topic.delete_aria":"Delete this topic", + "topic.unlock_tooltip":"Unlock Topic", + "topic.unlock_aria":"Unlock this topic", + "topic.lock_tooltip":"Lock Topic", + "topic.lock_aria":"Lock this topic", + "topic.unpin_tooltip":"Unpin Topic", + "topic.unpin_aria":"Unpin this topic", + "topic.pin_tooltip":"Pin Topic", + "topic.pin_aria":"Pin this topic", + "topic.ip_tooltip":"View IP", + "topic.ip_full_tooltip":"IP Address", + "topic.ip_full_aria":"This user's IP Address", + "topic.flag_tooltip":"Flag this topic", + "topic.flag_aria":"Flag this topic", + "topic.report_tooltip":"Report this topic", + "topic.report_aria":"Report this topic", + "topic.like_count_aria":"The number of likes on this topic", + "topic.like_count_tooltip":"Like Count", + "topic.level_aria":"The poster's level", + "topic.level_tooltip":"Level", + "topic.current_page_aria":"The current page for this topic", + "topic.post_like_tooltip":"Like this", + "topic.post_like_aria":"Like this post", + "topic.post_unlike_tooltip":"Unlike this", + "topic.post_unlike_aria":"Unlike this post", + "topic.post_edit_tooltip":"Edit Reply", + "topic.post_edit_aria":"Edit this post", + "topic.post_delete_tooltip":"Delete Reply", + "topic.post_delete_aria":"Delete this post", + "topic.post_ip_tooltip":"View IP", + "topic.post_flag_tooltip":"Flag this reply", + "topic.post_flag_aria":"Flag this reply", + "topic.post_like_count_tooltip":"Like Count", + "topic.post_level_aria":"The poster's level", + "topic.post_level_tooltip":"Level", + "topic.reply_aria":"The quick reply form", + "topic.reply_content":"Insert reply here", + "topic.reply_content_alt":"What do you think?", + "topic.reply_add_poll_option":"Add new poll option", + "topic.reply_button":"Create Reply", + "topic.reply_add_poll_button":"Add Poll", + "topic.reply_add_file_button":"Add File", - "topic_level_prefix":"Level ", - "topic_your_information":"Your information", + "topic.level_prefix":"Level ", + "topic.your_information":"Your information", "paginator_less_than":"<", "paginator_greater_than":">", diff --git a/main.go b/main.go index 84c84be8..66c9ae20 100644 --- a/main.go +++ b/main.go @@ -442,6 +442,9 @@ func main() { log.Fatal("Received a signal to shutdown: ", sig) }() + // Start up the WebSocket ticks + common.WsHub.Start() + //if profiling { // pprof.StopCPUProfile() //} diff --git a/public/global.js b/public/global.js index fbdd4fd4..fe211565 100644 --- a/public/global.js +++ b/public/global.js @@ -1,5 +1,7 @@ 'use strict'; var formVars = {}; +var tmplInits = {}; +var tmplPhrases = []; var alertList = []; var alertCount = 0; var conn; @@ -72,19 +74,21 @@ function loadAlerts(menuAlerts) for(var i in data.msgs) { var msg = data.msgs[i]; var mmsg = msg.msg; - if("sub" in msg) { for(var i = 0; i < msg.sub.length; i++) { mmsg = mmsg.replace("\{"+i+"\}", msg.sub[i]); //console.log("Sub #" + i + ":",msg.sub[i]); } } - alist += Template_alert({ + + let aItem = Template_alert({ ASID: msg.asid || 0, Path: msg.path, Avatar: msg.avatar || "", Message: mmsg }) + alist += aItem; + alertList.push(aItem); //console.log(msg); //console.log(mmsg); } @@ -138,64 +142,97 @@ function SplitN(data,ch,n) { return out; } -function runWebSockets() { - if(window.location.protocol == "https:") - conn = new WebSocket("wss://" + document.location.host + "/ws/"); - else conn = new WebSocket("ws://" + document.location.host + "/ws/"); +function wsAlertEvent(data) { + var msg = data.msg; + if("sub" in data) { + for(var i = 0; i < data.sub.length; i++) { + msg = msg.replace("\{"+i+"\}", data.sub[i]); + } + } - conn.onopen = function() { + let aItem = Template_alert({ + ASID: data.asid || 0, + Path: data.path, + Avatar: data.avatar || "", + Message: msg + }) + alertList.push(aItem); + if(alertList.length > 8) alertList.shift(); + //console.log("post alertList",alertList); + alertCount++; + + var alist = ""; + for (var i = 0; i < alertList.length; i++) alist += alertList[i]; + + //console.log(alist); + // TODO: Add support for other alert feeds like PM Alerts + var generalAlerts = document.getElementById("general_alerts"); + var alertListNode = generalAlerts.getElementsByClassName("alertList")[0]; + var alertCounterNode = generalAlerts.getElementsByClassName("alert_counter")[0]; + alertListNode.innerHTML = alist; + alertCounterNode.textContent = alertCount; + + // TODO: Add some sort of notification queue to avoid flooding the end-user with notices? + // TODO: Use the site name instead of "Something Happened" + if(Notification.permission === "granted") { + var n = new Notification("Something Happened",{ + body: msg, + icon: data.avatar, + }); + setTimeout(n.close.bind(n), 8000); + } + + bindToAlerts(); +} + +function runWebSockets() { + if(window.location.protocol == "https:") { + conn = new WebSocket("wss://" + document.location.host + "/ws/"); + } else conn = new WebSocket("ws://" + document.location.host + "/ws/"); + + conn.onopen = () => { console.log("The WebSockets connection was opened"); conn.send("page " + document.location.pathname + '\r'); // TODO: Don't ask again, if it's denied. We could have a setting in the UCP which automatically requests this when someone flips desktop notifications on - Notification.requestPermission(); + if(loggedIn) { + Notification.requestPermission(); + } } - conn.onclose = function() { + conn.onclose = () => { conn = false; console.log("The WebSockets connection was closed"); } - conn.onmessage = function(event) { + conn.onmessage = (event) => { //console.log("WSMessage:", event.data); if(event.data[0] == "{") { + console.log("json message"); + let data = ""; try { - var data = JSON.parse(event.data); + data = JSON.parse(event.data); } catch(err) { console.log(err); + return; } + // TODO: Fix the data races in this code if ("msg" in data) { - var msg = data.msg - if("sub" in data) - for(var i = 0; i < data.sub.length; i++) - msg = msg.replace("\{"+i+"\}", data.sub[i]); - - if("avatar" in data) alertList.push("
"+msg+"
"); - else alertList.push("
"+msg+"
"); - if(alertList.length > 8) alertList.shift(); - //console.log("post alertList",alertList); - alertCount++; - - var alist = "" - for (var i = 0; i < alertList.length; i++) alist += alertList[i]; - - //console.log(alist); - // TODO: Add support for other alert feeds like PM Alerts - var generalAlerts = document.getElementById("general_alerts"); - var alertListNode = generalAlerts.getElementsByClassName("alertList")[0]; - var alertCounterNode = generalAlerts.getElementsByClassName("alert_counter")[0]; - alertListNode.innerHTML = alist; - alertCounterNode.textContent = alertCount; - - // TODO: Add some sort of notification queue to avoid flooding the end-user with notices? - // TODO: Use the site name instead of "Something Happened" - if(Notification.permission === "granted") { - var n = new Notification("Something Happened",{ - body: msg, - icon: data.avatar, - }); - setTimeout(n.close.bind(n), 8000); + wsAlertEvent(data); + } else if("Topics" in data) { + console.log("topic in data"); + console.log("data:", data); + let topic = data.Topics[0]; + if(topic === undefined){ + console.log("empty topic list"); + return; } - - bindToAlerts(); + let renTopic = Template_topics_topic(topic); + let node = $(renTopic); + node.addClass("new_item"); + console.log("Prepending to topic list"); + $(".topic_list").prepend(node); + } else { + console.log("unknown message"); + console.log(data); } } @@ -217,6 +254,11 @@ function runWebSockets() { } } +// Temporary hack for templates +function len(item) { + return item.length; +} + function loadScript(name, callback) { let url = "//" +siteURL+"/static/"+name $.getScript(url) @@ -231,16 +273,49 @@ function loadScript(name, callback) { }); } +function DoNothingButPassBack(item) { + return item; +} + +function fetchPhrases() { + fetch("//" +siteURL+"/api/phrases/?query=status") + .then((resp) => resp.json()) + .then((data) => { + Object.keys(tmplInits).forEach((key) => { + let phrases = []; + let tmplInit = tmplInits[key]; + for(let phraseName of tmplInit) { + phrases.push(data[phraseName]); + } + console.log("Adding phrases"); + console.log("key:",key); + console.log("phrases:",phrases); + tmplPhrases[key] = phrases; + }) + }); +} + $(document).ready(function(){ runHook("start_init"); - loadScript("template_alert.js",() => { + if(loggedIn) { + let toLoad = 1; + loadScript("template_topics_topic.js", () => { + console.log("Loaded template_topics_topic.js"); + toLoad--; + if(toLoad===0) fetchPhrases(); + }); + } + + // We can only get away with this because template_alert has no phrases, otherwise it too would have to be part of the "dance", I miss Go concurrency :( + loadScript("template_alert.js", () => { console.log("Loaded template_alert.js"); alertsInitted = true; var alertMenuList = document.getElementsByClassName("menu_alerts"); for(var i = 0; i < alertMenuList.length; i++) { loadAlerts(alertMenuList[i]); } - }) + }); + if(window["WebSocket"]) runWebSockets(); else conn = false; diff --git a/router_gen/main.go b/router_gen/main.go index 4d5aef5c..7a4bda15 100644 --- a/router_gen/main.go +++ b/router_gen/main.go @@ -447,14 +447,14 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { // TODO: Cover more suspicious strings and at a lower layer than this for _, char := range req.URL.Path { if char != '&' && !(char > 44 && char < 58) && char != '=' && char != '?' && !(char > 64 && char < 91) && char != '\\' && char != '_' && !(char > 96 && char < 123) { - router.SuspiciousRequest(req,"") + router.SuspiciousRequest(req,"Bad char in path") break } } lowerPath := strings.ToLower(req.URL.Path) // TODO: Flag any requests which has a dot with anything but a number after that if strings.Contains(req.URL.Path,"..") || strings.Contains(req.URL.Path,"--") || strings.Contains(lowerPath,".php") || strings.Contains(lowerPath,".asp") || strings.Contains(lowerPath,".cgi") || strings.Contains(lowerPath,".py") || strings.Contains(lowerPath,".sql") || strings.Contains(lowerPath,".action") { - router.SuspiciousRequest(req,"") + router.SuspiciousRequest(req,"Bad snippet in path") } // Indirect the default route onto a different one @@ -528,7 +528,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { // TODO: Test this items = items[:0] indices = indices[:0] - router.SuspiciousRequest(req,"") + router.SuspiciousRequest(req,"Illegal char in UA") router.requestLogger.Print("UA Buffer: ", buffer) router.requestLogger.Print("UA Buffer String: ", string(buffer)) break diff --git a/router_gen/routes.go b/router_gen/routes.go index 6c042125..e6831cbc 100644 --- a/router_gen/routes.go +++ b/router_gen/routes.go @@ -2,7 +2,6 @@ package main // TODO: How should we handle *HeaderLite and *Header? func routes() { - addRoute(View("routeAPI", "/api/")) addRoute(View("routes.Overview", "/overview/")) addRoute(View("routes.CustomPage", "/pages/", "extraData")) addRoute(View("routes.ForumList", "/forums/" /*,"&forums"*/)) @@ -12,6 +11,12 @@ func routes() { View("routes.ShowAttachment", "/attachs/", "extraData").Before("ParseForm"), ) + apiGroup := newRouteGroup("/api/", + View("routeAPI", "/api/"), + View("routeAPIPhrases", "/api/phrases/"), // TODO: Be careful with exposing the panel phrases here + ) + addRouteGroup(apiGroup) + // TODO: Reduce the number of Befores. With a new method, perhaps? reportGroup := newRouteGroup("/report/", Action("routes.ReportSubmit", "/report/submit/", "extraData"), diff --git a/routes.go b/routes.go index 8fae9f97..6e371c24 100644 --- a/routes.go +++ b/routes.go @@ -7,8 +7,11 @@ package main import ( + "encoding/json" "net/http" "strconv" + "strings" + "unicode" "./common" ) @@ -95,3 +98,91 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.R } return nil } + +// TODO: Be careful with exposing the panel phrases here, maybe move them into a different namespace? We also need to educate the admin that phrases aren't necessarily secret +func routeAPIPhrases(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { + // TODO: Don't make this too JSON dependent so that we can swap in newer more efficient formats + w.Header().Set("Content-Type", "application/json") + err := r.ParseForm() + if err != nil { + return common.PreErrorJS("Bad Form", w, r) + } + + query := r.FormValue("query") + if query == "" { + return common.PreErrorJS("No query provided", w, r) + } + + var negations []string + var positives []string + + queryBits := strings.Split(query, ",") + for _, queryBit := range queryBits { + queryBit = strings.TrimSpace(queryBit) + if queryBit[0] == '!' && len(queryBit) > 1 { + queryBit = strings.TrimPrefix(queryBit, "!") + for _, char := range queryBit { + if !unicode.IsLetter(char) && char != '-' && char != '_' { + return common.PreErrorJS("No symbols allowed, only - and _", w, r) + } + } + negations = append(negations, queryBit) + } else { + for _, char := range queryBit { + if !unicode.IsLetter(char) && char != '-' && char != '_' { + return common.PreErrorJS("No symbols allowed, only - and _", w, r) + } + } + positives = append(positives, queryBit) + } + } + if len(positives) == 0 { + return common.PreErrorJS("You haven't requested any phrases", w, r) + } + + var phrases map[string]string + // A little optimisation to avoid copying entries from one map to the other, if we don't have to mutate it + if len(positives) > 1 { + phrases = make(map[string]string) + for _, positive := range positives { + // ! Constrain it to topic and status phrases for now + if !strings.HasPrefix(positive, "topic") && !strings.HasPrefix(positive, "status") { + return common.PreErrorJS("Not implemented!", w, r) + } + pPhrases, ok := common.GetTmplPhrasesByPrefix(positive) + if !ok { + return common.PreErrorJS("No such prefix", w, r) + } + for name, phrase := range pPhrases { + phrases[name] = phrase + } + } + } else { + // ! Constrain it to topic and status phrases for now + if !strings.HasPrefix(positives[0], "topic") && !strings.HasPrefix(positives[0], "status") { + return common.PreErrorJS("Not implemented!", w, r) + } + pPhrases, ok := common.GetTmplPhrasesByPrefix(positives[0]) + if !ok { + return common.PreErrorJS("No such prefix", w, r) + } + phrases = pPhrases + } + + for _, negation := range negations { + for name, _ := range phrases { + if strings.HasPrefix(name, negation) { + delete(phrases, name) + } + } + } + + // TODO: Cache the output of this, especially for things like topic, so we don't have to waste more time than we need on this + jsonBytes, err := json.Marshal(phrases) + if err != nil { + return common.InternalError(err, w, r) + } + w.Write(jsonBytes) + + return nil +} diff --git a/routes/forum.go b/routes/forum.go index 5777b87e..7cc31d25 100644 --- a/routes/forum.go +++ b/routes/forum.go @@ -81,7 +81,7 @@ func ViewForum(w http.ResponseWriter, r *http.Request, user common.User, sfid st topicItem.Link = common.BuildTopicURL(common.NameToSlug(topicItem.Title), topicItem.ID) topicItem.RelativeLastReplyAt = common.RelativeTime(topicItem.LastReplyAt) - common.RunVhook("forum_trow_assign", &topicItem, &forum) + common.RunVhookNoreturn("forum_trow_assign", &topicItem, &forum) topicList = append(topicList, &topicItem) reqUserList[topicItem.CreatedBy] = true reqUserList[topicItem.LastReplyBy] = true diff --git a/routes/profile.go b/routes/profile.go index 4b76ef5d..d6a989a1 100644 --- a/routes/profile.go +++ b/routes/profile.go @@ -88,7 +88,7 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user common.User) commo } replyLines = strings.Count(replyContent, "\n") - if group.IsMod || group.IsAdmin { + if group.IsMod { replyClassName = common.Config.StaffCSS } else { replyClassName = "" diff --git a/routes/stubs.go b/routes/stubs.go index c57c847c..66801d85 100644 --- a/routes/stubs.go +++ b/routes/stubs.go @@ -8,10 +8,7 @@ type HTTPSRedirect struct { func (red *HTTPSRedirect) ServeHTTP(w http.ResponseWriter, req *http.Request) { w.Header().Set("Connection", "close") - dest := "https://" + req.Host + req.URL.Path - if len(req.URL.RawQuery) > 0 { - dest += "?" + req.URL.RawQuery - } + dest := "https://" + req.Host + req.URL.String() http.Redirect(w, req, dest, http.StatusTemporaryRedirect) } diff --git a/routes/topic.go b/routes/topic.go index 4b8a9c6c..caea7084 100644 --- a/routes/topic.go +++ b/routes/topic.go @@ -84,7 +84,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit } topic.Tag = postGroup.Tag - if postGroup.IsMod || postGroup.IsAdmin { + if postGroup.IsMod { topic.ClassName = common.Config.StaffCSS } topic.RelativeCreatedAt = common.RelativeTime(topic.CreatedAt) @@ -146,7 +146,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit return common.InternalError(err, w, r) } - if postGroup.IsMod || postGroup.IsAdmin { + if postGroup.IsMod { replyItem.ClassName = common.Config.StaffCSS } else { replyItem.ClassName = "" @@ -185,7 +185,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit likedQueryList = append(likedQueryList, replyItem.ID) } - common.RunVhook("topic_reply_row_assign", &tpage, &replyItem) + common.RunVhookNoreturn("topic_reply_row_assign", &tpage, &replyItem) // TODO: Use a pointer instead to make it easier to abstract this loop? What impact would this have on escape analysis? tpage.ItemList = append(tpage.ItemList, replyItem) } @@ -261,7 +261,7 @@ func CreateTopic(w http.ResponseWriter, r *http.Request, user common.User, sfid // Lock this to the forum being linked? // Should we always put it in strictmode when it's linked from another forum? Well, the user might end up changing their mind on what forum they want to post in and it would be a hassle, if they had to switch pages, even if it is a single click for many (exc. mobile) var strictmode bool - common.RunVhook("topic_create_pre_loop", w, r, fid, &header, &user, &strictmode) + common.RunVhookNoreturn("topic_create_pre_loop", w, r, fid, &header, &user, &strictmode) // TODO: Re-add support for plugin_guilds var forumList []common.Forum diff --git a/run-nowebsockets.bat b/run-nowebsockets.bat index c176f39d..d3453350 100644 --- a/run-nowebsockets.bat +++ b/run-nowebsockets.bat @@ -2,7 +2,9 @@ rem TODO: Make these deletes a little less noisy del "template_*.go" del "gen_*.go" -del "tmpl_client/template_*.go" +cd tmpl_client +del "template_*.go" +cd .. del "gosora.exe" echo Generating the dynamic code diff --git a/run.bat b/run.bat index b7df9533..8f173b2b 100644 --- a/run.bat +++ b/run.bat @@ -2,7 +2,9 @@ rem TODO: Make these deletes a little less noisy del "template_*.go" del "gen_*.go" -del "tmpl_client/template_*.go" +cd tmpl_client +del "template_*.go" +cd .. del "gosora.exe" echo Generating the dynamic code diff --git a/run_mssql.bat b/run_mssql.bat index bbaf43d7..95e502bd 100644 --- a/run_mssql.bat +++ b/run_mssql.bat @@ -2,7 +2,9 @@ rem TODO: Make these deletes a little less noisy del "template_*.go" del "gen_*.go" -del "tmpl_client/template_*.go" +cd tmpl_client +del "template_*.go" +cd .. del "gosora.exe" echo Generating the dynamic code diff --git a/run_tests.bat b/run_tests.bat index f94f7698..c26d6e7a 100644 --- a/run_tests.bat +++ b/run_tests.bat @@ -2,7 +2,9 @@ rem TODO: Make these deletes a little less noisy del "template_*.go" del "gen_*.go" -del "tmpl_client/template_*.go" +cd tmpl_client +del "template_*.go" +cd .. del "gosora.exe" echo Generating the dynamic code diff --git a/run_tests_mssql.bat b/run_tests_mssql.bat index 876ad8c2..18eef5f9 100644 --- a/run_tests_mssql.bat +++ b/run_tests_mssql.bat @@ -2,7 +2,9 @@ rem TODO: Make these deletes a little less noisy del "template_*.go" del "gen_*.go" -del "tmpl_client/template_*.go" +cd tmpl_client +del "template_*.go" +cd .. del "gosora.exe" echo Generating the dynamic code diff --git a/templates/account_own_edit.html b/templates/account_own_edit.html index 48226698..c29ba2ad 100644 --- a/templates/account_own_edit.html +++ b/templates/account_own_edit.html @@ -6,8 +6,6 @@
- Saved -
diff --git a/templates/forum.html b/templates/forum.html index a0d6802c..8b84492b 100644 --- a/templates/forum.html +++ b/templates/forum.html @@ -39,21 +39,21 @@
{{if .CurrentUser.Perms.CreateTopic}} -