Added the AboutSegment feature, you can see this in use on Cosora, it's a little raw right now, but I'm planning to polish it in the next commit.

Refactored the code to use switches instead of if blocks in some places.
Refactored the Dashboard to make it easier to add icons to it like I did with Cosora.
You can now use maps in transpiled templates.
Made progress on Cosora's footer.
Swapped out the ThemeName property in the HeaderVars struct for a more general and flexible Theme property.
Added the colstack CSS class to make it easier to style the layouts for the Control Panel and profile.
Renamed the FStore variable to Forums.
Renamed the Fpstore variable to FPStore.
Renamed the Gstore variable to Groups.
Split the MemoryTopicStore into DefaultTopicStore and MemoryTopicCache.
Split the MemoryUserStore into DefaultUserStore and MemoryUserCache.
Removed the NullUserStore, SQLUserStore, and SQLTopicStore.
Added the NullTopicCache and NullUserCache.
Moved the Reload method out of the TopicCache interface and into the TopicStore one.
Moved the Reload method out of the UserCache interface and into the UserStore one.
Added the SetCache and GetCache methods to the TopicStore and UserStore.
Added the BypassGetAll method to the WordFilterMap type.
Renamed routePanelSetting to routePanelSettingEdit.
Renamed routePanelSettingEdit to routePanelSettingEditSubmit.
Moved the page titles into the english language pack.
Split main() into main and afterDBInit to avoid code duplication in general_test.go
Added the ReqIsJson method so that we don't have to sniff the headers every time.
Added the LogStore interface.
Added the SQLModLogStore and the SQLAdminLogStore.
Refactored the phrase system to use getPhrasePlaceholder instead of hard-coding the string to return in a bunch of functions.
Removed a redundant rank check.
Added the GuildStore to plugin_guilds.
Added the about_segment_title and about_segment_body settings.
Refactored the setting system to use predefined errors to make it easier for an upstream caller to filter out sensitive error messages as opposed to safe errors.
Added the BypassGetAll method to the SettingMap type.
Added the Update method to the SettingMap type.
BulkGet is now exposed via the MemoryUserCache.
Refactored more logs in the template transpiler to reduce the amount of indentation.
Refactored the tests to take up fewer lines.
Further improved the Cosora theme's colours, padding, and profiles.
Added styling for the Control Panel Dashboard to the Cosora Theme.
Reduced the amount of code duplication in the installer query generator and opened the door to certain types of auto-migrations.
Refactored the Control Panel Dashboard to reduce the amount of code duplication.
Refactored the modlog route to reduce the amount of code duplication and string concatenation.
This commit is contained in:
Azareal 2017-11-23 05:37:08 +00:00
parent 8d0479f4b2
commit 381ce3083a
93 changed files with 1890 additions and 1631 deletions

View File

@ -192,6 +192,8 @@ More images in the /images/ folder. Beware though, some of them are *really* out
* ithub.com/denisenkom/go-mssqldb For interfacing with MSSQL. You will be able to pick this instead of MSSQL soon.
* github.com/go-ego/riot A search engine library.
# Bundled Plugins
There are several plugins which are bundled with the software by default. These cover various common tasks which aren't common enough to clutter the core with or which have competing implementation methods (E.g. plugin_markdown vs plugin_bbcode for post mark-up).

View File

@ -6,31 +6,64 @@ import (
"../query_gen/lib"
)
type LogStmts struct {
addModLogEntry *sql.Stmt
addAdminLogEntry *sql.Stmt
var ModLogs LogStore
var AdminLogs LogStore
type LogStore interface {
Create(action string, elementID int, elementType string, ipaddress string, actorID int) (err error)
GlobalCount() int
}
var logStmts LogStmts
type SQLModLogStore struct {
create *sql.Stmt
count *sql.Stmt
}
func init() {
DbInits.Add(func(acc *qgen.Accumulator) error {
logStmts = LogStmts{
addModLogEntry: acc.Insert("moderation_logs").Columns("action, elementID, elementType, ipaddress, actorID, doneAt").Fields("?,?,?,?,?,UTC_TIMESTAMP()").Prepare(),
addAdminLogEntry: acc.Insert("administration_logs").Columns("action, elementID, elementType, ipaddress, actorID, doneAt").Fields("?,?,?,?,?,UTC_TIMESTAMP()").Prepare(),
func NewModLogStore() (*SQLModLogStore, error) {
acc := qgen.Builder.Accumulator()
return &SQLModLogStore{
create: acc.Insert("moderation_logs").Columns("action, elementID, elementType, ipaddress, actorID, doneAt").Fields("?,?,?,?,?,UTC_TIMESTAMP()").Prepare(),
count: acc.Count("moderation_logs").Prepare(),
}, acc.FirstError()
}
// TODO: Make a store for this?
func (store *SQLModLogStore) Create(action string, elementID int, elementType string, ipaddress string, actorID int) (err error) {
_, err = store.create.Exec(action, elementID, elementType, ipaddress, actorID)
return err
}
func (store *SQLModLogStore) GlobalCount() (logCount int) {
err := store.count.QueryRow().Scan(&logCount)
if err != nil {
LogError(err)
}
return acc.FirstError()
})
return logCount
}
type SQLAdminLogStore struct {
create *sql.Stmt
count *sql.Stmt
}
func NewAdminLogStore() (*SQLAdminLogStore, error) {
acc := qgen.Builder.Accumulator()
return &SQLAdminLogStore{
create: acc.Insert("administration_logs").Columns("action, elementID, elementType, ipaddress, actorID, doneAt").Fields("?,?,?,?,?,UTC_TIMESTAMP()").Prepare(),
count: acc.Count("administration_logs").Prepare(),
}, acc.FirstError()
}
// TODO: Make a store for this?
func AddModLog(action string, elementID int, elementType string, ipaddress string, actorID int) (err error) {
_, err = logStmts.addModLogEntry.Exec(action, elementID, elementType, ipaddress, actorID)
func (store *SQLAdminLogStore) Create(action string, elementID int, elementType string, ipaddress string, actorID int) (err error) {
_, err = store.create.Exec(action, elementID, elementType, ipaddress, actorID)
return err
}
// TODO: Make a store for this?
func AddAdminLog(action string, elementID string, elementType int, ipaddress string, actorID int) (err error) {
_, err = logStmts.addAdminLogEntry.Exec(action, elementID, elementType, ipaddress, actorID)
return err
func (store *SQLAdminLogStore) GlobalCount() (logCount int) {
err := store.count.QueryRow().Scan(&logCount)
if err != nil {
LogError(err)
}
return logCount
}

View File

@ -91,9 +91,9 @@ func (auth *DefaultAuth) ForceLogout(uid int) error {
}
// Flush the user out of the cache
ucache, ok := Users.(UserCache)
if ok {
ucache.CacheRemove(uid)
ucache := Users.GetCache()
if ucache != nil {
ucache.Remove(uid)
}
return nil
@ -170,9 +170,9 @@ func (auth *DefaultAuth) CreateSession(uid int) (session string, err error) {
}
// Flush the user data from the cache
ucache, ok := Users.(UserCache)
if ok {
ucache.CacheRemove(uid)
ucache := Users.GetCache()
if ucache != nil {
ucache.Remove(uid)
}
return session, nil
}

View File

@ -87,7 +87,7 @@ func (forum *Forum) Update(name string, desc string, active bool, preset string)
return err
}
}
_ = Fstore.Reload(forum.ID)
_ = Forums.Reload(forum.ID)
return nil
}
@ -113,11 +113,11 @@ func (forum *Forum) setPreset(fperms *ForumPerms, preset string, gid int) (err e
LogError(err)
return errors.New("Unable to update the forum")
}
err = Fstore.Reload(forum.ID)
err = Forums.Reload(forum.ID)
if err != nil {
return errors.New("Unable to reload forum")
}
err = Fpstore.ReloadGroup(forum.ID, gid)
err = FPStore.ReloadGroup(forum.ID, gid)
if err != nil {
return errors.New("Unable to reload the forum permissions")
}

View File

@ -22,6 +22,7 @@ var LocalPermList = []string{
"CloseTopic",
}
// TODO: Rename this to ForumPermSet?
/* Inherit from group permissions for ones we don't have */
type ForumPerms struct {
ViewTopic bool
@ -165,7 +166,7 @@ func PermmapToQuery(permmap map[string]*ForumPerms, fid int) error {
if err != nil {
return err
}
return Fpstore.Reload(fid)
return FPStore.Reload(fid)
}
func ReplaceForumPermsForGroup(gid int, presetSet map[int]string, permSets map[int]*ForumPerms) error {

View File

@ -9,7 +9,7 @@ import (
"../query_gen/lib"
)
var Fpstore ForumPermsStore
var FPStore ForumPermsStore
type ForumPermsStore interface {
Init() error
@ -41,7 +41,7 @@ func NewMemoryForumPermsStore() (*MemoryForumPermsStore, error) {
func (fps *MemoryForumPermsStore) Init() error {
fps.updateMutex.Lock()
defer fps.updateMutex.Unlock()
fids, err := Fstore.GetAllIDs()
fids, err := Forums.GetAllIDs()
if err != nil {
return err
}
@ -55,7 +55,6 @@ func (fps *MemoryForumPermsStore) Init() error {
debugLog("Adding the forum permissions")
debugDetail("forumPerms[gid][fid]")
// Temporarily store the forum perms in a map before transferring it to a much faster and thread-safe slice
forumPerms = make(map[int]map[int]*ForumPerms)
for rows.Next() {
var gid, fid int
@ -97,7 +96,7 @@ func (fps *MemoryForumPermsStore) Reload(fid int) error {
fps.updateMutex.Lock()
defer fps.updateMutex.Unlock()
debugLogf("Reloading the forum permissions for forum #%d", fid)
fids, err := Fstore.GetAllIDs()
fids, err := Forums.GetAllIDs()
if err != nil {
return err
}
@ -143,7 +142,7 @@ func (fps *MemoryForumPermsStore) ReloadGroup(fid int, gid int) (err error) {
if err != nil {
return err
}
group, err := Gstore.Get(gid)
group, err := Groups.Get(gid)
if err != nil {
return err
}
@ -153,7 +152,7 @@ func (fps *MemoryForumPermsStore) ReloadGroup(fid int, gid int) (err error) {
}
func (fps *MemoryForumPermsStore) cascadePermSetToGroups(forumPerms map[int]map[int]*ForumPerms, fids []int) error {
groups, err := Gstore.GetAll()
groups, err := Groups.GetAll()
if err != nil {
return err
}
@ -200,7 +199,7 @@ func (fps *MemoryForumPermsStore) cascadePermSetToGroup(forumPerms map[int]map[i
// TODO: Add a hook here and have plugin_guilds use it
func (fps *MemoryForumPermsStore) Get(fid int, gid int) (fperms *ForumPerms, err error) {
group, err := Gstore.Get(gid)
group, err := Groups.Get(gid)
if err != nil {
return fperms, ErrNoRows
}

View File

@ -19,7 +19,7 @@ import (
var forumCreateMutex sync.Mutex
var forumPerms map[int]map[int]*ForumPerms // [gid][fid]*ForumPerms // TODO: Add an abstraction around this and make it more thread-safe
var Fstore ForumStore
var Forums ForumStore
// ForumStore is an interface for accessing the forums and the metadata stored on them
type ForumStore interface {

View File

@ -51,7 +51,7 @@ func (group *Group) ChangeRank(isAdmin bool, isMod bool, isBanned bool) (err err
return err
}
Gstore.Reload(group.ID)
Groups.Reload(group.ID)
return nil
}

View File

@ -12,7 +12,7 @@ import (
"../query_gen/lib"
)
var Gstore GroupStore
var Groups GroupStore
// ? - We could fallback onto the database when an item can't be found in the cache?
type GroupStore interface {
@ -230,7 +230,7 @@ func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod
}
// Generate the forum permissions based on the presets...
fdata, err := Fstore.GetAll()
fdata, err := Forums.GetAll()
if err != nil {
return 0, err
}
@ -279,7 +279,7 @@ func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod
mgs.Unlock()
for _, forum := range fdata {
err = Fpstore.Reload(forum.ID)
err = FPStore.Reload(forum.ID)
if err != nil {
return gid, err
}

View File

@ -0,0 +1,55 @@
package common
type NullTopicCache struct {
}
// NewNullTopicCache gives you a new instance of NullTopicCache
func NewNullTopicCache() *NullTopicCache {
return &NullTopicCache{}
}
func (mts *NullTopicCache) Get(id int) (*Topic, error) {
return nil, ErrNoRows
}
func (mts *NullTopicCache) GetUnsafe(id int) (*Topic, error) {
return nil, ErrNoRows
}
func (mts *NullTopicCache) Set(_ *Topic) error {
return nil
}
func (mts *NullTopicCache) Add(item *Topic) error {
_ = item
return nil
}
// TODO: Make these length increments thread-safe. Ditto for the other DataStores
func (mts *NullTopicCache) AddUnsafe(item *Topic) error {
_ = item
return nil
}
// TODO: Make these length decrements thread-safe. Ditto for the other DataStores
func (mts *NullTopicCache) Remove(id int) error {
return nil
}
func (mts *NullTopicCache) RemoveUnsafe(id int) error {
return nil
}
func (mts *NullTopicCache) Flush() {
}
func (mts *NullTopicCache) Length() int {
return 0
}
func (mts *NullTopicCache) SetCapacity(_ int) {
}
func (mts *NullTopicCache) GetCapacity() int {
return 0
}

57
common/null_user_cache.go Normal file
View File

@ -0,0 +1,57 @@
package common
type NullUserCache struct {
}
// NewNullUserCache gives you a new instance of NullUserCache
func NewNullUserCache() *NullUserCache {
return &NullUserCache{}
}
func (mus *NullUserCache) Get(id int) (*User, error) {
return nil, ErrNoRows
}
func (mus *NullUserCache) BulkGet(ids []int) (list []*User) {
return list
}
func (mus *NullUserCache) GetUnsafe(id int) (*User, error) {
return nil, ErrNoRows
}
func (mus *NullUserCache) Set(_ *User) error {
return nil
}
func (mus *NullUserCache) Add(item *User) error {
_ = item
return nil
}
func (mus *NullUserCache) AddUnsafe(item *User) error {
_ = item
return nil
}
func (mus *NullUserCache) Remove(id int) error {
return nil
}
func (mus *NullUserCache) RemoveUnsafe(id int) error {
return nil
}
func (mus *NullUserCache) Flush() {
}
func (mus *NullUserCache) Length() int {
return 0
}
func (mus *NullUserCache) SetCapacity(_ int) {
}
func (mus *NullUserCache) GetCapacity() int {
return 0
}

View File

@ -13,8 +13,8 @@ type HeaderVars struct {
Widgets PageWidgets
Site *site
Settings SettingMap
Themes map[string]Theme // TODO: Use a slice containing every theme instead of the main map for speed
ThemeName string
Themes map[string]Theme // TODO: Use a slice containing every theme instead of the main map for speed?
Theme Theme
//TemplateName string // TODO: Use this to move template calls to the router rather than duplicating them over and over and over?
ExtData ExtData
}
@ -282,5 +282,5 @@ type AreYouSure struct {
// This is mostly for errors.go, please create *HeaderVars on the spot instead of relying on this or the atomic store underlying it, if possible
// TODO: Write a test for this
func DefaultHeaderVar() *HeaderVars {
return &HeaderVars{Site: Site, ThemeName: fallbackTheme}
return &HeaderVars{Site: Site, Theme: Themes[fallbackTheme]}
}

View File

@ -219,7 +219,7 @@ func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/)
i += intLen
topic, err := Topics.Get(tid)
if err != nil || !Fstore.Exists(topic.ParentID) {
if err != nil || !Forums.Exists(topic.ParentID) {
outbytes = append(outbytes, InvalidTopic...)
lastItem = i
continue
@ -250,7 +250,7 @@ func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/)
reply := BlankReply()
reply.ID = rid
topic, err := reply.Topic()
if err != nil || !Fstore.Exists(topic.ParentID) {
if err != nil || !Forums.Exists(topic.ParentID) {
outbytes = append(outbytes, InvalidTopic...)
lastItem = i
continue
@ -271,7 +271,7 @@ func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/)
fid, intLen := CoerceIntBytes(msgbytes[start:])
i += intLen
if !Fstore.Exists(fid) {
if !Forums.Exists(fid) {
outbytes = append(outbytes, InvalidForum...)
lastItem = i
continue

View File

@ -189,7 +189,7 @@ func RebuildGroupPermissions(gid int) error {
return err
}
group, err := Gstore.Get(gid)
group, err := Groups.Get(gid)
if err != nil {
return err
}

View File

@ -17,6 +17,7 @@ import (
"sync/atomic"
)
// TODO: Add a phrase store?
// TODO: Let the admin edit phrases from inside the Control Panel? How should we persist these? Should we create a copy of the langpack or edit the primaries? Use the changeLangpack mutex for this?
// nolint Be quiet megacheck, this *is* used
var currentLangPack atomic.Value
@ -41,6 +42,8 @@ type LanguagePack struct {
SettingLabels map[string]string
PermPresets map[string]string
Accounts map[string]string // TODO: Apply these phrases in the software proper
Errors map[string]map[string]string // map[category]map[name]value
PageTitles map[string]string
}
// TODO: Add the ability to edit language JSON files from the Control Panel and automatically scan the files for changes
@ -114,7 +117,7 @@ func GetPhrase(name string) (string, bool) {
func GetGlobalPermPhrase(name string) string {
res, ok := currentLangPack.Load().(*LanguagePack).GlobalPerms[name]
if !ok {
return "{name}"
return getPhrasePlaceholder()
}
return res
}
@ -122,7 +125,7 @@ func GetGlobalPermPhrase(name string) string {
func GetLocalPermPhrase(name string) string {
res, ok := currentLangPack.Load().(*LanguagePack).LocalPerms[name]
if !ok {
return "{name}"
return getPhrasePlaceholder()
}
return res
}
@ -130,7 +133,7 @@ func GetLocalPermPhrase(name string) string {
func GetSettingLabel(name string) string {
res, ok := currentLangPack.Load().(*LanguagePack).SettingLabels[name]
if !ok {
return "{name}"
return getPhrasePlaceholder()
}
return res
}
@ -146,11 +149,32 @@ func GetAllPermPresets() map[string]string {
func GetAccountPhrase(name string) string {
res, ok := currentLangPack.Load().(*LanguagePack).Accounts[name]
if !ok {
return "{name}"
return getPhrasePlaceholder()
}
return res
}
// TODO: Does comma ok work with multi-dimensional maps?
func GetErrorPhrase(category string, name string) string {
res, ok := currentLangPack.Load().(*LanguagePack).Errors[category][name]
if !ok {
return getPhrasePlaceholder()
}
return res
}
func GetTitlePhrase(name string) string {
res, ok := currentLangPack.Load().(*LanguagePack).PageTitles[name]
if !ok {
return getPhrasePlaceholder()
}
return res
}
func getPhrasePlaceholder() string {
return "{name}"
}
// ? - Use runtime reflection for updating phrases?
// TODO: Implement these
func AddPhrase() {

View File

@ -112,9 +112,9 @@ func (reply *Reply) Delete() error {
}
// TODO: Move this bit to *Topic
_, err = replyStmts.removeRepliesFromTopic.Exec(1, reply.ParentID)
tcache, ok := Topics.(TopicCache)
if ok {
tcache.CacheRemove(reply.ParentID)
tcache := Topics.GetCache()
if tcache != nil {
tcache.Remove(reply.ParentID)
}
return err
}

View File

@ -31,8 +31,7 @@ func BuildWidgets(zone string, data interface{}, headerVars *HeaderVars, r *http
}
}
//log.Print("Themes[headerVars.ThemeName].Sidebars", Themes[headerVars.ThemeName].Sidebars)
if Themes[headerVars.ThemeName].Sidebars == "right" {
if Themes[headerVars.Theme.Name].Sidebars == "right" {
if len(Docks.RightSidebar) != 0 {
var sbody string
for _, widget := range Docks.RightSidebar {
@ -48,7 +47,7 @@ func BuildWidgets(zone string, data interface{}, headerVars *HeaderVars, r *http
}
func simpleForumUserCheck(w http.ResponseWriter, r *http.Request, user *User, fid int) (headerLite *HeaderLite, rerr RouteError) {
if !Fstore.Exists(fid) {
if !Forums.Exists(fid) {
return nil, PreError("The target forum doesn't exist.", w, r)
}
@ -61,7 +60,7 @@ func simpleForumUserCheck(w http.ResponseWriter, r *http.Request, user *User, fi
}
}
fperms, err := Fpstore.Get(fid, user.Group)
fperms, err := FPStore.Get(fid, user.Group)
if err != nil {
// TODO: Refactor this
log.Printf("Unable to get the forum perms for Group #%d for User #%d", user.Group, user.ID)
@ -76,7 +75,7 @@ func forumUserCheck(w http.ResponseWriter, r *http.Request, user *User, fid int)
if rerr != nil {
return headerVars, rerr
}
if !Fstore.Exists(fid) {
if !Forums.Exists(fid) {
return headerVars, NotFound(w, r)
}
@ -88,14 +87,12 @@ func forumUserCheck(w http.ResponseWriter, r *http.Request, user *User, fid int)
}
}
fperms, err := Fpstore.Get(fid, user.Group)
fperms, err := FPStore.Get(fid, user.Group)
if err != nil {
// TODO: Refactor this
log.Printf("Unable to get the forum perms for Group #%d for User #%d", user.Group, user.ID)
return nil, PreError("Something weird happened", w, r)
}
//log.Printf("user.Perms: %+v\n", user.Perms)
//log.Printf("fperms: %+v\n", fperms)
cascadeForumPerms(fperms, user)
return headerVars, rerr
}
@ -125,28 +122,30 @@ func cascadeForumPerms(fperms *ForumPerms, user *User) {
// Even if they have the right permissions, the control panel is only open to supermods+. There are many areas without subpermissions which assume that the current user is a supermod+ and admins are extremely unlikely to give these permissions to someone who isn't at-least a supermod to begin with
// TODO: Do a panel specific theme?
func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerVars *HeaderVars, stats PanelStats, rerr RouteError) {
var themeName = DefaultThemeBox.Load().(string)
var theme Theme
cookie, err := r.Cookie("current_theme")
if err == nil {
cookie := html.EscapeString(cookie.Value)
theme, ok := Themes[cookie]
inTheme, ok := Themes[html.EscapeString(cookie.Value)]
if ok && !theme.HideFromThemes {
themeName = cookie
theme = inTheme
}
}
if theme.Name == "" {
theme = Themes[DefaultThemeBox.Load().(string)]
}
headerVars = &HeaderVars{
Site: Site,
Settings: SettingBox.Load().(SettingMap),
Themes: Themes,
ThemeName: themeName,
Theme: theme,
}
// TODO: We should probably initialise headerVars.ExtData
headerVars.Stylesheets = append(headerVars.Stylesheets, headerVars.ThemeName+"/panel.css")
if len(Themes[headerVars.ThemeName].Resources) > 0 {
rlist := Themes[headerVars.ThemeName].Resources
headerVars.Stylesheets = append(headerVars.Stylesheets, theme.Name+"/panel.css")
if len(theme.Resources) > 0 {
rlist := theme.Resources
for _, resource := range rlist {
if resource.Location == "global" || resource.Location == "panel" {
extarr := strings.Split(resource.Name, ".")
@ -161,8 +160,8 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerV
}
stats.Users = Users.GlobalCount()
stats.Groups = Gstore.GlobalCount()
stats.Forums = Fstore.GlobalCount() // TODO: Stop it from showing the blanked forums
stats.Groups = Groups.GlobalCount()
stats.Forums = Forums.GlobalCount() // TODO: Stop it from showing the blanked forums
stats.Settings = len(headerVars.Settings)
stats.WordFilters = len(WordFilterBox.Load().(WordFilterMap))
stats.Themes = len(Themes)
@ -170,12 +169,17 @@ func panelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerV
pusher, ok := w.(http.Pusher)
if ok {
pusher.Push("/static/"+headerVars.ThemeName+"/main.css", nil)
pusher.Push("/static/"+headerVars.ThemeName+"/panel.css", nil)
pusher.Push("/static/"+theme.Name+"/main.css", nil)
pusher.Push("/static/"+theme.Name+"/panel.css", nil)
pusher.Push("/static/global.js", nil)
pusher.Push("/static/jquery-3.1.1.min.js", nil)
// TODO: Push the theme CSS files
// TODO: Push the theme scripts
// TODO: Test these
for _, sheet := range headerVars.Stylesheets {
pusher.Push("/static/"+sheet, nil)
}
for _, script := range headerVars.Scripts {
pusher.Push("/static/"+script, nil)
}
// TODO: Push avatars?
}
@ -209,30 +213,32 @@ func simpleUserCheck(w http.ResponseWriter, r *http.Request, user *User) (header
// TODO: Add the ability for admins to restrict certain themes to certain groups?
func userCheck(w http.ResponseWriter, r *http.Request, user *User) (headerVars *HeaderVars, rerr RouteError) {
var themeName = DefaultThemeBox.Load().(string)
var theme Theme
cookie, err := r.Cookie("current_theme")
if err == nil {
cookie := html.EscapeString(cookie.Value)
theme, ok := Themes[cookie]
inTheme, ok := Themes[html.EscapeString(cookie.Value)]
if ok && !theme.HideFromThemes {
themeName = cookie
theme = inTheme
}
}
if theme.Name == "" {
theme = Themes[DefaultThemeBox.Load().(string)]
}
headerVars = &HeaderVars{
Site: Site,
Settings: SettingBox.Load().(SettingMap),
Themes: Themes,
ThemeName: themeName,
Theme: theme,
}
if user.IsBanned {
headerVars.NoticeList = append(headerVars.NoticeList, "Your account has been suspended. Some of your permissions may have been revoked.")
}
if len(Themes[headerVars.ThemeName].Resources) > 0 {
rlist := Themes[headerVars.ThemeName].Resources
if len(theme.Resources) > 0 {
rlist := theme.Resources
for _, resource := range rlist {
if resource.Location == "global" || resource.Location == "frontend" {
extarr := strings.Split(resource.Name, ".")
@ -248,11 +254,16 @@ func userCheck(w http.ResponseWriter, r *http.Request, user *User) (headerVars *
pusher, ok := w.(http.Pusher)
if ok {
pusher.Push("/static/"+headerVars.ThemeName+"/main.css", nil)
pusher.Push("/static/"+theme.Name+"/main.css", nil)
pusher.Push("/static/global.js", nil)
pusher.Push("/static/jquery-3.1.1.min.js", nil)
// TODO: Push the theme CSS files
// TODO: Push the theme scripts
// TODO: Test these
for _, sheet := range headerVars.Stylesheets {
pusher.Push("/static/"+sheet, nil)
}
for _, script := range headerVars.Scripts {
pusher.Push("/static/"+script, nil)
}
// TODO: Push avatars?
}
@ -344,3 +355,7 @@ func NoSessionMismatch(w http.ResponseWriter, r *http.Request, user User) RouteE
}
return nil
}
func ReqIsJson(r *http.Request) bool {
return r.Header.Get("Content-type") == "application/json"
}

View File

@ -2,6 +2,7 @@ package common
import (
"database/sql"
"errors"
"strconv"
"strings"
"sync/atomic"
@ -17,6 +18,7 @@ type SettingMap map[string]interface{}
type SettingStore interface {
ParseSetting(sname string, scontent string, stype string, sconstraint string) string
BypassGet(name string) (*Setting, error)
BypassGetAll(name string) ([]*Setting, error)
}
type OptionLabel struct {
@ -35,6 +37,7 @@ type Setting struct {
type SettingStmts struct {
getAll *sql.Stmt
get *sql.Stmt
update *sql.Stmt
}
var settingStmts SettingStmts
@ -45,80 +48,83 @@ func init() {
settingStmts = SettingStmts{
getAll: acc.Select("settings").Columns("name, content, type, constraints").Prepare(),
get: acc.Select("settings").Columns("content, type, constraints").Where("name = ?").Prepare(),
update: acc.Update("settings").Set("content = ?").Where("name = ?").Prepare(),
}
return acc.FirstError()
})
}
func LoadSettings() error {
rows, err := settingStmts.getAll.Query()
if err != nil {
return err
}
defer rows.Close()
var sBox = SettingMap(make(map[string]interface{}))
var sname, scontent, stype, sconstraints string
for rows.Next() {
err = rows.Scan(&sname, &scontent, &stype, &sconstraints)
settings, err := sBox.BypassGetAll()
if err != nil {
return err
}
errmsg := sBox.ParseSetting(sname, scontent, stype, sconstraints)
if errmsg != "" {
return err
}
}
err = rows.Err()
for _, setting := range settings {
err = sBox.ParseSetting(setting.Name, setting.Content, setting.Type, setting.Constraint)
if err != nil {
return err
}
}
SettingBox.Store(sBox)
return nil
}
// nolint
var ErrNotInteger = errors.New("You were supposed to enter an integer x.x")
var ErrSettingNotInteger = errors.New("Only integers are allowed in this setting x.x")
var ErrBadConstraintNotInteger = errors.New("Invalid contraint! The constraint field wasn't an integer!")
var ErrBadSettingRange = errors.New("Only integers between a certain range are allowed in this setting")
// To avoid leaking internal state to the user
// TODO: We need to add some sort of DualError interface
func SafeSettingError(err error) bool {
return err == ErrNotInteger || err == ErrSettingNotInteger || err == ErrBadConstraintNotInteger || err == ErrBadSettingRange || err == ErrNoRows
}
// TODO: Add better support for HTML attributes (html-attribute). E.g. Meta descriptions.
func (sBox SettingMap) ParseSetting(sname string, scontent string, stype string, constraint string) string {
var err error
func (sBox SettingMap) ParseSetting(sname string, scontent string, stype string, constraint string) (err error) {
var ssBox = map[string]interface{}(sBox)
if stype == "bool" {
switch stype {
case "bool":
ssBox[sname] = (scontent == "1")
} else if stype == "int" {
case "int":
ssBox[sname], err = strconv.Atoi(scontent)
if err != nil {
return "You were supposed to enter an integer x.x\nType mismatch in " + sname
return ErrNotInteger
}
} else if stype == "int64" {
case "int64":
ssBox[sname], err = strconv.ParseInt(scontent, 10, 64)
if err != nil {
return "You were supposed to enter an integer x.x\nType mismatch in " + sname
return ErrNotInteger
}
} else if stype == "list" {
case "list":
cons := strings.Split(constraint, "-")
if len(cons) < 2 {
return "Invalid constraint! The second field wasn't set!"
return errors.New("Invalid constraint! The second field wasn't set!")
}
con1, err := strconv.Atoi(cons[0])
con2, err2 := strconv.Atoi(cons[1])
if err != nil || err2 != nil {
return "Invalid contraint! The constraint field wasn't an integer!"
return ErrBadConstraintNotInteger
}
value, err := strconv.Atoi(scontent)
if err != nil {
return "Only integers are allowed in this setting x.x\nType mismatch in " + sname
return ErrSettingNotInteger
}
if value < con1 || value > con2 {
return "Only integers between a certain range are allowed in this setting"
return ErrBadSettingRange
}
ssBox[sname] = value
} else {
default:
ssBox[sname] = scontent
}
return ""
return nil
}
func (sBox SettingMap) BypassGet(name string) (*Setting, error) {
@ -126,3 +132,51 @@ func (sBox SettingMap) BypassGet(name string) (*Setting, error) {
err := settingStmts.get.QueryRow(name).Scan(&setting.Content, &setting.Type, &setting.Constraint)
return setting, err
}
func (sBox SettingMap) BypassGetAll() (settingList []*Setting, err error) {
rows, err := settingStmts.getAll.Query()
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
setting := &Setting{Name: ""}
err := rows.Scan(&setting.Name, &setting.Content, &setting.Type, &setting.Constraint)
if err != nil {
return nil, err
}
settingList = append(settingList, setting)
}
return settingList, rows.Err()
}
func (sBox SettingMap) Update(name string, content string) error {
setting, err := sBox.BypassGet(name)
if err == ErrNoRows {
return err
}
// TODO: Why is this here and not in a common function?
if setting.Type == "bool" {
if content == "on" || content == "1" {
content = "1"
} else {
content = "0"
}
}
// TODO: Make this a method or function?
_, err = settingStmts.update.Exec(content, name)
if err != nil {
return err
}
err = sBox.ParseSetting(name, content, setting.Type, setting.Constraint)
if err != nil {
return err
}
// TODO: Do a reload instead?
SettingBox.Store(sBox)
return nil
}

View File

@ -99,7 +99,7 @@ func ProcessConfig() error {
}
func VerifyConfig() error {
if !Fstore.Exists(Config.DefaultForum) {
if !Forums.Exists(Config.DefaultForum) {
return errors.New("Invalid default forum")
}
return nil

View File

@ -67,7 +67,7 @@ func HandleServerSync() error {
if lastUpdate.After(lastSync) {
// TODO: A more granular sync
err = Fstore.LoadForums()
err = Forums.LoadForums()
if err != nil {
log.Print("Unable to reload the forums")
return err

View File

@ -96,7 +96,7 @@ func compileTemplates() error {
Site: Site,
Settings: SettingBox.Load().(SettingMap),
Themes: Themes,
ThemeName: DefaultThemeBox.Load().(string),
Theme: Themes[DefaultThemeBox.Load().(string)],
NoticeList: []string{"test"},
Stylesheets: []string{"panel"},
Scripts: []string{"whatever"},
@ -132,7 +132,7 @@ func compileTemplates() error {
// TODO: Use a dummy forum list to avoid o(n) problems
var forumList []Forum
forums, err := Fstore.GetAll()
forums, err := Forums.GetAll()
if err != nil {
return err
}

View File

@ -37,6 +37,8 @@ type CTemplateSet struct {
FragOut string
varList map[string]VarItem
localVars map[string]map[string]VarItemReflect
hasDispInt bool
localDispStructIndex int
stats map[string]int
pVarList string
pVarPosition int
@ -102,6 +104,8 @@ func (c *CTemplateSet) Compile(name string, dir string, expects string, expectsI
}
c.varList = varList
c.hasDispInt = false
c.localDispStructIndex = 0
//c.pVarList = ""
//c.pVarPosition = 0
c.stats = make(map[string]int)
@ -187,7 +191,6 @@ w.Write([]byte(`, " + ", -1)
c.log("Output!")
c.log(fout)
//log.Fatal("remove the log.Fatal line")
return fout, nil
}
@ -334,10 +337,8 @@ func (c *CTemplateSet) compileSubswitch(varholder string, holdreflect reflect.Va
varbit += ".(" + cur.Type().Name() + ")"
}
for _, id := range n.Ident {
c.log("Data Kind:", cur.Kind().String())
c.log("Field Bit:", id)
// ! Might not work so well for non-struct pointers
skipPointers := func(cur reflect.Value, id string) reflect.Value {
if cur.Kind() == reflect.Ptr {
c.log("Looping over pointer")
for cur.Kind() == reflect.Ptr {
@ -346,40 +347,87 @@ func (c *CTemplateSet) compileSubswitch(varholder string, holdreflect reflect.Va
c.log("Data Kind:", cur.Kind().String())
c.log("Field Bit:", id)
}
return cur
}
var assLines string
var multiline = false
for _, id := range n.Ident {
c.log("Data Kind:", cur.Kind().String())
c.log("Field Bit:", id)
cur = skipPointers(cur, id)
if !cur.IsValid() {
if c.debug {
fmt.Println("Debug Data:")
fmt.Println("Holdreflect:", holdreflect)
fmt.Println("Holdreflect.Kind():", holdreflect.Kind())
c.error("Debug Data:")
c.error("Holdreflect:", holdreflect)
c.error("Holdreflect.Kind():", holdreflect.Kind())
if !c.superDebug {
fmt.Println("cur.Kind():", cur.Kind().String())
c.error("cur.Kind():", cur.Kind().String())
}
fmt.Println("")
}
c.error("")
if !multiline {
panic(varholder + varbit + "^\n" + "Invalid value. Maybe, it doesn't exist?")
} else {
panic(varbit + "^\n" + "Invalid value. Maybe, it doesn't exist?")
}
}
cur = cur.FieldByName(id)
c.log("in-loop varbit: " + varbit)
if cur.Kind() == reflect.Map {
cur = cur.MapIndex(reflect.ValueOf(id))
varbit += "[\"" + id + "\"]"
cur = skipPointers(cur, id)
if cur.Kind() == reflect.Struct || cur.Kind() == reflect.Interface {
// TODO: Move the newVarByte declaration to the top level or to the if level, if a dispInt is only used in a particular if statement
var dispStr, newVarByte string
if cur.Kind() == reflect.Interface {
cur = cur.Elem()
// TODO: Surely, there's a better way of detecting this?
/*if cur.Kind() == reflect.String && cur.Type().Name() != "string" {
varbit = "string(" + varbit + "." + id + ")"*/
//if cur.Kind() == reflect.String && cur.Type().Name() != "string" {
if cur.Type().PkgPath() != "main" && cur.Type().PkgPath() != "" {
c.importMap["html/template"] = "html/template"
varbit += "." + id + ".(" + strings.TrimPrefix(cur.Type().PkgPath(), "html/") + "." + cur.Type().Name() + ")"
} else {
varbit += "." + id + ".(" + cur.Type().Name() + ")"
dispStr = "Int"
if !c.hasDispInt {
newVarByte = ":"
c.hasDispInt = true
}
}
// TODO: De-dupe identical struct types rather than allocating a variable for each one
if cur.Kind() == reflect.Struct {
dispStr = "Struct" + strconv.Itoa(c.localDispStructIndex)
newVarByte = ":"
c.localDispStructIndex++
}
varbit = "disp" + dispStr + " " + newVarByte + "= " + varholder + varbit + "\n"
varholder = "disp" + dispStr
multiline = true
} else {
continue
}
}
if cur.Kind() != reflect.Interface {
cur = cur.FieldByName(id)
varbit += "." + id
}
c.log("End Cycle")
// TODO: Handle deeply nested pointers mixed with interfaces mixed with pointers better
if cur.Kind() == reflect.Interface {
cur = cur.Elem()
varbit += ".("
// TODO: Surely, there's a better way of doing this?
if cur.Type().PkgPath() != "main" && cur.Type().PkgPath() != "" {
c.importMap["html/template"] = "html/template"
varbit += strings.TrimPrefix(cur.Type().PkgPath(), "html/") + "."
}
out = c.compileVarsub(varholder+varbit, cur)
varbit += cur.Type().Name() + ")"
}
c.log("End Cycle: ", varbit)
}
if multiline {
assSplit := strings.Split(varbit, "\n")
varbit = assSplit[len(assSplit)-1]
assSplit = assSplit[:len(assSplit)-1]
assLines = strings.Join(assSplit, "\n") + "\n"
}
varbit = varholder + varbit
out = c.compileVarsub(varbit, cur, assLines)
for _, varItem := range c.varList {
if strings.HasPrefix(out, varItem.Destination) {
@ -389,20 +437,21 @@ func (c *CTemplateSet) compileSubswitch(varholder string, holdreflect reflect.Va
return out
case *parse.DotNode:
c.log("Dot Node:", node.String())
return c.compileVarsub(varholder, holdreflect)
return c.compileVarsub(varholder, holdreflect, "")
case *parse.NilNode:
panic("Nil is not a command x.x")
case *parse.VariableNode:
c.log("Variable Node:", n.String())
c.log(n.Ident)
varname, reflectVal := c.compileIfVarsub(n.String(), varholder, templateName, holdreflect)
return c.compileVarsub(varname, reflectVal)
return c.compileVarsub(varname, reflectVal, "")
case *parse.StringNode:
return n.Quoted
case *parse.IdentifierNode:
c.log("Identifier Node:", node)
c.log("Identifier Node Args:", node.Args)
return c.compileVarsub(c.compileIdentSwitch(varholder, holdreflect, templateName, node))
out, outval := c.compileIdentSwitch(varholder, holdreflect, templateName, node)
return c.compileVarsub(out, outval, "")
default:
return c.unknownNode(node)
}
@ -525,7 +574,6 @@ func (c *CTemplateSet) compareJoin(varholder string, holdreflect reflect.Value,
func (c *CTemplateSet) compileIdentSwitch(varholder string, holdreflect reflect.Value, templateName string, node *parse.CommandNode) (out string, val reflect.Value) {
c.log("in compileIdentSwitch")
//var outbuf map[int]string
ArgLoop:
for pos := 0; pos < len(node.Args); pos++ {
id := node.Args[pos]
@ -728,7 +776,7 @@ func (c *CTemplateSet) compileBoolsub(varname string, varholder string, template
return out
}
func (c *CTemplateSet) compileVarsub(varname string, val reflect.Value) string {
func (c *CTemplateSet) compileVarsub(varname string, val reflect.Value, assLines string) (out string) {
c.log("in compileVarsub")
for _, varItem := range c.varList {
if strings.HasPrefix(varname, varItem.Destination) {
@ -747,29 +795,33 @@ func (c *CTemplateSet) compileVarsub(varname string, val reflect.Value) string {
val = val.Elem()
}
c.log("varname: ", varname)
c.log("assLines: ", assLines)
switch val.Kind() {
case reflect.Int:
c.importMap["strconv"] = "strconv"
return "w.Write([]byte(strconv.Itoa(" + varname + ")))\n"
out = "w.Write([]byte(strconv.Itoa(" + varname + ")))\n"
case reflect.Bool:
return "if " + varname + " {\nw.Write([]byte(\"true\"))} else {\nw.Write([]byte(\"false\"))\n}\n"
out = "if " + varname + " {\nw.Write([]byte(\"true\"))} else {\nw.Write([]byte(\"false\"))\n}\n"
case reflect.String:
if val.Type().Name() != "string" && !strings.HasPrefix(varname, "string(") {
return "w.Write([]byte(string(" + varname + ")))\n"
varname = "string(" + varname + ")"
}
return "w.Write([]byte(" + varname + "))\n"
out = "w.Write([]byte(" + varname + "))\n"
case reflect.Int64:
c.importMap["strconv"] = "strconv"
return "w.Write([]byte(strconv.FormatInt(" + varname + ", 10)))"
out = "w.Write([]byte(strconv.FormatInt(" + varname + ", 10)))"
default:
if !val.IsValid() {
panic(varname + "^\n" + "Invalid value. Maybe, it doesn't exist?")
panic(assLines + varname + "^\n" + "Invalid value. Maybe, it doesn't exist?")
}
fmt.Println("Unknown Variable Name:", varname)
fmt.Println("Unknown Kind:", val.Kind())
fmt.Println("Unknown Type:", val.Type().Name())
panic("// I don't know what this variable's type is o.o\n")
}
c.log("out: ", out)
return assLines + out
}
func (c *CTemplateSet) compileSubtemplate(pvarholder string, pholdreflect reflect.Value, node *parse.TemplateNode) (out string) {
@ -831,6 +883,12 @@ func (c *CTemplateSet) log(args ...interface{}) {
}
}
func (c *CTemplateSet) error(args ...interface{}) {
if c.debug {
fmt.Println(args...)
}
}
func (c *CTemplateSet) compileCommand(*parse.CommandNode) (out string) {
panic("Uh oh! Something went wrong!")
}

View File

@ -22,7 +22,7 @@ import (
"../query_gen/lib"
)
type ThemeList map[string]Theme
type ThemeList map[string]Theme // ? Use pointers instead?
var Themes ThemeList = make(map[string]Theme)
var DefaultThemeBox atomic.Value
@ -47,6 +47,7 @@ type Theme struct {
Tag string
URL string
Sidebars string // Allowed Values: left, right, both, false
AboutSegment bool // ? - Should this be a theme var instead?
//DisableMinifier // Is this really a good idea? I don't think themes should be fighting against the minifier
Settings map[string]ThemeSetting
Templates []TemplateMapping

View File

@ -149,9 +149,9 @@ func init() {
// Flush the topic out of the cache
// ? - We do a CacheRemove() here instead of mutating the pointer to avoid creating a race condition
func (topic *Topic) cacheRemove() {
tcache, ok := Topics.(TopicCache)
if ok {
tcache.CacheRemove(topic.ID)
tcache := Topics.(TopicCache)
if tcache != nil {
tcache.Remove(topic.ID)
}
}
@ -226,7 +226,7 @@ func (topic *Topic) Delete() error {
return err
}
err = Fstore.RemoveTopic(topic.ParentID)
err = Forums.RemoveTopic(topic.ParentID)
if err != nil && err != ErrNoRows {
return err
}
@ -263,10 +263,10 @@ func (topic *Topic) Copy() Topic {
// TODO: Refactor the caller to take a Topic and a User rather than a combined TopicUser
func GetTopicUser(tid int) (TopicUser, error) {
tcache, tok := Topics.(TopicCache)
ucache, uok := Users.(UserCache)
if tok && uok {
topic, err := tcache.CacheGet(tid)
tcache := Topics.GetCache()
ucache := Users.GetCache()
if tcache != nil && ucache != nil {
topic, err := tcache.Get(tid)
if err == nil {
user, err := Users.Get(topic.CreatedBy)
if err != nil {
@ -292,12 +292,12 @@ func GetTopicUser(tid int) (TopicUser, error) {
err := topicStmts.getTopicUser.QueryRow(tid).Scan(&tu.Title, &tu.Content, &tu.CreatedBy, &tu.CreatedAt, &tu.IsClosed, &tu.Sticky, &tu.ParentID, &tu.IPAddress, &tu.PostCount, &tu.LikeCount, &tu.CreatedByName, &tu.Avatar, &tu.Group, &tu.URLPrefix, &tu.URLName, &tu.Level)
tu.Link = BuildTopicURL(NameToSlug(tu.Title), tu.ID)
tu.UserLink = BuildProfileURL(NameToSlug(tu.CreatedByName), tu.CreatedBy)
tu.Tag = Gstore.DirtyGet(tu.Group).Tag
tu.Tag = Groups.DirtyGet(tu.Group).Tag
if tok {
if tcache != nil {
theTopic := Topic{ID: tu.ID, Link: tu.Link, Title: tu.Title, Content: tu.Content, CreatedBy: tu.CreatedBy, IsClosed: tu.IsClosed, Sticky: tu.Sticky, CreatedAt: tu.CreatedAt, LastReplyAt: tu.LastReplyAt, ParentID: tu.ParentID, IPAddress: tu.IPAddress, PostCount: tu.PostCount, LikeCount: tu.LikeCount}
//log.Printf("theTopic: %+v\n", theTopic)
_ = tcache.CacheAdd(&theTopic)
_ = tcache.Add(&theTopic)
}
return tu, err
}

127
common/topic_cache.go Normal file
View File

@ -0,0 +1,127 @@
package common
import (
"sync"
"sync/atomic"
)
type TopicCache interface {
Get(id int) (*Topic, error)
GetUnsafe(id int) (*Topic, error)
Set(item *Topic) error
Add(item *Topic) error
AddUnsafe(item *Topic) error
Remove(id int) error
RemoveUnsafe(id int) error
Flush()
Length() int
SetCapacity(capacity int)
GetCapacity() int
}
type MemoryTopicCache struct {
items map[int]*Topic
length int64 // sync/atomic only lets us operate on int32s and int64s
capacity int
sync.RWMutex
}
// NewMemoryTopicCache gives you a new instance of MemoryTopicCache
func NewMemoryTopicCache(capacity int) *MemoryTopicCache {
return &MemoryTopicCache{
items: make(map[int]*Topic),
capacity: capacity,
}
}
func (mts *MemoryTopicCache) Get(id int) (*Topic, error) {
mts.RLock()
item, ok := mts.items[id]
mts.RUnlock()
if ok {
return item, nil
}
return item, ErrNoRows
}
func (mts *MemoryTopicCache) GetUnsafe(id int) (*Topic, error) {
item, ok := mts.items[id]
if ok {
return item, nil
}
return item, ErrNoRows
}
func (mts *MemoryTopicCache) Set(item *Topic) error {
mts.Lock()
_, ok := mts.items[item.ID]
if ok {
mts.items[item.ID] = item
} else if int(mts.length) >= mts.capacity {
mts.Unlock()
return ErrStoreCapacityOverflow
} else {
mts.items[item.ID] = item
atomic.AddInt64(&mts.length, 1)
}
mts.Unlock()
return nil
}
func (mts *MemoryTopicCache) Add(item *Topic) error {
if int(mts.length) >= mts.capacity {
return ErrStoreCapacityOverflow
}
mts.Lock()
mts.items[item.ID] = item
mts.Unlock()
atomic.AddInt64(&mts.length, 1)
return nil
}
// TODO: Make these length increments thread-safe. Ditto for the other DataStores
func (mts *MemoryTopicCache) AddUnsafe(item *Topic) error {
if int(mts.length) >= mts.capacity {
return ErrStoreCapacityOverflow
}
mts.items[item.ID] = item
atomic.AddInt64(&mts.length, 1)
return nil
}
// TODO: Make these length decrements thread-safe. Ditto for the other DataStores
func (mts *MemoryTopicCache) Remove(id int) error {
mts.Lock()
delete(mts.items, id)
mts.Unlock()
atomic.AddInt64(&mts.length, -1)
return nil
}
func (mts *MemoryTopicCache) RemoveUnsafe(id int) error {
delete(mts.items, id)
atomic.AddInt64(&mts.length, -1)
return nil
}
func (mts *MemoryTopicCache) Flush() {
mts.Lock()
mts.items = make(map[int]*Topic)
mts.length = 0
mts.Unlock()
}
// ! Is this concurrent?
// Length returns the number of topics in the memory cache
func (mts *MemoryTopicCache) Length() int {
return int(mts.length)
}
func (mts *MemoryTopicCache) SetCapacity(capacity int) {
mts.capacity = capacity
}
func (mts *MemoryTopicCache) GetCapacity() int {
return mts.capacity
}

View File

@ -10,8 +10,6 @@ import (
"database/sql"
"errors"
"strings"
"sync"
"sync/atomic"
"../query_gen/lib"
)
@ -31,44 +29,33 @@ type TopicStore interface {
Exists(id int) bool
Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error)
AddLastTopic(item *Topic, fid int) error // unimplemented
Reload(id int) error // Too much SQL logic to move into TopicCache
// TODO: Implement these two methods
//Replies(tid int) ([]*Reply, error)
//RepliesRange(tid int, lower int, higher int) ([]*Reply, error)
GlobalCount() int
SetCache(cache TopicCache)
GetCache() TopicCache
}
type TopicCache interface {
CacheGet(id int) (*Topic, error)
CacheGetUnsafe(id int) (*Topic, error)
CacheSet(item *Topic) error
CacheAdd(item *Topic) error
CacheAddUnsafe(item *Topic) error
CacheRemove(id int) error
CacheRemoveUnsafe(id int) error
Flush()
Reload(id int) error
Length() int
SetCapacity(capacity int)
GetCapacity() int
}
type DefaultTopicStore struct {
cache TopicCache
type MemoryTopicStore struct {
items map[int]*Topic
length int64 // sync/atomic only lets us operate on int32s and int64s
capacity int
get *sql.Stmt
exists *sql.Stmt
topicCount *sql.Stmt
create *sql.Stmt
sync.RWMutex
}
// NewMemoryTopicStore gives you a new instance of MemoryTopicStore
func NewMemoryTopicStore(capacity int) (*MemoryTopicStore, error) {
// NewDefaultTopicStore gives you a new instance of DefaultTopicStore
func NewDefaultTopicStore(cache TopicCache) (*DefaultTopicStore, error) {
acc := qgen.Builder.Accumulator()
return &MemoryTopicStore{
items: make(map[int]*Topic),
capacity: capacity,
if cache == nil {
cache = NewNullTopicCache()
}
return &DefaultTopicStore{
cache: cache,
get: acc.Select("topics").Columns("title, content, createdBy, createdAt, lastReplyAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, data").Where("tid = ?").Prepare(),
exists: acc.Select("topics").Columns("tid").Where("tid = ?").Prepare(),
topicCount: acc.Count("topics").Prepare(),
@ -76,84 +63,63 @@ func NewMemoryTopicStore(capacity int) (*MemoryTopicStore, error) {
}, acc.FirstError()
}
func (mts *MemoryTopicStore) CacheGet(id int) (*Topic, error) {
mts.RLock()
item, ok := mts.items[id]
mts.RUnlock()
if ok {
return item, nil
}
return item, ErrNoRows
}
func (mts *MemoryTopicStore) CacheGetUnsafe(id int) (*Topic, error) {
item, ok := mts.items[id]
if ok {
return item, nil
}
return item, ErrNoRows
}
func (mts *MemoryTopicStore) DirtyGet(id int) *Topic {
mts.RLock()
topic, ok := mts.items[id]
mts.RUnlock()
if ok {
func (mts *DefaultTopicStore) DirtyGet(id int) *Topic {
topic, err := mts.cache.Get(id)
if err == nil {
return topic
}
topic = &Topic{ID: id}
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
err = mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
if err == nil {
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
_ = mts.CacheAdd(topic)
_ = mts.cache.Add(topic)
return topic
}
return BlankTopic()
}
func (mts *MemoryTopicStore) Get(id int) (*Topic, error) {
mts.RLock()
topic, ok := mts.items[id]
mts.RUnlock()
if ok {
// TODO: Log weird cache errors?
func (mts *DefaultTopicStore) Get(id int) (topic *Topic, err error) {
topic, err = mts.cache.Get(id)
if err == nil {
return topic, nil
}
topic = &Topic{ID: id}
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
err = mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
if err == nil {
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
_ = mts.CacheAdd(topic)
_ = mts.cache.Add(topic)
}
return topic, err
}
// BypassGet will always bypass the cache and pull the topic directly from the database
func (mts *MemoryTopicStore) BypassGet(id int) (*Topic, error) {
func (mts *DefaultTopicStore) BypassGet(id int) (*Topic, error) {
topic := &Topic{ID: id}
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
return topic, err
}
func (mts *MemoryTopicStore) Reload(id int) error {
func (mts *DefaultTopicStore) Reload(id int) error {
topic := &Topic{ID: id}
err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
if err == nil {
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
_ = mts.CacheSet(topic)
_ = mts.cache.Set(topic)
} else {
_ = mts.CacheRemove(id)
_ = mts.cache.Remove(id)
}
return err
}
func (mts *MemoryTopicStore) Exists(id int) bool {
func (mts *DefaultTopicStore) Exists(id int) bool {
return mts.exists.QueryRow(id).Scan(&id) == nil
}
func (mts *MemoryTopicStore) Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error) {
func (mts *DefaultTopicStore) Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error) {
topicName = strings.TrimSpace(topicName)
if topicName == "" {
return 0, ErrNoBody
@ -177,91 +143,17 @@ func (mts *MemoryTopicStore) Create(fid int, topicName string, content string, u
return 0, err
}
return int(lastID), Fstore.AddTopic(int(lastID), uid, fid)
}
func (mts *MemoryTopicStore) CacheSet(item *Topic) error {
mts.Lock()
_, ok := mts.items[item.ID]
if ok {
mts.items[item.ID] = item
} else if int(mts.length) >= mts.capacity {
mts.Unlock()
return ErrStoreCapacityOverflow
} else {
mts.items[item.ID] = item
atomic.AddInt64(&mts.length, 1)
}
mts.Unlock()
return nil
}
func (mts *MemoryTopicStore) CacheAdd(item *Topic) error {
if int(mts.length) >= mts.capacity {
return ErrStoreCapacityOverflow
}
mts.Lock()
mts.items[item.ID] = item
mts.Unlock()
atomic.AddInt64(&mts.length, 1)
return nil
}
// TODO: Make these length increments thread-safe. Ditto for the other DataStores
func (mts *MemoryTopicStore) CacheAddUnsafe(item *Topic) error {
if int(mts.length) >= mts.capacity {
return ErrStoreCapacityOverflow
}
mts.items[item.ID] = item
atomic.AddInt64(&mts.length, 1)
return nil
}
// TODO: Make these length decrements thread-safe. Ditto for the other DataStores
func (mts *MemoryTopicStore) CacheRemove(id int) error {
mts.Lock()
delete(mts.items, id)
mts.Unlock()
atomic.AddInt64(&mts.length, -1)
return nil
}
func (mts *MemoryTopicStore) CacheRemoveUnsafe(id int) error {
delete(mts.items, id)
atomic.AddInt64(&mts.length, -1)
return nil
return int(lastID), Forums.AddTopic(int(lastID), uid, fid)
}
// ? - What is this? Do we need it? Should it be in the main store interface?
func (mts *MemoryTopicStore) AddLastTopic(item *Topic, fid int) error {
func (mts *DefaultTopicStore) AddLastTopic(item *Topic, fid int) error {
// Coming Soon...
return nil
}
func (mts *MemoryTopicStore) Flush() {
mts.Lock()
mts.items = make(map[int]*Topic)
mts.length = 0
mts.Unlock()
}
// ! Is this concurrent?
// Length returns the number of topics in the memory cache
func (mts *MemoryTopicStore) Length() int {
return int(mts.length)
}
func (mts *MemoryTopicStore) SetCapacity(capacity int) {
mts.capacity = capacity
}
func (mts *MemoryTopicStore) GetCapacity() int {
return mts.capacity
}
// GlobalCount returns the total number of topics on these forums
func (mts *MemoryTopicStore) GlobalCount() int {
var tcount int
func (mts *DefaultTopicStore) GlobalCount() (tcount int) {
err := mts.topicCount.QueryRow().Scan(&tcount)
if err != nil {
LogError(err)
@ -269,91 +161,15 @@ func (mts *MemoryTopicStore) GlobalCount() int {
return tcount
}
type SQLTopicStore struct {
get *sql.Stmt
exists *sql.Stmt
topicCount *sql.Stmt
create *sql.Stmt
func (mts *DefaultTopicStore) SetCache(cache TopicCache) {
mts.cache = cache
}
func NewSQLTopicStore() (*SQLTopicStore, error) {
acc := qgen.Builder.Accumulator()
return &SQLTopicStore{
get: acc.Select("topics").Columns("title, content, createdBy, createdAt, lastReplyAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, data").Where("tid = ?").Prepare(),
exists: acc.Select("topics").Columns("tid").Where("tid = ?").Prepare(),
topicCount: acc.Count("topics").Prepare(),
create: acc.Insert("topics").Columns("parentID, title, content, parsed_content, createdAt, lastReplyAt, lastReplyBy, ipaddress, words, createdBy").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,?").Prepare(),
}, acc.FirstError()
}
func (sts *SQLTopicStore) DirtyGet(id int) *Topic {
topic := &Topic{ID: id}
err := sts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
if err != nil {
return BlankTopic()
}
return topic
}
func (sts *SQLTopicStore) Get(id int) (*Topic, error) {
topic := Topic{ID: id}
err := sts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
return &topic, err
}
// BypassGet is an alias of Get(), as we don't have a cache for SQLTopicStore
func (sts *SQLTopicStore) BypassGet(id int) (*Topic, error) {
topic := &Topic{ID: id}
err := sts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.PostCount, &topic.LikeCount, &topic.Data)
topic.Link = BuildTopicURL(NameToSlug(topic.Title), id)
return topic, err
}
func (sts *SQLTopicStore) Exists(id int) bool {
return sts.exists.QueryRow(id).Scan(&id) == nil
}
func (sts *SQLTopicStore) Create(fid int, topicName string, content string, uid int, ipaddress string) (tid int, err error) {
topicName = strings.TrimSpace(topicName)
if topicName == "" {
return 0, ErrNoBody
}
content = strings.TrimSpace(content)
parsedContent := ParseMessage(content, fid, "forums")
if strings.TrimSpace(parsedContent) == "" {
return 0, ErrNoBody
}
wcount := WordCount(content)
// TODO: Move this statement into the topic store
res, err := sts.create.Exec(fid, topicName, content, parsedContent, uid, ipaddress, wcount, uid)
if err != nil {
return 0, err
}
lastID, err := res.LastInsertId()
if err != nil {
return 0, err
}
return int(lastID), Fstore.AddTopic(int(lastID), uid, fid)
}
// ? - What're we going to do about this?
func (sts *SQLTopicStore) AddLastTopic(item *Topic, fid int) error {
// Coming Soon...
// TODO: We're temporarily doing this so that you can do tcache != nil in getTopicUser. Refactor it.
func (mts *DefaultTopicStore) GetCache() TopicCache {
_, ok := mts.cache.(*NullTopicCache)
if ok {
return nil
}
// GlobalCount returns the total number of topics on these forums
func (sts *SQLTopicStore) GlobalCount() int {
var tcount int
err := sts.topicCount.QueryRow().Scan(&tcount)
if err != nil {
LogError(err)
}
return tcount
return mts.cache
}

View File

@ -7,8 +7,6 @@
package common
import (
//"log"
//"fmt"
"database/sql"
"errors"
"strconv"
@ -111,14 +109,15 @@ func (user *User) Init() {
user.Avatar = strings.Replace(Config.Noavatar, "{id}", strconv.Itoa(user.ID), 1)
}
user.Link = BuildProfileURL(NameToSlug(user.Name), user.ID)
user.Tag = Gstore.DirtyGet(user.Group).Tag
user.Tag = Groups.DirtyGet(user.Group).Tag
user.InitPerms()
}
// TODO: Refactor this idiom into something shorter, maybe with a NullUserCache when one isn't set?
func (user *User) CacheRemove() {
ucache, ok := Users.(UserCache)
if ok {
ucache.CacheRemove(user.ID)
ucache := Users.GetCache()
if ucache != nil {
ucache.Remove(user.ID)
}
}
@ -337,7 +336,7 @@ func (user *User) InitPerms() {
user.Group = user.TempGroup
}
group := Gstore.DirtyGet(user.Group)
group := Groups.DirtyGet(user.Group)
if user.IsSuperAdmin {
user.Perms = AllPerms
user.PluginPerms = AllPluginPerms

146
common/user_cache.go Normal file
View File

@ -0,0 +1,146 @@
package common
import (
"sync"
"sync/atomic"
)
type UserCache interface {
Get(id int) (*User, error)
GetUnsafe(id int) (*User, error)
BulkGet(ids []int) (list []*User)
Set(item *User) error
Add(item *User) error
AddUnsafe(item *User) error
Remove(id int) error
RemoveUnsafe(id int) error
Flush()
Length() int
SetCapacity(capacity int)
GetCapacity() int
}
type MemoryUserCache struct {
items map[int]*User
length int64
capacity int
sync.RWMutex
}
// NewMemoryUserCache gives you a new instance of MemoryUserCache
func NewMemoryUserCache(capacity int) *MemoryUserCache {
return &MemoryUserCache{
items: make(map[int]*User),
capacity: capacity,
}
}
func (mus *MemoryUserCache) Get(id int) (*User, error) {
mus.RLock()
item, ok := mus.items[id]
mus.RUnlock()
if ok {
return item, nil
}
return item, ErrNoRows
}
func (mus *MemoryUserCache) BulkGet(ids []int) (list []*User) {
list = make([]*User, len(ids))
mus.RLock()
for i, id := range ids {
list[i] = mus.items[id]
}
mus.RUnlock()
return list
}
func (mus *MemoryUserCache) GetUnsafe(id int) (*User, error) {
item, ok := mus.items[id]
if ok {
return item, nil
}
return item, ErrNoRows
}
func (mus *MemoryUserCache) Set(item *User) error {
mus.Lock()
user, ok := mus.items[item.ID]
if ok {
mus.Unlock()
*user = *item
} else if int(mus.length) >= mus.capacity {
mus.Unlock()
return ErrStoreCapacityOverflow
} else {
mus.items[item.ID] = item
mus.Unlock()
atomic.AddInt64(&mus.length, 1)
}
return nil
}
func (mus *MemoryUserCache) Add(item *User) error {
if int(mus.length) >= mus.capacity {
return ErrStoreCapacityOverflow
}
mus.Lock()
mus.items[item.ID] = item
mus.length = int64(len(mus.items))
mus.Unlock()
return nil
}
func (mus *MemoryUserCache) AddUnsafe(item *User) error {
if int(mus.length) >= mus.capacity {
return ErrStoreCapacityOverflow
}
mus.items[item.ID] = item
mus.length = int64(len(mus.items))
return nil
}
func (mus *MemoryUserCache) Remove(id int) error {
mus.Lock()
_, ok := mus.items[id]
if !ok {
mus.Unlock()
return ErrNoRows
}
delete(mus.items, id)
mus.Unlock()
atomic.AddInt64(&mus.length, -1)
return nil
}
func (mus *MemoryUserCache) RemoveUnsafe(id int) error {
_, ok := mus.items[id]
if !ok {
return ErrNoRows
}
delete(mus.items, id)
atomic.AddInt64(&mus.length, -1)
return nil
}
func (mus *MemoryUserCache) Flush() {
mus.Lock()
mus.items = make(map[int]*User)
mus.length = 0
mus.Unlock()
}
// ! Is this concurrent?
// Length returns the number of users in the memory cache
func (mus *MemoryUserCache) Length() int {
return int(mus.length)
}
func (mus *MemoryUserCache) SetCapacity(capacity int) {
mus.capacity = capacity
}
func (mus *MemoryUserCache) GetCapacity() int {
return mus.capacity
}

View File

@ -5,8 +5,6 @@ import (
"errors"
"log"
"strconv"
"sync"
"sync/atomic"
"../query_gen/lib"
"golang.org/x/crypto/bcrypt"
@ -25,43 +23,32 @@ type UserStore interface {
BulkGetMap(ids []int) (map[int]*User, error)
BypassGet(id int) (*User, error)
Create(username string, password string, email string, group int, active bool) (int, error)
GlobalCount() int
}
type UserCache interface {
CacheGet(id int) (*User, error)
CacheGetUnsafe(id int) (*User, error)
CacheSet(item *User) error
CacheAdd(item *User) error
CacheAddUnsafe(item *User) error
CacheRemove(id int) error
CacheRemoveUnsafe(id int) error
Flush()
Reload(id int) error
Length() int
SetCapacity(capacity int)
GetCapacity() int
GlobalCount() int
SetCache(cache UserCache)
GetCache() UserCache
}
type MemoryUserStore struct {
items map[int]*User
length int64
capacity int
type DefaultUserStore struct {
cache UserCache
get *sql.Stmt
exists *sql.Stmt
register *sql.Stmt
usernameExists *sql.Stmt
userCount *sql.Stmt
sync.RWMutex
}
// NewMemoryUserStore gives you a new instance of MemoryUserStore
func NewMemoryUserStore(capacity int) (*MemoryUserStore, error) {
// NewDefaultUserStore gives you a new instance of DefaultUserStore
func NewDefaultUserStore(cache UserCache) (*DefaultUserStore, error) {
acc := qgen.Builder.Accumulator()
if cache == nil {
cache = NewNullUserCache()
}
// TODO: Add an admin version of registerStmt with more flexibility?
return &MemoryUserStore{
items: make(map[int]*User),
capacity: capacity,
return &DefaultUserStore{
cache: cache,
get: acc.SimpleSelect("users", "name, group, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, last_ip, temp_group", "uid = ?", "", ""),
exists: acc.SimpleSelect("users", "uid", "uid = ?", "", ""),
register: acc.SimpleInsert("users", "name, email, password, salt, group, is_super_admin, session, active, message, createdAt, lastActiveAt", "?,?,?,?,?,0,'',?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP()"),
@ -70,75 +57,43 @@ func NewMemoryUserStore(capacity int) (*MemoryUserStore, error) {
}, acc.FirstError()
}
func (mus *MemoryUserStore) CacheGet(id int) (*User, error) {
mus.RLock()
item, ok := mus.items[id]
mus.RUnlock()
if ok {
return item, nil
}
return item, ErrNoRows
}
func (mus *MemoryUserStore) CacheGetUnsafe(id int) (*User, error) {
item, ok := mus.items[id]
if ok {
return item, nil
}
return item, ErrNoRows
}
func (mus *MemoryUserStore) DirtyGet(id int) *User {
mus.RLock()
user, ok := mus.items[id]
mus.RUnlock()
if ok {
func (mus *DefaultUserStore) DirtyGet(id int) *User {
user, err := mus.cache.Get(id)
if err == nil {
return user
}
user = &User{ID: id, Loggedin: true}
err := mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup)
err = mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup)
user.Init()
if err == nil {
mus.CacheSet(user)
mus.cache.Set(user)
return user
}
return BlankUser()
}
func (mus *MemoryUserStore) Get(id int) (*User, error) {
mus.RLock()
user, ok := mus.items[id]
mus.RUnlock()
if ok {
// TODO: Log weird cache errors? Not just here but in every *Cache?
func (mus *DefaultUserStore) Get(id int) (*User, error) {
user, err := mus.cache.Get(id)
if err == nil {
return user, nil
}
user = &User{ID: id, Loggedin: true}
err := mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup)
err = mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup)
user.Init()
if err == nil {
mus.CacheSet(user)
mus.cache.Set(user)
}
return user, err
}
// WARNING: We did a little hack to make this as thin and quick as possible to reduce lock contention, use the * Cascade* methods instead for normal use
func (mus *MemoryUserStore) bulkGet(ids []int) (list []*User) {
list = make([]*User, len(ids))
mus.RLock()
for i, id := range ids {
list[i] = mus.items[id]
}
mus.RUnlock()
return list
}
// TODO: Optimise the query to avoid preparing it on the spot? Maybe, use knowledge of the most common IN() parameter counts?
// TODO: ID of 0 should always error?
func (mus *MemoryUserStore) BulkGetMap(ids []int) (list map[int]*User, err error) {
func (mus *DefaultUserStore) BulkGetMap(ids []int) (list map[int]*User, err error) {
var idCount = len(ids)
list = make(map[int]*User)
if idCount == 0 {
@ -146,7 +101,7 @@ func (mus *MemoryUserStore) BulkGetMap(ids []int) (list map[int]*User, err error
}
var stillHere []int
sliceList := mus.bulkGet(ids)
sliceList := mus.cache.BulkGet(ids)
for i, sliceItem := range sliceList {
if sliceItem != nil {
list[sliceItem.ID] = sliceItem
@ -184,7 +139,7 @@ func (mus *MemoryUserStore) BulkGetMap(ids []int) (list map[int]*User, err error
}
user.Init()
mus.CacheSet(user)
mus.cache.Set(user)
list[user.ID] = user
}
@ -216,7 +171,7 @@ func (mus *MemoryUserStore) BulkGetMap(ids []int) (list map[int]*User, err error
return list, err
}
func (mus *MemoryUserStore) BypassGet(id int) (*User, error) {
func (mus *DefaultUserStore) BypassGet(id int) (*User, error) {
user := &User{ID: id, Loggedin: true}
err := mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup)
@ -224,20 +179,20 @@ func (mus *MemoryUserStore) BypassGet(id int) (*User, error) {
return user, err
}
func (mus *MemoryUserStore) Reload(id int) error {
func (mus *DefaultUserStore) Reload(id int) error {
user := &User{ID: id, Loggedin: true}
err := mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup)
if err != nil {
mus.CacheRemove(id)
mus.cache.Remove(id)
return err
}
user.Init()
_ = mus.CacheSet(user)
_ = mus.cache.Set(user)
return nil
}
func (mus *MemoryUserStore) Exists(id int) bool {
func (mus *DefaultUserStore) Exists(id int) bool {
err := mus.exists.QueryRow(id).Scan(&id)
if err != nil && err != ErrNoRows {
LogError(err)
@ -245,210 +200,8 @@ func (mus *MemoryUserStore) Exists(id int) bool {
return err != ErrNoRows
}
func (mus *MemoryUserStore) CacheSet(item *User) error {
mus.Lock()
user, ok := mus.items[item.ID]
if ok {
mus.Unlock()
*user = *item
} else if int(mus.length) >= mus.capacity {
mus.Unlock()
return ErrStoreCapacityOverflow
} else {
mus.items[item.ID] = item
mus.Unlock()
atomic.AddInt64(&mus.length, 1)
}
return nil
}
func (mus *MemoryUserStore) CacheAdd(item *User) error {
if int(mus.length) >= mus.capacity {
return ErrStoreCapacityOverflow
}
mus.Lock()
mus.items[item.ID] = item
mus.length = int64(len(mus.items))
mus.Unlock()
return nil
}
func (mus *MemoryUserStore) CacheAddUnsafe(item *User) error {
if int(mus.length) >= mus.capacity {
return ErrStoreCapacityOverflow
}
mus.items[item.ID] = item
mus.length = int64(len(mus.items))
return nil
}
func (mus *MemoryUserStore) CacheRemove(id int) error {
mus.Lock()
_, ok := mus.items[id]
if !ok {
mus.Unlock()
return ErrNoRows
}
delete(mus.items, id)
mus.Unlock()
atomic.AddInt64(&mus.length, -1)
return nil
}
func (mus *MemoryUserStore) CacheRemoveUnsafe(id int) error {
_, ok := mus.items[id]
if !ok {
return ErrNoRows
}
delete(mus.items, id)
atomic.AddInt64(&mus.length, -1)
return nil
}
// TODO: Change active to a bool?
func (mus *MemoryUserStore) Create(username string, password string, email string, group int, active bool) (int, error) {
// Is this username already taken..?
err := mus.usernameExists.QueryRow(username).Scan(&username)
if err != ErrNoRows {
return 0, ErrAccountExists
}
salt, err := GenerateSafeString(SaltLength)
if err != nil {
return 0, err
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password+salt), bcrypt.DefaultCost)
if err != nil {
return 0, err
}
res, err := mus.register.Exec(username, email, string(hashedPassword), salt, group, active)
if err != nil {
return 0, err
}
lastID, err := res.LastInsertId()
return int(lastID), err
}
func (mus *MemoryUserStore) Flush() {
mus.Lock()
mus.items = make(map[int]*User)
mus.length = 0
mus.Unlock()
}
// ! Is this concurrent?
// Length returns the number of users in the memory cache
func (mus *MemoryUserStore) Length() int {
return int(mus.length)
}
func (mus *MemoryUserStore) SetCapacity(capacity int) {
mus.capacity = capacity
}
func (mus *MemoryUserStore) GetCapacity() int {
return mus.capacity
}
// GlobalCount returns the total number of users registered on the forums
func (mus *MemoryUserStore) GlobalCount() (ucount int) {
err := mus.userCount.QueryRow().Scan(&ucount)
if err != nil {
LogError(err)
}
return ucount
}
type SQLUserStore struct {
get *sql.Stmt
exists *sql.Stmt
register *sql.Stmt
usernameExists *sql.Stmt
userCount *sql.Stmt
}
func NewSQLUserStore() (*SQLUserStore, error) {
acc := qgen.Builder.Accumulator()
// TODO: Add an admin version of registerStmt with more flexibility?
return &SQLUserStore{
get: acc.SimpleSelect("users", "name, group, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, last_ip, temp_group", "uid = ?", "", ""),
exists: acc.SimpleSelect("users", "uid", "uid = ?", "", ""),
register: acc.SimpleInsert("users", "name, email, password, salt, group, is_super_admin, session, active, message, createdAt, lastActiveAt", "?,?,?,?,?,0,'',?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP()"),
usernameExists: acc.SimpleSelect("users", "name", "name = ?", "", ""),
userCount: acc.SimpleCount("users", "", ""),
}, acc.FirstError()
}
func (mus *SQLUserStore) DirtyGet(id int) *User {
user := &User{ID: id, Loggedin: true}
err := mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup)
user.Init()
if err != nil {
return BlankUser()
}
return user
}
func (mus *SQLUserStore) Get(id int) (*User, error) {
user := &User{ID: id, Loggedin: true}
err := mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup)
user.Init()
return user, err
}
// TODO: Optimise the query to avoid preparing it on the spot? Maybe, use knowledge of the most common IN() parameter counts?
func (mus *SQLUserStore) BulkGetMap(ids []int) (list map[int]*User, err error) {
var qlist string
var uidList []interface{}
for _, id := range ids {
uidList = append(uidList, strconv.Itoa(id))
qlist += "?,"
}
qlist = qlist[0 : len(qlist)-1]
acc := qgen.Builder.Accumulator()
rows, err := acc.Select("users").Columns("uid, name, group, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, last_ip, temp_group").Where("uid IN(" + qlist + ")").Query(uidList...)
if err != nil {
return nil, err
}
list = make(map[int]*User)
for rows.Next() {
user := &User{Loggedin: true}
err := rows.Scan(&user.ID, &user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup)
if err != nil {
return nil, err
}
user.Init()
list[user.ID] = user
}
return list, nil
}
func (mus *SQLUserStore) BypassGet(id int) (*User, error) {
user := &User{ID: id, Loggedin: true}
err := mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup)
user.Init()
return user, err
}
func (mus *SQLUserStore) Exists(id int) bool {
err := mus.exists.QueryRow(id).Scan(&id)
if err != nil && err != ErrNoRows {
LogError(err)
}
return err != ErrNoRows
}
func (mus *SQLUserStore) Create(username string, password string, email string, group int, active bool) (int, error) {
func (mus *DefaultUserStore) Create(username string, password string, email string, group int, active bool) (int, error) {
// Is this username already taken..?
err := mus.usernameExists.QueryRow(username).Scan(&username)
if err != ErrNoRows {
@ -475,7 +228,7 @@ func (mus *SQLUserStore) Create(username string, password string, email string,
}
// GlobalCount returns the total number of users registered on the forums
func (mus *SQLUserStore) GlobalCount() (ucount int) {
func (mus *DefaultUserStore) GlobalCount() (ucount int) {
err := mus.userCount.QueryRow().Scan(&ucount)
if err != nil {
LogError(err)
@ -483,54 +236,15 @@ func (mus *SQLUserStore) GlobalCount() (ucount int) {
return ucount
}
// TODO: MockUserStore
// NullUserStore is here for tests because Go doesn't have short-circuiting
type NullUserStore struct {
func (mus *DefaultUserStore) SetCache(cache UserCache) {
mus.cache = cache
}
func (nus *NullUserStore) CacheGet(_ int) (*User, error) {
return nil, ErrNoRows
}
func (nus *NullUserStore) CacheGetUnsafe(_ int) (*User, error) {
return nil, ErrNoRows
}
func (nus *NullUserStore) CacheSet(_ *User) error {
return ErrStoreCapacityOverflow
}
func (nus *NullUserStore) CacheAdd(_ *User) error {
return ErrStoreCapacityOverflow
}
func (nus *NullUserStore) CacheAddUnsafe(_ *User) error {
return ErrStoreCapacityOverflow
}
func (nus *NullUserStore) CacheRemove(_ int) error {
return ErrNoRows
}
func (nus *NullUserStore) CacheRemoveUnsafe(_ int) error {
return ErrNoRows
}
func (nus *NullUserStore) Flush() {
}
func (nus *NullUserStore) Reload(_ int) error {
return ErrNoRows
}
func (nus *NullUserStore) Length() int {
return 0
}
func (nus *NullUserStore) SetCapacity(_ int) {
}
func (nus *NullUserStore) GetCapacity() int {
return 0
// TODO: We're temporarily doing this so that you can do ucache != nil in getTopicUser. Refactor it.
func (mus *DefaultUserStore) GetCache() UserCache {
_, ok := mus.cache.(*NullUserCache)
if ok {
return nil
}
return mus.cache
}

View File

@ -33,26 +33,37 @@ func init() {
}
func LoadWordFilters() error {
rows, err := filterStmts.getWordFilters.Query()
var wordFilters = WordFilterMap(make(map[int]WordFilter))
filters, err := wordFilters.BypassGetAll()
if err != nil {
return err
}
for _, filter := range filters {
wordFilters[filter.ID] = filter
}
WordFilterBox.Store(wordFilters)
return nil
}
// TODO: Return pointers to word filters intead to save memory?
func (wBox WordFilterMap) BypassGetAll() (filters []WordFilter, err error) {
rows, err := filterStmts.getWordFilters.Query()
if err != nil {
return nil, err
}
defer rows.Close()
var wordFilters = WordFilterMap(make(map[int]WordFilter))
var wfid int
var find string
var replacement string
for rows.Next() {
err := rows.Scan(&wfid, &find, &replacement)
filter := WordFilter{ID: 0}
err := rows.Scan(&filter.ID, &filter.Find, &filter.Replacement)
if err != nil {
return err
return filters, err
}
wordFilters[wfid] = WordFilter{ID: wfid, Find: find, Replacement: replacement}
filters = append(filters, filter)
}
WordFilterBox.Store(wordFilters)
return rows.Err()
return filters, rows.Err()
}
func AddWordFilter(id int, find string, replacement string) {

View File

@ -35,47 +35,50 @@ func InitDatabase() (err error) {
}
log.Print("Loading the usergroups.")
common.Gstore, err = common.NewMemoryGroupStore()
common.Groups, err = common.NewMemoryGroupStore()
if err != nil {
return err
}
err2 := common.Gstore.LoadGroups()
err2 := common.Groups.LoadGroups()
if err2 != nil {
return err2
}
// We have to put this here, otherwise LoadForums() won't be able to get the last poster data when building it's forums
log.Print("Initialising the user and topic stores")
var ucache common.UserCache
var tcache common.TopicCache
if common.Config.CacheTopicUser == common.CACHE_STATIC {
common.Users, err = common.NewMemoryUserStore(common.Config.UserCacheCapacity)
common.Topics, err2 = common.NewMemoryTopicStore(common.Config.TopicCacheCapacity)
} else {
common.Users, err = common.NewSQLUserStore()
common.Topics, err2 = common.NewSQLTopicStore()
ucache = common.NewMemoryUserCache(common.Config.UserCacheCapacity)
tcache = common.NewMemoryTopicCache(common.Config.TopicCacheCapacity)
}
common.Users, err = common.NewDefaultUserStore(ucache)
if err != nil {
return err
}
if err2 != nil {
common.Topics, err = common.NewDefaultTopicStore(tcache)
if err != nil {
return err2
}
log.Print("Loading the forums.")
common.Fstore, err = common.NewMemoryForumStore()
common.Forums, err = common.NewMemoryForumStore()
if err != nil {
return err
}
err = common.Fstore.LoadForums()
err = common.Forums.LoadForums()
if err != nil {
return err
}
log.Print("Loading the forum permissions.")
common.Fpstore, err = common.NewMemoryForumPermsStore()
common.FPStore, err = common.NewMemoryForumPermsStore()
if err != nil {
return err
}
err = common.Fpstore.Init()
err = common.FPStore.Init()
if err != nil {
return err
}

View File

@ -0,0 +1,44 @@
package guilds
import "database/sql"
import "../../../query_gen/lib"
var Gstore GuildStore
type GuildStore interface {
Get(guildID int) (guild *Guild, err error)
Create(name string, desc string, active bool, privacy int, uid int, fid int) (int, error)
}
type SQLGuildStore struct {
get *sql.Stmt
create *sql.Stmt
}
func NewSQLGuildStore() (*SQLGuildStore, error) {
acc := qgen.Builder.Accumulator()
return &SQLGuildStore{
get: acc.Select("guilds").Columns("name, desc, active, privacy, joinable, owner, memberCount, mainForum, backdrop, createdAt, lastUpdateTime").Where("guildID = ?").Prepare(),
create: acc.Insert("guilds").Columns("name, desc, active, privacy, joinable, owner, memberCount, mainForum, backdrop, createdAt, lastUpdateTime").Fields("?,?,?,?,1,?,1,?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP()").Prepare(),
}, acc.FirstError()
}
func (store *SQLGuildStore) Close() {
_ = store.get.Close()
_ = store.create.Close()
}
func (store *SQLGuildStore) Get(guildID int) (guild *Guild, err error) {
guild = &Guild{ID: guildID}
err = store.get.QueryRow(guildID).Scan(&guild.Name, &guild.Desc, &guild.Active, &guild.Privacy, &guild.Joinable, &guild.Owner, &guild.MemberCount, &guild.MainForumID, &guild.Backdrop, &guild.CreatedAt, &guild.LastUpdateTime)
return guild, err
}
func (store *SQLGuildStore) Create(name string, desc string, active bool, privacy int, uid int, fid int) (int, error) {
res, err := store.create.Exec(name, desc, active, privacy, uid, fid)
if err != nil {
return 0, err
}
lastID, err := res.LastInsertId()
return int(lastID), err
}

View File

@ -21,8 +21,6 @@ var ListStmt *sql.Stmt
var MemberListStmt *sql.Stmt
var MemberListJoinStmt *sql.Stmt
var GetMemberStmt *sql.Stmt
var GetGuildStmt *sql.Stmt
var CreateGuildStmt *sql.Stmt
var AttachForumStmt *sql.Stmt
var UnattachForumStmt *sql.Stmt
var AddMemberStmt *sql.Stmt
@ -105,8 +103,8 @@ func PrebuildTmplList(user common.User, headerVars *common.HeaderVars) common.CT
CreatedAt: "date",
LastUpdateTime: "date",
MainForumID: 1,
MainForum: common.Fstore.DirtyGet(1),
Forums: []*common.Forum{common.Fstore.DirtyGet(1)},
MainForum: common.Forums.DirtyGet(1),
Forums: []*common.Forum{common.Forums.DirtyGet(1)},
},
}
listPage := ListPage{"Guild List", user, headerVars, guildList}
@ -127,9 +125,9 @@ func CommonAreaWidgets(headerVars *common.HeaderVars) {
return
}
if common.Themes[headerVars.ThemeName].Sidebars == "left" {
if common.Themes[headerVars.Theme.Name].Sidebars == "left" {
headerVars.Widgets.LeftSidebar = template.HTML(string(b.Bytes()))
} else if common.Themes[headerVars.ThemeName].Sidebars == "right" || common.Themes[headerVars.ThemeName].Sidebars == "both" {
} else if common.Themes[headerVars.Theme.Name].Sidebars == "right" || common.Themes[headerVars.Theme.Name].Sidebars == "both" {
headerVars.Widgets.RightSidebar = template.HTML(string(b.Bytes()))
}
}
@ -151,9 +149,9 @@ func GuildWidgets(headerVars *common.HeaderVars, guildItem *Guild) (success bool
return false
}
if themes[headerVars.ThemeName].Sidebars == "left" {
if themes[headerVars.Theme.Name].Sidebars == "left" {
headerVars.Widgets.LeftSidebar = template.HTML(string(b.Bytes()))
} else if themes[headerVars.ThemeName].Sidebars == "right" || themes[headerVars.ThemeName].Sidebars == "both" {
} else if themes[headerVars.Theme.Name].Sidebars == "right" || themes[headerVars.Theme.Name].Sidebars == "both" {
headerVars.Widgets.RightSidebar = template.HTML(string(b.Bytes()))
} else {
return false
@ -194,19 +192,13 @@ func RouteGuildList(w http.ResponseWriter, r *http.Request, user common.User) co
}
pi := ListPage{"Guild List", user, headerVars, guildList}
err = common.RunThemeTemplate(headerVars.ThemeName, "guilds_guild_list", pi, w)
err = common.RunThemeTemplate(headerVars.Theme.Name, "guilds_guild_list", pi, w)
if err != nil {
return common.InternalError(err, w, r)
}
return nil
}
func GetGuild(guildID int) (guildItem *Guild, err error) {
guildItem = &Guild{ID: guildID}
err = GetGuildStmt.QueryRow(guildID).Scan(&guildItem.Name, &guildItem.Desc, &guildItem.Active, &guildItem.Privacy, &guildItem.Joinable, &guildItem.Owner, &guildItem.MemberCount, &guildItem.MainForumID, &guildItem.Backdrop, &guildItem.CreatedAt, &guildItem.LastUpdateTime)
return guildItem, err
}
func MiddleViewGuild(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
// SEO URLs...
halves := strings.Split(r.URL.Path[len("/guild/"):], ".")
@ -218,7 +210,7 @@ func MiddleViewGuild(w http.ResponseWriter, r *http.Request, user common.User) c
return common.PreError("Not a valid guild ID", w, r)
}
guildItem, err := GetGuild(guildID)
guildItem, err := Gstore.Get(guildID)
if err != nil {
return common.LocalError("Bad guild", w, r, user)
}
@ -277,32 +269,28 @@ func RouteCreateGuildSubmit(w http.ResponseWriter, r *http.Request, user common.
}
// Create the backing forum
fid, err := common.Fstore.Create(guildName, "", true, "")
fid, err := common.Forums.Create(guildName, "", true, "")
if err != nil {
return common.InternalError(err, w, r)
}
res, err := CreateGuildStmt.Exec(guildName, guildDesc, guildActive, guildPrivacy, user.ID, fid)
if err != nil {
return common.InternalError(err, w, r)
}
lastID, err := res.LastInsertId()
gid, err := Gstore.Create(guildName, guildDesc, guildActive, guildPrivacy, user.ID, fid)
if err != nil {
return common.InternalError(err, w, r)
}
// Add the main backing forum to the forum list
err = AttachForum(int(lastID), fid)
err = AttachForum(gid, fid)
if err != nil {
return common.InternalError(err, w, r)
}
_, err = AddMemberStmt.Exec(lastID, user.ID, 2)
_, err = AddMemberStmt.Exec(gid, user.ID, 2)
if err != nil {
return common.InternalError(err, w, r)
}
http.Redirect(w, r, BuildGuildURL(common.NameToSlug(guildName), int(lastID)), http.StatusSeeOther)
http.Redirect(w, r, BuildGuildURL(common.NameToSlug(guildName), gid), http.StatusSeeOther)
return nil
}
@ -322,9 +310,7 @@ func RouteMemberList(w http.ResponseWriter, r *http.Request, user common.User) c
return common.PreError("Not a valid group ID", w, r)
}
var guildItem = &Guild{ID: guildID}
var mainForum int // Unused
err = GetGuildStmt.QueryRow(guildID).Scan(&guildItem.Name, &guildItem.Desc, &guildItem.Active, &guildItem.Privacy, &guildItem.Joinable, &guildItem.Owner, &guildItem.MemberCount, &mainForum, &guildItem.Backdrop, &guildItem.CreatedAt, &guildItem.LastUpdateTime)
guildItem, err := Gstore.Get(guildID)
if err != nil {
return common.LocalError("Bad group", w, r, user)
}
@ -380,7 +366,7 @@ func RouteMemberList(w http.ResponseWriter, r *http.Request, user common.User) c
return nil
}
}
err = common.RunThemeTemplate(headerVars.ThemeName, "guilds_member_list", pi, w)
err = common.RunThemeTemplate(headerVars.Theme.Name, "guilds_member_list", pi, w)
if err != nil {
return common.InternalError(err, w, r)
}
@ -439,7 +425,7 @@ func TrowAssign(args ...interface{}) interface{} {
// TODO: It would be nice, if you could select one of the boards in the group from that drop-down rather than just the one you got linked from
func TopicCreatePreLoop(args ...interface{}) interface{} {
var fid = args[2].(int)
if common.Fstore.DirtyGet(fid).ParentType == "guild" {
if common.Forums.DirtyGet(fid).ParentType == "guild" {
var strictmode = args[5].(*bool)
*strictmode = true
}
@ -452,14 +438,14 @@ func TopicCreatePreLoop(args ...interface{}) interface{} {
func ForumCheck(args ...interface{}) (skip bool, rerr common.RouteError) {
var r = args[1].(*http.Request)
var fid = args[3].(*int)
var forum = common.Fstore.DirtyGet(*fid)
var forum = common.Forums.DirtyGet(*fid)
if forum.ParentType == "guild" {
var err error
var w = args[0].(http.ResponseWriter)
guildItem, ok := r.Context().Value("guilds_current_group").(*Guild)
if !ok {
guildItem, err = GetGuild(forum.ParentID)
guildItem, err = Gstore.Get(forum.ParentID)
if err != nil {
return true, common.InternalError(errors.New("Unable to find the parent group for a forum"), w, r)
}

View File

@ -10,7 +10,6 @@ import "./common"
// nolint
type Stmts struct {
getPassword *sql.Stmt
getSettings *sql.Stmt
isPluginActive *sql.Stmt
getUsersOffset *sql.Stmt
isThemeDefault *sql.Stmt
@ -45,7 +44,6 @@ type Stmts struct {
createWordFilter *sql.Stmt
editReply *sql.Stmt
editProfileReply *sql.Stmt
updateSetting *sql.Stmt
updatePlugin *sql.Stmt
updatePluginInstall *sql.Stmt
updateTheme *sql.Stmt
@ -61,7 +59,6 @@ type Stmts struct {
deleteActivityStreamMatch *sql.Stmt
deleteWordFilter *sql.Stmt
reportExists *sql.Stmt
modlogCount *sql.Stmt
notifyWatchers *sql.Stmt
getActivityFeedByWatcher *sql.Stmt
@ -90,13 +87,6 @@ func _gen_mssql() (err error) {
return err
}
log.Print("Preparing getSettings statement.")
stmts.getSettings, err = db.Prepare("SELECT [name],[content],[type] FROM [settings]")
if err != nil {
log.Print("Bad Query: ","SELECT [name],[content],[type] FROM [settings]")
return err
}
log.Print("Preparing isPluginActive statement.")
stmts.isPluginActive, err = db.Prepare("SELECT [active] FROM [plugins] WHERE [uname] = ?1")
if err != nil {
@ -335,13 +325,6 @@ func _gen_mssql() (err error) {
return err
}
log.Print("Preparing updateSetting statement.")
stmts.updateSetting, err = db.Prepare("UPDATE [settings] SET [content] = ? WHERE [name] = ?")
if err != nil {
log.Print("Bad Query: ","UPDATE [settings] SET [content] = ? WHERE [name] = ?")
return err
}
log.Print("Preparing updatePlugin statement.")
stmts.updatePlugin, err = db.Prepare("UPDATE [plugins] SET [active] = ? WHERE [uname] = ?")
if err != nil {
@ -447,13 +430,6 @@ func _gen_mssql() (err error) {
return err
}
log.Print("Preparing modlogCount statement.")
stmts.modlogCount, err = db.Prepare("SELECT COUNT(*) AS [count] FROM [moderation_logs]")
if err != nil {
log.Print("Bad Query: ","SELECT COUNT(*) AS [count] FROM [moderation_logs]")
return err
}
log.Print("Preparing notifyWatchers statement.")
stmts.notifyWatchers, err = db.Prepare("INSERT INTO [activity_stream_matches] ([watcher],[asid]) SELECT [activity_subscriptions].[user],[activity_stream].[asid] FROM [activity_stream] INNER JOIN [activity_subscriptions] ON [activity_subscriptions].[targetType] = [activity_stream].[elementType] AND [activity_subscriptions].[targetID] = [activity_stream].[elementID] AND [activity_subscriptions].[user] != [activity_stream].[actor] WHERE [asid] = ?1")
if err != nil {

View File

@ -12,7 +12,6 @@ import "./common"
// nolint
type Stmts struct {
getPassword *sql.Stmt
getSettings *sql.Stmt
isPluginActive *sql.Stmt
getUsersOffset *sql.Stmt
isThemeDefault *sql.Stmt
@ -47,7 +46,6 @@ type Stmts struct {
createWordFilter *sql.Stmt
editReply *sql.Stmt
editProfileReply *sql.Stmt
updateSetting *sql.Stmt
updatePlugin *sql.Stmt
updatePluginInstall *sql.Stmt
updateTheme *sql.Stmt
@ -63,7 +61,6 @@ type Stmts struct {
deleteActivityStreamMatch *sql.Stmt
deleteWordFilter *sql.Stmt
reportExists *sql.Stmt
modlogCount *sql.Stmt
notifyWatchers *sql.Stmt
getActivityFeedByWatcher *sql.Stmt
@ -91,12 +88,6 @@ func _gen_mysql() (err error) {
return err
}
log.Print("Preparing getSettings statement.")
stmts.getSettings, err = db.Prepare("SELECT `name`,`content`,`type` FROM `settings`")
if err != nil {
return err
}
log.Print("Preparing isPluginActive statement.")
stmts.isPluginActive, err = db.Prepare("SELECT `active` FROM `plugins` WHERE `uname` = ?")
if err != nil {
@ -301,12 +292,6 @@ func _gen_mysql() (err error) {
return err
}
log.Print("Preparing updateSetting statement.")
stmts.updateSetting, err = db.Prepare("UPDATE `settings` SET `content` = ? WHERE `name` = ?")
if err != nil {
return err
}
log.Print("Preparing updatePlugin statement.")
stmts.updatePlugin, err = db.Prepare("UPDATE `plugins` SET `active` = ? WHERE `uname` = ?")
if err != nil {
@ -397,12 +382,6 @@ func _gen_mysql() (err error) {
return err
}
log.Print("Preparing modlogCount statement.")
stmts.modlogCount, err = db.Prepare("SELECT COUNT(*) AS `count` FROM `moderation_logs`")
if err != nil {
return err
}
log.Print("Preparing notifyWatchers statement.")
stmts.notifyWatchers, err = db.Prepare("INSERT INTO `activity_stream_matches`(`watcher`,`asid`) SELECT `activity_subscriptions`.`user`, `activity_stream`.`asid` FROM `activity_stream` INNER JOIN `activity_subscriptions` ON `activity_subscriptions`.`targetType` = `activity_stream`.`elementType` AND `activity_subscriptions`.`targetID` = `activity_stream`.`elementID` AND `activity_subscriptions`.`user` != `activity_stream`.`actor` WHERE `asid` = ?")
if err != nil {

View File

@ -11,7 +11,6 @@ import "./common"
type Stmts struct {
editReply *sql.Stmt
editProfileReply *sql.Stmt
updateSetting *sql.Stmt
updatePlugin *sql.Stmt
updatePluginInstall *sql.Stmt
updateTheme *sql.Stmt
@ -55,12 +54,6 @@ func _gen_pgsql() (err error) {
return err
}
log.Print("Preparing updateSetting statement.")
stmts.updateSetting, err = db.Prepare("UPDATE `settings` SET `content` = ? WHERE `name` = ?")
if err != nil {
return err
}
log.Print("Preparing updatePlugin statement.")
stmts.updatePlugin, err = db.Prepare("UPDATE `plugins` SET `active` = ? WHERE `uname` = ?")
if err != nil {

View File

@ -246,7 +246,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
case "/panel/settings/":
err = routePanelSettings(w,req,user)
case "/panel/settings/edit/":
err = routePanelSetting(w,req,user,extra_data)
err = routePanelSettingEdit(w,req,user,extra_data)
case "/panel/settings/edit/submit/":
err = common.NoSessionMismatch(w,req,user)
if err != nil {
@ -254,7 +254,7 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
err = routePanelSettingEdit(w,req,user,extra_data)
err = routePanelSettingEditSubmit(w,req,user,extra_data)
case "/panel/settings/word-filters/":
err = routePanelWordFilters(w,req,user)
case "/panel/settings/word-filters/create/":

View File

@ -15,7 +15,6 @@ import (
"./common"
"./install/install"
"./query_gen/lib"
//"runtime/pprof"
//"github.com/husobee/vestigo"
)
@ -77,44 +76,7 @@ func gloinit() (err error) {
return err
}
common.Rstore, err = common.NewSQLReplyStore()
if err != nil {
return err
}
common.Prstore, err = common.NewSQLProfileReplyStore()
if err != nil {
return err
}
dbProd = db
//db_test, err = sql.Open("testdb","")
//if err != nil {
// return err
//}
err = common.InitTemplates()
if err != nil {
return err
}
dbProd.SetMaxOpenConns(64)
err = common.InitPhrases()
if err != nil {
log.Fatal(err)
}
log.Print("Loading the static files.")
err = common.StaticFiles.Init()
if err != nil {
return err
}
common.Auth, err = common.NewDefaultAuth()
if err != nil {
return err
}
err = common.LoadWordFilters()
err = afterDBInit()
if err != nil {
return err
}

View File

@ -1,7 +1,6 @@
/*
*
* Gosora MSSQL Interface
* Under heavy development
* Copyright Azareal 2017 - 2018
*
*/
@ -80,14 +79,9 @@ func (ins *MssqlInstaller) InitDatabase() (err error) {
// TODO: Create the database, if it doesn't exist
// Ready the query builder
qgen.Builder.SetConn(db)
err = qgen.Builder.SetAdapter("mssql")
if err != nil {
return err
}
ins.db = db
return nil
qgen.Builder.SetConn(db)
return qgen.Builder.SetAdapter("mssql")
}
func (ins *MssqlInstaller) TableDefs() (err error) {
@ -126,7 +120,6 @@ func (ins *MssqlInstaller) TableDefs() (err error) {
return err
}
}
//fmt.Println("Finished creating the tables")
return nil
}
@ -150,8 +143,6 @@ func (ins *MssqlInstaller) InitialData() (err error) {
return err
}
}
//fmt.Println("Finished inserting the database data")
return nil
}

View File

@ -55,5 +55,36 @@
"Accounts": {
"VerifyEmailSubject": "Validate Your Email @ {{name}}",
"VerifyEmailBody": "Dear {{username}}, following your registration on our forums, we ask you to validate your email, so that we can confirm that this email actually belongs to you.\n\nClick on the following link to do so. {{schema}}://{{url}}/user/edit/token/{{token}}\n\nIf you haven't created an account here, then please feel free to ignore this email.\nWe're sorry for the inconvenience this may have caused."
},
"Errors": {
"NoPerms": {
}
},
"PageTitles": {
"overview":"Overview",
"page":"Page",
"topics":"All Topics",
"forums":"Forum List",
"login":"Login",
"register":"Registration",
"ip-search":"IP Search",
"panel-dashboard":"Control Panel Dashboard",
"panel-forums":"Forum Manager",
"panel-delete-forum":"Delete Forum",
"panel-edit-forum":"Forum Editor",
"panel-settings":"Setting Manager",
"panel-edit-setting":"Edit Setting",
"panel-word-filters":"Word Filter Manager",
"panel-edit-word-filter":"Edit Word Filter",
"panel-plugins":"Plugin Manager",
"panel-users":"User Manager",
"panel-edit-user":"User Editor",
"panel-groups":"Group Manager",
"panel-edit-group":"Group Editor",
"panel-themes":"Theme Manager",
"panel-backups":"Backups",
"panel-mod-logs":"Moderation Logs",
"panel-debug":"Debug"
}
}

94
main.go
View File

@ -29,6 +29,61 @@ type Globs struct {
stmts *Stmts
}
func afterDBInit() (err error) {
common.Rstore, err = common.NewSQLReplyStore()
if err != nil {
return err
}
common.Prstore, err = common.NewSQLProfileReplyStore()
if err != nil {
return err
}
err = common.InitTemplates()
if err != nil {
return err
}
err = common.InitPhrases()
if err != nil {
return err
}
log.Print("Loading the static files.")
err = common.StaticFiles.Init()
if err != nil {
return err
}
log.Print("Initialising the widgets")
err = common.InitWidgets()
if err != nil {
return err
}
log.Print("Initialising the authentication system")
common.Auth, err = common.NewDefaultAuth()
if err != nil {
return err
}
err = common.LoadWordFilters()
if err != nil {
return err
}
common.ModLogs, err = common.NewModLogStore()
if err != nil {
return err
}
common.AdminLogs, err = common.NewAdminLogStore()
if err != nil {
return err
}
return nil
}
// TODO: Split this function up
func main() {
// TODO: Recover from panics
@ -90,44 +145,7 @@ func main() {
log.Fatal(err)
}
common.Rstore, err = common.NewSQLReplyStore()
if err != nil {
log.Fatal(err)
}
common.Prstore, err = common.NewSQLProfileReplyStore()
if err != nil {
log.Fatal(err)
}
err = common.InitTemplates()
if err != nil {
log.Fatal(err)
}
err = common.InitPhrases()
if err != nil {
log.Fatal(err)
}
log.Print("Loading the static files.")
err = common.StaticFiles.Init()
if err != nil {
log.Fatal(err)
}
log.Print("Initialising the widgets")
err = common.InitWidgets()
if err != nil {
log.Fatal(err)
}
log.Print("Initialising the authentication system")
common.Auth, err = common.NewDefaultAuth()
if err != nil {
log.Fatal(err)
}
err = common.LoadWordFilters()
err = afterDBInit()
if err != nil {
log.Fatal(err)
}

View File

@ -55,12 +55,12 @@ func routeTopicCreate(w http.ResponseWriter, r *http.Request, user common.User,
var forumList []common.Forum
var canSee []int
if user.IsSuperAdmin {
canSee, err = common.Fstore.GetAllVisibleIDs()
canSee, err = common.Forums.GetAllVisibleIDs()
if err != nil {
return common.InternalError(err, w, r)
}
} else {
group, err := common.Gstore.Get(user.Group)
group, err := common.Groups.Get(user.Group)
if err != nil {
// TODO: Refactor this
common.LocalError("Something weird happened behind the scenes", w, r, user)
@ -78,7 +78,7 @@ func routeTopicCreate(w http.ResponseWriter, r *http.Request, user common.User,
}
// Do a bulk forum fetch, just in case it's the SqlForumStore?
forum := common.Fstore.DirtyGet(ffid)
forum := common.Forums.DirtyGet(ffid)
if forum.Name != "" && forum.Active {
fcopy := forum.Copy()
if common.Hooks["topic_create_frow_assign"] != nil {
@ -98,7 +98,7 @@ func routeTopicCreate(w http.ResponseWriter, r *http.Request, user common.User,
}
}
err = common.RunThemeTemplate(headerVars.ThemeName, "create-topic", ctpage, w)
err = common.RunThemeTemplate(headerVars.Theme.Name, "create-topic", ctpage, w)
if err != nil {
return common.InternalError(err, w, r)
}
@ -340,7 +340,7 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user common.User)
return common.InternalError(err, w, r)
}
err = common.Fstore.UpdateLastTopic(tid, user.ID, topic.ParentID)
err = common.Forums.UpdateLastTopic(tid, user.ID, topic.ParentID)
if err != nil && err != ErrNoRows {
return common.InternalError(err, w, r)
}
@ -623,7 +623,7 @@ func routeReportSubmit(w http.ResponseWriter, r *http.Request, user common.User,
return common.InternalError(err, w, r)
}
err = common.Fstore.AddTopic(int(lastID), user.ID, fid)
err = common.Forums.AddTopic(int(lastID), user.ID, fid)
if err != nil && err != ErrNoRows {
return common.InternalError(err, w, r)
}

1
migrations/filler.txt Normal file
View File

@ -0,0 +1 @@
This file is here so that Git will include this folder in the repository.

View File

@ -5,7 +5,6 @@ import (
"fmt"
"net/http/httptest"
"runtime/debug"
"strconv"
"testing"
"time"
@ -40,29 +39,39 @@ func TestUserStore(t *testing.T) {
}
var err error
common.Users, err = common.NewMemoryUserStore(common.Config.UserCacheCapacity)
ucache := common.NewMemoryUserCache(common.Config.UserCacheCapacity)
common.Users, err = common.NewDefaultUserStore(ucache)
expectNilErr(t, err)
common.Users.(common.UserCache).Flush()
ucache.Flush()
userStoreTest(t, 2)
common.Users, err = common.NewSQLUserStore()
common.Users, err = common.NewDefaultUserStore(nil)
expectNilErr(t, err)
userStoreTest(t, 3)
}
func userStoreTest(t *testing.T, newUserID int) {
ucache, hasCache := common.Users.(common.UserCache)
ucache := common.Users.GetCache()
// Go doesn't have short-circuiting, so this'll allow us to do one liner tests
if !hasCache {
ucache = &common.NullUserStore{}
isCacheLengthZero := func(ucache common.UserCache) bool {
if ucache == nil {
return true
}
expect(t, (!hasCache || ucache.Length() == 0), fmt.Sprintf("The initial ucache length should be zero, not %d", ucache.Length()))
return ucache.Length() == 0
}
cacheLength := func(ucache common.UserCache) int {
if ucache == nil {
return 0
}
return ucache.Length()
}
expect(t, isCacheLengthZero(ucache), fmt.Sprintf("The initial ucache length should be zero, not %d", cacheLength(ucache)))
_, err := common.Users.Get(-1)
recordMustNotExist(t, err, "UID #-1 shouldn't exist")
expect(t, !hasCache || ucache.Length() == 0, fmt.Sprintf("We found %d items in the user cache and it's supposed to be empty", ucache.Length()))
expect(t, isCacheLengthZero(ucache), fmt.Sprintf("We found %d items in the user cache and it's supposed to be empty", cacheLength(ucache)))
_, err = common.Users.Get(0)
recordMustNotExist(t, err, "UID #0 shouldn't exist")
expect(t, !hasCache || ucache.Length() == 0, fmt.Sprintf("We found %d items in the user cache and it's supposed to be empty", ucache.Length()))
expect(t, isCacheLengthZero(ucache), fmt.Sprintf("We found %d items in the user cache and it's supposed to be empty", cacheLength(ucache)))
user, err := common.Users.Get(1)
recordMustExist(t, err, "Couldn't find UID #1")
@ -79,24 +88,20 @@ func userStoreTest(t *testing.T, newUserID int) {
_, err = common.Users.Get(newUserID)
recordMustNotExist(t, err, fmt.Sprintf("UID #%d shouldn't exist", newUserID))
if hasCache {
if ucache != nil {
expectIntToBeX(t, ucache.Length(), 1, "User cache length should be 1, not %d")
_, err = ucache.CacheGet(-1)
_, err = ucache.Get(-1)
recordMustNotExist(t, err, "UID #-1 shouldn't exist, even in the cache")
_, err = ucache.CacheGet(0)
_, err = ucache.Get(0)
recordMustNotExist(t, err, "UID #0 shouldn't exist, even in the cache")
user, err = ucache.CacheGet(1)
user, err = ucache.Get(1)
recordMustExist(t, err, "Couldn't find UID #1 in the cache")
if user.ID != 1 {
t.Error("user.ID does not match the requested UID. Got '" + strconv.Itoa(user.ID) + "' instead.")
}
if user.Name != "Admin" {
t.Error("user.Name should be 'Admin', not '" + user.Name + "'")
}
expect(t, user.ID == 1, fmt.Sprintf("user.ID does not match the requested UID. Got '%d' instead.", user.ID))
expect(t, user.Name == "Admin", fmt.Sprintf("user.Name should be 'Admin', not '%s'", user.Name))
_, err = ucache.CacheGet(newUserID)
_, err = ucache.Get(newUserID)
recordMustNotExist(t, err, "UID #%d shouldn't exist, even in the cache", newUserID)
ucache.Flush()
@ -106,22 +111,12 @@ func userStoreTest(t *testing.T, newUserID int) {
// TODO: Lock onto the specific error type. Is this even possible without sacrificing the detailed information in the error message?
var userList map[int]*common.User
userList, _ = common.Users.BulkGetMap([]int{-1})
if len(userList) > 0 {
t.Error("There shouldn't be any results for UID #-1")
}
if hasCache {
expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d")
}
expect(t, len(userList) == 0, fmt.Sprintf("The userList length should be 0, not %d", len(userList)))
expect(t, isCacheLengthZero(ucache), fmt.Sprintf("User cache length should be 0, not %d", cacheLength(ucache)))
userList, _ = common.Users.BulkGetMap([]int{0})
if len(userList) > 0 {
t.Error("There shouldn't be any results for UID #0")
}
if hasCache {
expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d")
}
expect(t, len(userList) == 0, fmt.Sprintf("The userList length should be 0, not %d", len(userList)))
expect(t, isCacheLengthZero(ucache), fmt.Sprintf("User cache length should be 0, not %d", cacheLength(ucache)))
userList, _ = common.Users.BulkGetMap([]int{1})
if len(userList) == 0 {
@ -135,18 +130,14 @@ func userStoreTest(t *testing.T, newUserID int) {
t.Error("We couldn't find UID #1 in the returned map")
t.Error("userList", userList)
}
if user.ID != 1 {
t.Error("user.ID does not match the requested UID. Got '" + strconv.Itoa(user.ID) + "' instead.")
}
expect(t, user.ID == 1, fmt.Sprintf("user.ID does not match the requested UID. Got '%d' instead.", user.ID))
if hasCache {
if ucache != nil {
expectIntToBeX(t, ucache.Length(), 1, "User cache length should be 1, not %d")
user, err = ucache.CacheGet(1)
user, err = ucache.Get(1)
recordMustExist(t, err, "Couldn't find UID #1 in the cache")
if user.ID != 1 {
t.Errorf("user.ID does not match the requested UID. Got '%d' instead.", user.ID)
}
expect(t, user.ID == 1, fmt.Sprintf("user.ID does not match the requested UID. Got '%d' instead.", user.ID))
ucache.Flush()
}
@ -155,7 +146,7 @@ func userStoreTest(t *testing.T, newUserID int) {
expect(t, common.Users.Exists(1), "UID #1 should exist")
expect(t, !common.Users.Exists(newUserID), fmt.Sprintf("UID #%d shouldn't exist", newUserID))
expect(t, !hasCache || ucache.Length() == 0, fmt.Sprintf("User cache length should be 0, not %d", ucache.Length()))
expect(t, isCacheLengthZero(ucache), fmt.Sprintf("User cache length should be 0, not %d", cacheLength(ucache)))
expectIntToBeX(t, common.Users.GlobalCount(), 1, "The number of users should be one, not %d")
var awaitingActivation = 5
@ -166,9 +157,7 @@ func userStoreTest(t *testing.T, newUserID int) {
user, err = common.Users.Get(newUserID)
recordMustExist(t, err, "Couldn't find UID #%d", newUserID)
if user.ID != newUserID {
t.Errorf("The UID of the user record should be %d", newUserID)
}
expect(t, user.ID == newUserID, fmt.Sprintf("The UID of the user record should be %d", newUserID))
expect(t, user.Name == "Sam", "The user should be named Sam")
expect(t, !user.IsSuperAdmin, "Sam should not be a super admin")
@ -178,9 +167,9 @@ func userStoreTest(t *testing.T, newUserID int) {
expect(t, !user.IsBanned, "Sam should not be banned")
expectIntToBeX(t, user.Group, 5, "Sam should be in group 5")
if hasCache {
if ucache != nil {
expectIntToBeX(t, ucache.Length(), 1, "User cache length should be 1, not %d")
user, err = ucache.CacheGet(newUserID)
user, err = ucache.Get(newUserID)
recordMustExist(t, err, "Couldn't find UID #%d in the cache", newUserID)
expect(t, user.ID == newUserID, fmt.Sprintf("user.ID does not match the requested UID. Got '%d' instead.", user.ID))
}
@ -190,9 +179,9 @@ func userStoreTest(t *testing.T, newUserID int) {
expectIntToBeX(t, user.Group, 5, "Sam should still be in group 5 in this copy")
// ? - What if we change the caching mechanism so it isn't hard purged and reloaded? We'll deal with that when we come to it, but for now, this is a sign of a cache bug
if hasCache {
if ucache != nil {
expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d")
_, err = ucache.CacheGet(newUserID)
_, err = ucache.Get(newUserID)
recordMustNotExist(t, err, "UID #%d shouldn't be in the cache", newUserID)
}
@ -216,18 +205,16 @@ func userStoreTest(t *testing.T, newUserID int) {
expectNilErr(t, err)
expect(t, user.Group == common.Config.DefaultGroup, fmt.Sprintf("Sam should be in group %d, not %d", common.Config.DefaultGroup, user.Group))
if hasCache {
if ucache != nil {
expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d")
_, err = ucache.CacheGet(2)
_, err = ucache.Get(2)
recordMustNotExist(t, err, "UID #%d shouldn't be in the cache", newUserID)
}
user, err = common.Users.Get(newUserID)
recordMustExist(t, err, "Couldn't find UID #%d", newUserID)
if user.ID != newUserID {
t.Errorf("The UID of the user record should be %d", newUserID)
}
expect(t, user.ID == newUserID, fmt.Sprintf("The UID of the user record should be %d", newUserID))
expect(t, !user.IsSuperAdmin, "Sam should not be a super admin")
expect(t, !user.IsAdmin, "Sam should not be an admin")
expect(t, !user.IsSuperMod, "Sam should not be a super mod")
@ -242,9 +229,9 @@ func userStoreTest(t *testing.T, newUserID int) {
expectNilErr(t, err)
expectIntToBeX(t, user.Group, common.BanGroup, "Sam should still be in the ban group in this copy")
if hasCache {
if ucache != nil {
expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d")
_, err = ucache.CacheGet(newUserID)
_, err = ucache.Get(newUserID)
recordMustNotExist(t, err, "UID #%d shouldn't be in the cache", newUserID)
}
@ -414,9 +401,9 @@ func userStoreTest(t *testing.T, newUserID int) {
expectNilErr(t, err)
expect(t, !common.Users.Exists(newUserID), fmt.Sprintf("UID #%d should no longer exist", newUserID))
if hasCache {
if ucache != nil {
expectIntToBeX(t, ucache.Length(), 0, "User cache length should be 0, not %d")
_, err = ucache.CacheGet(newUserID)
_, err = ucache.Get(newUserID)
recordMustNotExist(t, err, "UID #%d shouldn't be in the cache", newUserID)
}
@ -505,10 +492,11 @@ func TestTopicStore(t *testing.T) {
}
var err error
common.Topics, err = common.NewMemoryTopicStore(common.Config.TopicCacheCapacity)
tcache := common.NewMemoryTopicCache(common.Config.TopicCacheCapacity)
common.Topics, err = common.NewDefaultTopicStore(tcache)
expectNilErr(t, err)
topicStoreTest(t)
common.Topics, err = common.NewSQLTopicStore()
common.Topics, err = common.NewDefaultTopicStore(nil)
expectNilErr(t, err)
topicStoreTest(t)
}
@ -526,25 +514,19 @@ func topicStoreTest(t *testing.T) {
recordMustExist(t, err, "Couldn't find TID #1")
if topic.ID != 1 {
t.Error("topic.ID does not match the requested TID. Got '" + strconv.Itoa(topic.ID) + "' instead.")
t.Error("topic.ID does not match the requested TID. Got '%d' instead.", topic.ID)
}
// TODO: Add BulkGetMap() to the TopicStore
ok := common.Topics.Exists(-1)
if ok {
t.Error("TID #-1 shouldn't exist")
}
expect(t, !ok, "TID #-1 shouldn't exist")
ok = common.Topics.Exists(0)
if ok {
t.Error("TID #0 shouldn't exist")
}
expect(t, !ok, "TID #0 shouldn't exist")
ok = common.Topics.Exists(1)
if !ok {
t.Error("TID #1 should exist")
}
expect(t, ok, "TID #1 should exist")
count := common.Topics.GlobalCount()
if count <= 0 {
@ -563,17 +545,17 @@ func TestForumStore(t *testing.T) {
common.InitPlugins()
}
_, err := common.Fstore.Get(-1)
_, err := common.Forums.Get(-1)
recordMustNotExist(t, err, "FID #-1 shouldn't exist")
_, err = common.Fstore.Get(0)
_, err = common.Forums.Get(0)
recordMustNotExist(t, err, "FID #0 shouldn't exist")
forum, err := common.Fstore.Get(1)
forum, err := common.Forums.Get(1)
recordMustExist(t, err, "Couldn't find FID #1")
if forum.ID != 1 {
t.Error("forum.ID doesn't not match the requested FID. Got '" + strconv.Itoa(forum.ID) + "' instead.'")
t.Error("forum.ID doesn't not match the requested FID. Got '%d' instead.'", forum.ID)
}
// TODO: Check the preset and forum permissions
expect(t, forum.Name == "Reports", fmt.Sprintf("FID #0 is named '%s' and not 'Reports'", forum.Name))
@ -581,7 +563,7 @@ func TestForumStore(t *testing.T) {
var expectDesc = "All the reports go here"
expect(t, forum.Desc == expectDesc, fmt.Sprintf("The forum description should be '%s' not '%s'", expectDesc, forum.Desc))
forum, err = common.Fstore.Get(2)
forum, err = common.Forums.Get(2)
recordMustExist(t, err, "Couldn't find FID #1")
expect(t, forum.ID == 2, fmt.Sprintf("The FID should be 2 not %d", forum.ID))
@ -590,11 +572,11 @@ func TestForumStore(t *testing.T) {
expectDesc = "A place for general discussions which don't fit elsewhere"
expect(t, forum.Desc == expectDesc, fmt.Sprintf("The forum description should be '%s' not '%s'", expectDesc, forum.Desc))
ok := common.Fstore.Exists(-1)
ok := common.Forums.Exists(-1)
expect(t, !ok, "FID #-1 shouldn't exist")
ok = common.Fstore.Exists(0)
ok = common.Forums.Exists(0)
expect(t, !ok, "FID #0 shouldn't exist")
ok = common.Fstore.Exists(1)
ok = common.Forums.Exists(1)
expect(t, ok, "FID #1 should exist")
// TODO: Test forum creation
@ -621,43 +603,38 @@ func TestGroupStore(t *testing.T) {
common.InitPlugins()
}
_, err := common.Gstore.Get(-1)
_, err := common.Groups.Get(-1)
recordMustNotExist(t, err, "GID #-1 shouldn't exist")
// TODO: Refactor the group store to remove GID #0
group, err := common.Gstore.Get(0)
group, err := common.Groups.Get(0)
recordMustExist(t, err, "Couldn't find GID #0")
if group.ID != 0 {
t.Errorf("group.ID doesn't not match the requested GID. Got '%d' instead.", group.ID)
}
expect(t, group.ID == 0, fmt.Sprintf("group.ID doesn't not match the requested GID. Got '%d' instead.", group.ID))
expect(t, group.Name == "Unknown", fmt.Sprintf("GID #0 is named '%s' and not 'Unknown'", group.Name))
group, err = common.Gstore.Get(1)
group, err = common.Groups.Get(1)
recordMustExist(t, err, "Couldn't find GID #1")
expect(t, group.ID == 1, fmt.Sprintf("group.ID doesn't not match the requested GID. Got '%d' instead.'", group.ID))
if group.ID != 1 {
t.Errorf("group.ID doesn't not match the requested GID. Got '%d' instead.'", group.ID)
}
ok := common.Gstore.Exists(-1)
ok := common.Groups.Exists(-1)
expect(t, !ok, "GID #-1 shouldn't exist")
// 0 aka Unknown, for system posts and other oddities
ok = common.Gstore.Exists(0)
ok = common.Groups.Exists(0)
expect(t, ok, "GID #0 should exist")
ok = common.Gstore.Exists(1)
ok = common.Groups.Exists(1)
expect(t, ok, "GID #1 should exist")
var isAdmin = true
var isMod = true
var isBanned = false
gid, err := common.Gstore.Create("Testing", "Test", isAdmin, isMod, isBanned)
gid, err := common.Groups.Create("Testing", "Test", isAdmin, isMod, isBanned)
expectNilErr(t, err)
expect(t, common.Gstore.Exists(gid), "The group we just made doesn't exist")
expect(t, common.Groups.Exists(gid), "The group we just made doesn't exist")
group, err = common.Gstore.Get(gid)
group, err = common.Groups.Get(gid)
expectNilErr(t, err)
expect(t, group.ID == gid, "The group ID should match the requested ID")
expect(t, group.IsAdmin, "This should be an admin group")
@ -667,11 +644,11 @@ func TestGroupStore(t *testing.T) {
isAdmin = false
isMod = true
isBanned = true
gid, err = common.Gstore.Create("Testing 2", "Test", isAdmin, isMod, isBanned)
gid, err = common.Groups.Create("Testing 2", "Test", isAdmin, isMod, isBanned)
expectNilErr(t, err)
expect(t, common.Gstore.Exists(gid), "The group we just made doesn't exist")
expect(t, common.Groups.Exists(gid), "The group we just made doesn't exist")
group, err = common.Gstore.Get(gid)
group, err = common.Groups.Get(gid)
expectNilErr(t, err)
expect(t, group.ID == gid, "The group ID should match the requested ID")
expect(t, !group.IsAdmin, "This should not be an admin group")
@ -682,7 +659,7 @@ func TestGroupStore(t *testing.T) {
err = group.ChangeRank(false, false, true)
expectNilErr(t, err)
group, err = common.Gstore.Get(gid)
group, err = common.Groups.Get(gid)
expectNilErr(t, err)
expect(t, group.ID == gid, "The group ID should match the requested ID")
expect(t, !group.IsAdmin, "This shouldn't be an admin group")
@ -692,7 +669,7 @@ func TestGroupStore(t *testing.T) {
err = group.ChangeRank(true, true, true)
expectNilErr(t, err)
group, err = common.Gstore.Get(gid)
group, err = common.Groups.Get(gid)
expectNilErr(t, err)
expect(t, group.ID == gid, "The group ID should match the requested ID")
expect(t, group.IsAdmin, "This should be an admin group")
@ -702,7 +679,7 @@ func TestGroupStore(t *testing.T) {
err = group.ChangeRank(false, true, true)
expectNilErr(t, err)
group, err = common.Gstore.Get(gid)
group, err = common.Groups.Get(gid)
expectNilErr(t, err)
expect(t, group.ID == gid, "The group ID should match the requested ID")
expect(t, !group.IsAdmin, "This shouldn't be an admin group")
@ -710,9 +687,9 @@ func TestGroupStore(t *testing.T) {
expect(t, !group.IsBanned, "This shouldn't be a ban group")
// Make sure the data is static
common.Gstore.Reload(gid)
common.Groups.Reload(gid)
group, err = common.Gstore.Get(gid)
group, err = common.Groups.Get(gid)
expectNilErr(t, err)
expect(t, group.ID == gid, "The group ID should match the requested ID")
expect(t, !group.IsAdmin, "This shouldn't be an admin group")

View File

@ -15,6 +15,7 @@ import (
// TODO: Update the stats after edits so that we don't under or over decrement stats during deletes
// TODO: Disable stat updates in posts handled by plugin_guilds
// TODO: Make sure this route is member only
func routeEditTopic(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
err := r.ParseForm()
if err != nil {
@ -50,7 +51,7 @@ func routeEditTopic(w http.ResponseWriter, r *http.Request, user common.User) co
return common.InternalErrorJSQ(err, w, r, isJs)
}
err = common.Fstore.UpdateLastTopic(topic.ID, user.ID, topic.ParentID)
err = common.Forums.UpdateLastTopic(topic.ID, user.ID, topic.ParentID)
if err != nil && err != ErrNoRows {
return common.InternalErrorJSQ(err, w, r, isJs)
}
@ -65,11 +66,12 @@ func routeEditTopic(w http.ResponseWriter, r *http.Request, user common.User) co
// TODO: Add support for soft-deletion and add a permission for hard delete in addition to the usual
// TODO: Disable stat updates in posts handled by plugin_guilds
// TODO: Make sure this route is member only
func routeDeleteTopic(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError {
// TODO: Move this to some sort of middleware
var tids []int
var isJs = false
if r.Header.Get("Content-type") == "application/json" {
if common.ReqIsJson(r) {
if r.Body == nil {
return common.PreErrorJS("No request body", w, r)
}
@ -114,7 +116,7 @@ func routeDeleteTopic(w http.ResponseWriter, r *http.Request, user common.User)
return common.InternalErrorJSQ(err, w, r, isJs)
}
err = common.AddModLog("delete", tid, "topic", user.LastIP, user.ID)
err = common.ModLogs.Create("delete", tid, "topic", user.LastIP, user.ID)
if err != nil {
return common.InternalErrorJSQ(err, w, r, isJs)
}
@ -158,7 +160,7 @@ func routeStickTopic(w http.ResponseWriter, r *http.Request, user common.User) c
return common.InternalError(err, w, r)
}
err = common.AddModLog("stick", tid, "topic", user.LastIP, user.ID)
err = common.ModLogs.Create("stick", tid, "topic", user.LastIP, user.ID)
if err != nil {
return common.InternalError(err, w, r)
}
@ -197,7 +199,7 @@ func routeUnstickTopic(w http.ResponseWriter, r *http.Request, user common.User)
return common.InternalError(err, w, r)
}
err = common.AddModLog("unstick", tid, "topic", user.LastIP, user.ID)
err = common.ModLogs.Create("unstick", tid, "topic", user.LastIP, user.ID)
if err != nil {
return common.InternalError(err, w, r)
}
@ -214,7 +216,7 @@ func routeLockTopic(w http.ResponseWriter, r *http.Request, user common.User) co
// TODO: Move this to some sort of middleware
var tids []int
var isJs = false
if r.Header.Get("Content-type") == "application/json" {
if common.ReqIsJson(r) {
if r.Body == nil {
return common.PreErrorJS("No request body", w, r)
}
@ -256,7 +258,7 @@ func routeLockTopic(w http.ResponseWriter, r *http.Request, user common.User) co
return common.InternalErrorJSQ(err, w, r, isJs)
}
err = common.AddModLog("lock", tid, "topic", user.LastIP, user.ID)
err = common.ModLogs.Create("lock", tid, "topic", user.LastIP, user.ID)
if err != nil {
return common.InternalErrorJSQ(err, w, r, isJs)
}
@ -299,7 +301,7 @@ func routeUnlockTopic(w http.ResponseWriter, r *http.Request, user common.User)
return common.InternalError(err, w, r)
}
err = common.AddModLog("unlock", tid, "topic", user.LastIP, user.ID)
err = common.ModLogs.Create("unlock", tid, "topic", user.LastIP, user.ID)
if err != nil {
return common.InternalError(err, w, r)
}
@ -425,7 +427,7 @@ func routeReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user common.
return common.InternalErrorJSQ(err, w, r, isJs)
}
err = common.AddModLog("delete", reply.ParentID, "reply", user.LastIP, user.ID)
err = common.ModLogs.Create("delete", reply.ParentID, "reply", user.LastIP, user.ID)
if err != nil {
return common.InternalErrorJSQ(err, w, r, isJs)
}
@ -588,7 +590,7 @@ func routeIps(w http.ResponseWriter, r *http.Request, user common.User) common.R
return common.InternalError(err, w, r)
}
pi := common.IPSearchPage{"IP Search", user, headerVars, userList, ip}
pi := common.IPSearchPage{common.GetTitlePhrase("ip-search"), user, headerVars, userList, ip}
if common.PreRenderHooks["pre_render_ips"] != nil {
if common.RunPreRenderHook("pre_render_ips", w, r, &user, &pi) {
return nil
@ -610,9 +612,9 @@ func routeBanSubmit(w http.ResponseWriter, r *http.Request, user common.User) co
if err != nil {
return common.LocalError("The provided common.User ID is not a valid number.", w, r, user)
}
/*if uid == -2 {
return common.LocalError("Stop trying to ban Merlin! Ban admin! Bad! No!",w,r,user)
}*/
if uid == -2 {
return common.LocalError("Why don't you like Merlin?", w, r, user)
}
targetUser, err := common.Users.Get(uid)
if err == ErrNoRows {
@ -622,7 +624,7 @@ func routeBanSubmit(w http.ResponseWriter, r *http.Request, user common.User) co
}
// TODO: Is there a difference between IsMod and IsSuperMod? Should we delete the redundant one?
if targetUser.IsSuperAdmin || targetUser.IsAdmin || targetUser.IsMod {
if targetUser.IsMod {
return common.LocalError("You may not ban another staff member.", w, r, user)
}
if uid == user.ID {
@ -665,7 +667,7 @@ func routeBanSubmit(w http.ResponseWriter, r *http.Request, user common.User) co
return common.InternalError(err, w, r)
}
err = common.AddModLog("ban", uid, "user", user.LastIP, user.ID)
err = common.ModLogs.Create("ban", uid, "user", user.LastIP, user.ID)
if err != nil {
return common.InternalError(err, w, r)
}
@ -704,7 +706,7 @@ func routeUnban(w http.ResponseWriter, r *http.Request, user common.User) common
return common.InternalError(err, w, r)
}
err = common.AddModLog("unban", uid, "user", user.LastIP, user.ID)
err = common.ModLogs.Create("unban", uid, "user", user.LastIP, user.ID)
if err != nil {
return common.InternalError(err, w, r)
}
@ -738,7 +740,7 @@ func routeActivate(w http.ResponseWriter, r *http.Request, user common.User) com
return common.InternalError(err, w, r)
}
err = common.AddModLog("activate", targetUser.ID, "user", user.LastIP, user.ID)
err = common.ModLogs.Create("activate", targetUser.ID, "user", user.LastIP, user.ID)
if err != nil {
return common.InternalError(err, w, r)
}

View File

@ -28,6 +28,16 @@ func routePanel(w http.ResponseWriter, r *http.Request, user common.User) common
var cpustr = "Unknown"
var cpuColour string
lessThanSwitch := func(number int, lowerBound int, midBound int) string {
switch {
case number < lowerBound:
return "stat_green"
case number < midBound:
return "stat_orange"
}
return "stat_red"
}
var ramstr, ramColour string
memres, err := mem.VirtualMemory()
if err != nil {
@ -37,7 +47,6 @@ func routePanel(w http.ResponseWriter, r *http.Request, user common.User) common
usedCount := common.ConvertByteInUnit(float64(memres.Total-memres.Available), totalUnit)
// Round totals with .9s up, it's how most people see it anyway. Floats are notoriously imprecise, so do it off 0.85
//log.Print("pre used_count",used_count)
var totstr string
if (totalCount - float64(int(totalCount))) > 0.85 {
usedCount += 1.0 - (totalCount - float64(int(totalCount)))
@ -45,7 +54,6 @@ func routePanel(w http.ResponseWriter, r *http.Request, user common.User) common
} else {
totstr = fmt.Sprintf("%.1f", totalCount)
}
//log.Print("post used_count",used_count)
if usedCount > totalCount {
usedCount = totalCount
@ -53,31 +61,27 @@ func routePanel(w http.ResponseWriter, r *http.Request, user common.User) common
ramstr = fmt.Sprintf("%.1f", usedCount) + " / " + totstr + totalUnit
ramperc := ((memres.Total - memres.Available) * 100) / memres.Total
//log.Print("ramperc",ramperc)
if ramperc < 50 {
ramColour = "stat_green"
} else if ramperc < 75 {
ramColour = "stat_orange"
} else {
ramColour = "stat_red"
}
ramColour = lessThanSwitch(int(ramperc), 50, 75)
}
greaterThanSwitch := func(number int, lowerBound int, midBound int) string {
switch {
case number > midBound:
return "stat_green"
case number > lowerBound:
return "stat_orange"
}
return "stat_red"
}
// TODO: Add a stat store for this?
var postCount int
err = stmts.todaysPostCount.QueryRow().Scan(&postCount)
if err != nil && err != ErrNoRows {
return common.InternalError(err, w, r)
}
var postInterval = "day"
var postColour string
if postCount > 25 {
postColour = "stat_green"
} else if postCount > 5 {
postColour = "stat_orange"
} else {
postColour = "stat_red"
}
var postColour = greaterThanSwitch(postCount, 5, 25)
var topicCount int
err = stmts.todaysTopicCount.QueryRow().Scan(&topicCount)
@ -85,15 +89,7 @@ func routePanel(w http.ResponseWriter, r *http.Request, user common.User) common
return common.InternalError(err, w, r)
}
var topicInterval = "day"
var topicColour string
if topicCount > 8 {
topicColour = "stat_green"
} else if topicCount > 0 {
topicColour = "stat_orange"
} else {
topicColour = "stat_red"
}
var topicColour = greaterThanSwitch(topicCount, 0, 8)
var reportCount int
err = stmts.todaysReportCount.QueryRow().Scan(&reportCount)
@ -120,32 +116,9 @@ func routePanel(w http.ResponseWriter, r *http.Request, user common.User) common
gonline := wsHub.guestCount()
totonline := uonline + gonline
var onlineColour string
if totonline > 10 {
onlineColour = "stat_green"
} else if totonline > 3 {
onlineColour = "stat_orange"
} else {
onlineColour = "stat_red"
}
var onlineGuestsColour string
if gonline > 10 {
onlineGuestsColour = "stat_green"
} else if gonline > 1 {
onlineGuestsColour = "stat_orange"
} else {
onlineGuestsColour = "stat_red"
}
var onlineUsersColour string
if uonline > 5 {
onlineUsersColour = "stat_green"
} else if uonline > 1 {
onlineUsersColour = "stat_orange"
} else {
onlineUsersColour = "stat_red"
}
var onlineColour = greaterThanSwitch(totonline, 3, 10)
var onlineGuestsColour = greaterThanSwitch(gonline, 1, 10)
var onlineUsersColour = greaterThanSwitch(uonline, 1, 5)
totonline, totunit := common.ConvertFriendlyUnit(totonline)
uonline, uunit := common.ConvertFriendlyUnit(uonline)
@ -168,7 +141,7 @@ func routePanel(w http.ResponseWriter, r *http.Request, user common.User) common
gridElements = append(gridElements, common.GridElement{"dash-visitorsperweek", "2 visitors / week", 13, "grid_stat stat_disabled", "", "", "Coming Soon!" /*"The number of unique visitors we've had over the last 7 days"*/})
gridElements = append(gridElements, common.GridElement{"dash-postsperuser", "5 posts / user / week", 14, "grid_stat stat_disabled", "", "", "Coming Soon!" /*"The average number of posts made by each active user over the past week"*/})
pi := common.PanelDashboardPage{"Control Panel Dashboard", user, headerVars, stats, gridElements}
pi := common.PanelDashboardPage{common.GetTitlePhrase("panel-dashboard"), user, headerVars, stats, gridElements}
if common.PreRenderHooks["pre_render_panel_dashboard"] != nil {
if common.RunPreRenderHook("pre_render_panel_dashboard", w, r, &user, &pi) {
return nil
@ -192,7 +165,7 @@ func routePanelForums(w http.ResponseWriter, r *http.Request, user common.User)
// TODO: Paginate this?
var forumList []interface{}
forums, err := common.Fstore.GetAll()
forums, err := common.Forums.GetAll()
if err != nil {
return common.InternalError(err, w, r)
}
@ -207,7 +180,7 @@ func routePanelForums(w http.ResponseWriter, r *http.Request, user common.User)
forumList = append(forumList, fadmin)
}
}
pi := common.PanelPage{"Forum Manager", user, headerVars, stats, forumList, nil}
pi := common.PanelPage{common.GetTitlePhrase("panel-forums"), user, headerVars, stats, forumList, nil}
if common.PreRenderHooks["pre_render_panel_forums"] != nil {
if common.RunPreRenderHook("pre_render_panel_forums", w, r, &user, &pi) {
return nil
@ -217,6 +190,7 @@ func routePanelForums(w http.ResponseWriter, r *http.Request, user common.User)
if err != nil {
return common.InternalError(err, w, r)
}
return nil
}
@ -235,7 +209,7 @@ func routePanelForumsCreateSubmit(w http.ResponseWriter, r *http.Request, user c
factive := r.PostFormValue("forum-name")
active := (factive == "on" || factive == "1")
_, err := common.Fstore.Create(fname, fdesc, active, fpreset)
_, err := common.Forums.Create(fname, fdesc, active, fpreset)
if err != nil {
return common.InternalError(err, w, r)
}
@ -259,17 +233,18 @@ func routePanelForumsDelete(w http.ResponseWriter, r *http.Request, user common.
return common.LocalError("The provided Forum ID is not a valid number.", w, r, user)
}
forum, err := common.Fstore.Get(fid)
forum, err := common.Forums.Get(fid)
if err == ErrNoRows {
return common.LocalError("The forum you're trying to delete doesn't exist.", w, r, user)
} else if err != nil {
return common.InternalError(err, w, r)
}
// TODO: Make this a phrase
confirmMsg := "Are you sure you want to delete the '" + forum.Name + "' forum?"
yousure := common.AreYouSure{"/panel/forums/delete/submit/" + strconv.Itoa(fid), confirmMsg}
pi := common.PanelPage{"Delete Forum", user, headerVars, stats, tList, yousure}
pi := common.PanelPage{common.GetTitlePhrase("panel-delete-forum"), user, headerVars, stats, tList, yousure}
if common.PreRenderHooks["pre_render_panel_delete_forum"] != nil {
if common.RunPreRenderHook("pre_render_panel_delete_forum", w, r, &user, &pi) {
return nil
@ -296,7 +271,7 @@ func routePanelForumsDeleteSubmit(w http.ResponseWriter, r *http.Request, user c
return common.LocalError("The provided Forum ID is not a valid number.", w, r, user)
}
err = common.Fstore.Delete(fid)
err = common.Forums.Delete(fid)
if err == ErrNoRows {
return common.LocalError("The forum you're trying to delete doesn't exist.", w, r, user)
} else if err != nil {
@ -321,7 +296,7 @@ func routePanelForumsEdit(w http.ResponseWriter, r *http.Request, user common.Us
return common.LocalError("The provided Forum ID is not a valid number.", w, r, user)
}
forum, err := common.Fstore.Get(fid)
forum, err := common.Forums.Get(fid)
if err == ErrNoRows {
return common.LocalError("The forum you're trying to edit doesn't exist.", w, r, user)
} else if err != nil {
@ -332,7 +307,7 @@ func routePanelForumsEdit(w http.ResponseWriter, r *http.Request, user common.Us
forum.Preset = "custom"
}
glist, err := common.Gstore.GetAll()
glist, err := common.Groups.GetAll()
if err != nil {
return common.InternalError(err, w, r)
}
@ -345,7 +320,7 @@ func routePanelForumsEdit(w http.ResponseWriter, r *http.Request, user common.Us
gplist = append(gplist, common.GroupForumPermPreset{group, common.ForumPermsToGroupForumPreset(group.Forums[fid])})
}
pi := common.PanelEditForumPage{"Forum Editor", user, headerVars, stats, forum.ID, forum.Name, forum.Desc, forum.Active, forum.Preset, gplist}
pi := common.PanelEditForumPage{common.GetTitlePhrase("panel-edit-forum"), user, headerVars, stats, forum.ID, forum.Name, forum.Desc, forum.Active, forum.Preset, gplist}
if common.PreRenderHooks["pre_render_panel_edit_forum"] != nil {
if common.RunPreRenderHook("pre_render_panel_edit_forum", w, r, &user, &pi) {
return nil
@ -355,6 +330,7 @@ func routePanelForumsEdit(w http.ResponseWriter, r *http.Request, user common.Us
if err != nil {
return common.InternalError(err, w, r)
}
return nil
}
@ -373,7 +349,7 @@ func routePanelForumsEditSubmit(w http.ResponseWriter, r *http.Request, user com
return common.LocalErrorJSQ("The provided Forum ID is not a valid number.", w, r, user, isJs)
}
forum, err := common.Fstore.Get(fid)
forum, err := common.Forums.Get(fid)
if err == ErrNoRows {
return common.LocalErrorJSQ("The forum you're trying to edit doesn't exist.", w, r, user, isJs)
} else if err != nil {
@ -425,7 +401,7 @@ func routePanelForumsEditPermsSubmit(w http.ResponseWriter, r *http.Request, use
return common.LocalErrorJSQ("Invalid Group ID", w, r, user, isJs)
}
forum, err := common.Fstore.Get(fid)
forum, err := common.Forums.Get(fid)
if err == ErrNoRows {
return common.LocalErrorJSQ("This forum doesn't exist", w, r, user, isJs)
} else if err != nil {
@ -454,46 +430,35 @@ func routePanelSettings(w http.ResponseWriter, r *http.Request, user common.User
if !user.Perms.EditSettings {
return common.NoPermissions(w, r, user)
}
var settingList = make(map[string]interface{})
rows, err := stmts.getSettings.Query()
settings, err := headerVars.Settings.BypassGetAll()
if err != nil {
return common.InternalError(err, w, r)
}
defer rows.Close()
// nolint need the type so people viewing this file understand what it returns without visiting setting.go
// nolint need the type so people viewing this file understand what it returns without visiting phrases.go
var settingLabels map[string]string = common.GetAllSettingLabels()
var sname, scontent, stype string
for rows.Next() {
err := rows.Scan(&sname, &scontent, &stype)
if err != nil {
return common.InternalError(err, w, r)
}
if stype == "list" {
llist := settingLabels[sname]
for _, setting := range settings {
if setting.Type == "list" {
llist := settingLabels[setting.Name]
labels := strings.Split(llist, ",")
conv, err := strconv.Atoi(scontent)
conv, err := strconv.Atoi(setting.Content)
if err != nil {
return common.LocalError("The setting '"+sname+"' can't be converted to an integer", w, r, user)
return common.LocalError("The setting '"+setting.Name+"' can't be converted to an integer", w, r, user)
}
scontent = labels[conv-1]
} else if stype == "bool" {
if scontent == "1" {
scontent = "Yes"
setting.Content = labels[conv-1]
} else if setting.Type == "bool" {
if setting.Content == "1" {
setting.Content = "Yes"
} else {
scontent = "No"
setting.Content = "No"
}
}
settingList[sname] = scontent
}
err = rows.Err()
if err != nil {
return common.InternalError(err, w, r)
settingList[setting.Name] = setting.Content
}
pi := common.PanelPage{"Setting Manager", user, headerVars, stats, tList, settingList}
pi := common.PanelPage{common.GetTitlePhrase("panel-settings"), user, headerVars, stats, tList, settingList}
if common.PreRenderHooks["pre_render_panel_settings"] != nil {
if common.RunPreRenderHook("pre_render_panel_settings", w, r, &user, &pi) {
return nil
@ -506,7 +471,7 @@ func routePanelSettings(w http.ResponseWriter, r *http.Request, user common.User
return nil
}
func routePanelSetting(w http.ResponseWriter, r *http.Request, user common.User, sname string) common.RouteError {
func routePanelSettingEdit(w http.ResponseWriter, r *http.Request, user common.User, sname string) common.RouteError {
headerVars, stats, ferr := common.PanelUserCheck(w, r, &user)
if ferr != nil {
return ferr
@ -539,7 +504,7 @@ func routePanelSetting(w http.ResponseWriter, r *http.Request, user common.User,
}
}
pi := common.PanelPage{"Edit Setting", user, headerVars, stats, itemList, setting}
pi := common.PanelPage{common.GetTitlePhrase("panel-edit-setting"), user, headerVars, stats, itemList, setting}
if common.PreRenderHooks["pre_render_panel_setting"] != nil {
if common.RunPreRenderHook("pre_render_panel_setting", w, r, &user, &pi) {
return nil
@ -552,7 +517,7 @@ func routePanelSetting(w http.ResponseWriter, r *http.Request, user common.User,
return nil
}
func routePanelSettingEdit(w http.ResponseWriter, r *http.Request, user common.User, sname string) common.RouteError {
func routePanelSettingEditSubmit(w http.ResponseWriter, r *http.Request, user common.User, sname string) common.RouteError {
headerLite, ferr := common.SimplePanelUserCheck(w, r, &user)
if ferr != nil {
return ferr
@ -562,34 +527,14 @@ func routePanelSettingEdit(w http.ResponseWriter, r *http.Request, user common.U
}
scontent := r.PostFormValue("setting-value")
setting, err := headerLite.Settings.BypassGet(sname)
if err == ErrNoRows {
return common.LocalError("The setting you want to edit doesn't exist.", w, r, user)
} else if err != nil {
return common.InternalError(err, w, r)
}
if setting.Type == "bool" {
if scontent == "on" || scontent == "1" {
scontent = "1"
} else {
scontent = "0"
}
}
// TODO: Make this a method or function?
_, err = stmts.updateSetting.Exec(scontent, sname)
err := headerLite.Settings.Update(sname, scontent)
if err != nil {
if common.SafeSettingError(err) {
return common.LocalError(err.Error(), w, r, user)
}
return common.InternalError(err, w, r)
}
errmsg := headerLite.Settings.ParseSetting(sname, scontent, setting.Type, setting.Constraint)
if errmsg != "" {
return common.LocalError(errmsg, w, r, user)
}
// TODO: Do a reload instead?
common.SettingBox.Store(headerLite.Settings)
http.Redirect(w, r, "/panel/settings/", http.StatusSeeOther)
return nil
}
@ -604,7 +549,7 @@ func routePanelWordFilters(w http.ResponseWriter, r *http.Request, user common.U
}
var filterList = common.WordFilterBox.Load().(common.WordFilterMap)
pi := common.PanelPage{"Word Filter Manager", user, headerVars, stats, tList, filterList}
pi := common.PanelPage{common.GetTitlePhrase("panel-word-filters"), user, headerVars, stats, tList, filterList}
if common.PreRenderHooks["pre_render_panel_word_filters"] != nil {
if common.RunPreRenderHook("pre_render_panel_word_filters", w, r, &user, &pi) {
return nil
@ -665,7 +610,7 @@ func routePanelWordFiltersEdit(w http.ResponseWriter, r *http.Request, user comm
_ = wfid
pi := common.PanelPage{"Edit Word Filter", user, headerVars, stats, tList, nil}
pi := common.PanelPage{common.GetTitlePhrase("panel-edit-word-filter"), user, headerVars, stats, tList, nil}
if common.PreRenderHooks["pre_render_panel_word_filters_edit"] != nil {
if common.RunPreRenderHook("pre_render_panel_word_filters_edit", w, r, &user, &pi) {
return nil
@ -758,7 +703,7 @@ func routePanelPlugins(w http.ResponseWriter, r *http.Request, user common.User)
pluginList = append(pluginList, plugin)
}
pi := common.PanelPage{"Plugin Manager", user, headerVars, stats, pluginList, nil}
pi := common.PanelPage{common.GetTitlePhrase("panel-plugins"), user, headerVars, stats, pluginList, nil}
if common.PreRenderHooks["pre_render_panel_plugins"] != nil {
if common.RunPreRenderHook("pre_render_panel_plugins", w, r, &user, &pi) {
return nil
@ -966,8 +911,8 @@ func routePanelUsers(w http.ResponseWriter, r *http.Request, user common.User) c
puser.Avatar = strings.Replace(common.Config.Noavatar, "{id}", strconv.Itoa(puser.ID), 1)
}
if common.Gstore.DirtyGet(puser.Group).Tag != "" {
puser.Tag = common.Gstore.DirtyGet(puser.Group).Tag
if common.Groups.DirtyGet(puser.Group).Tag != "" {
puser.Tag = common.Groups.DirtyGet(puser.Group).Tag
} else {
puser.Tag = ""
}
@ -979,7 +924,7 @@ func routePanelUsers(w http.ResponseWriter, r *http.Request, user common.User) c
}
pageList := common.Paginate(stats.Users, perPage, 5)
pi := common.PanelUserPage{"User Manager", user, headerVars, stats, userList, pageList, page, lastPage}
pi := common.PanelUserPage{common.GetTitlePhrase("panel-users"), user, headerVars, stats, userList, pageList, page, lastPage}
if common.PreRenderHooks["pre_render_panel_users"] != nil {
if common.RunPreRenderHook("pre_render_panel_users", w, r, &user, &pi) {
return nil
@ -1018,7 +963,7 @@ func routePanelUsersEdit(w http.ResponseWriter, r *http.Request, user common.Use
}
// ? - Should we stop admins from deleting all the groups? Maybe, protect the group they're currently using?
groups, err := common.Gstore.GetRange(1, 0) // ? - 0 = Go to the end
groups, err := common.Groups.GetRange(1, 0) // ? - 0 = Go to the end
if err != nil {
return common.InternalError(err, w, r)
}
@ -1034,7 +979,7 @@ func routePanelUsersEdit(w http.ResponseWriter, r *http.Request, user common.Use
groupList = append(groupList, group)
}
pi := common.PanelPage{"User Editor", user, headerVars, stats, groupList, targetUser}
pi := common.PanelPage{common.GetTitlePhrase("panel-edit-user"), user, headerVars, stats, groupList, targetUser}
if common.PreRenderHooks["pre_render_panel_edit_user"] != nil {
if common.RunPreRenderHook("pre_render_panel_edit_user", w, r, &user, &pi) {
return nil
@ -1095,7 +1040,7 @@ func routePanelUsersEditSubmit(w http.ResponseWriter, r *http.Request, user comm
return common.LocalError("You need to provide a whole number for the group ID", w, r, user)
}
group, err := common.Gstore.Get(newgroup)
group, err := common.Groups.Get(newgroup)
if err == ErrNoRows {
return common.LocalError("The group you're trying to place this user in doesn't exist.", w, r, user)
} else if err != nil {
@ -1109,6 +1054,7 @@ func routePanelUsersEditSubmit(w http.ResponseWriter, r *http.Request, user comm
return common.LocalError("You need the EditUserGroupSuperMod permission to assign someone to a super mod group.", w, r, user)
}
// TODO: Move this query into common
_, err = stmts.updateUser.Exec(newname, newemail, newgroup, targetUser.ID)
if err != nil {
return common.InternalError(err, w, r)
@ -1138,7 +1084,7 @@ func routePanelGroups(w http.ResponseWriter, r *http.Request, user common.User)
var count int
var groupList []common.GroupAdmin
groups, _ := common.Gstore.GetRange(offset, 0)
groups, _ := common.Groups.GetRange(offset, 0)
for _, group := range groups {
if count == perPage {
break
@ -1149,6 +1095,7 @@ func routePanelGroups(w http.ResponseWriter, r *http.Request, user common.User)
var canEdit bool
var canDelete = false
// TODO: Use a switch for this
if group.IsAdmin {
rank = "Admin"
rankClass = "admin"
@ -1173,7 +1120,7 @@ func routePanelGroups(w http.ResponseWriter, r *http.Request, user common.User)
//log.Printf("groupList: %+v\n", groupList)
pageList := common.Paginate(stats.Groups, perPage, 5)
pi := common.PanelGroupPage{"Group Manager", user, headerVars, stats, groupList, pageList, page, lastPage}
pi := common.PanelGroupPage{common.GetTitlePhrase("panel-groups"), user, headerVars, stats, groupList, pageList, page, lastPage}
if common.PreRenderHooks["pre_render_panel_groups"] != nil {
if common.RunPreRenderHook("pre_render_panel_groups", w, r, &user, &pi) {
return nil
@ -1201,7 +1148,7 @@ func routePanelGroupsEdit(w http.ResponseWriter, r *http.Request, user common.Us
return common.LocalError("You need to provide a whole number for the group ID", w, r, user)
}
group, err := common.Gstore.Get(gid)
group, err := common.Groups.Get(gid)
if err == ErrNoRows {
//log.Print("aaaaa monsters")
return common.NotFound(w, r)
@ -1232,7 +1179,7 @@ func routePanelGroupsEdit(w http.ResponseWriter, r *http.Request, user common.Us
disableRank := !user.Perms.EditGroupGlobalPerms || (group.ID == 6)
pi := common.PanelEditGroupPage{"Group Editor", user, headerVars, stats, group.ID, group.Name, group.Tag, rank, disableRank}
pi := common.PanelEditGroupPage{common.GetTitlePhrase("panel-edit-group"), user, headerVars, stats, group.ID, group.Name, group.Tag, rank, disableRank}
if common.PreRenderHooks["pre_render_panel_edit_group"] != nil {
if common.RunPreRenderHook("pre_render_panel_edit_group", w, r, &user, &pi) {
return nil
@ -1259,7 +1206,7 @@ func routePanelGroupsEditPerms(w http.ResponseWriter, r *http.Request, user comm
return common.LocalError("The Group ID is not a valid integer.", w, r, user)
}
group, err := common.Gstore.Get(gid)
group, err := common.Groups.Get(gid)
if err == ErrNoRows {
//log.Print("aaaaa monsters")
return common.NotFound(w, r)
@ -1310,7 +1257,7 @@ func routePanelGroupsEditPerms(w http.ResponseWriter, r *http.Request, user comm
globalPerms = append(globalPerms, common.NameLangToggle{"ViewIPs", common.GetGlobalPermPhrase("ViewIPs"), group.Perms.ViewIPs})
globalPerms = append(globalPerms, common.NameLangToggle{"UploadFiles", common.GetGlobalPermPhrase("UploadFiles"), group.Perms.UploadFiles})
pi := common.PanelEditGroupPermsPage{"Group Editor", user, headerVars, stats, group.ID, group.Name, localPerms, globalPerms}
pi := common.PanelEditGroupPermsPage{common.GetTitlePhrase("panel-edit-group"), user, headerVars, stats, group.ID, group.Name, localPerms, globalPerms}
if common.PreRenderHooks["pre_render_panel_edit_group_perms"] != nil {
if common.RunPreRenderHook("pre_render_panel_edit_group_perms", w, r, &user, &pi) {
return nil
@ -1337,7 +1284,7 @@ func routePanelGroupsEditSubmit(w http.ResponseWriter, r *http.Request, user com
return common.LocalError("You need to provide a whole number for the group ID", w, r, user)
}
group, err := common.Gstore.Get(gid)
group, err := common.Groups.Get(gid)
if err == ErrNoRows {
//log.Print("aaaaa monsters")
return common.NotFound(w, r)
@ -1360,6 +1307,7 @@ func routePanelGroupsEditSubmit(w http.ResponseWriter, r *http.Request, user com
rank := r.FormValue("group-type")
var originalRank string
// TODO: Use a switch for this
if group.IsAdmin {
originalRank = "Admin"
} else if group.IsMod {
@ -1407,7 +1355,7 @@ func routePanelGroupsEditSubmit(w http.ResponseWriter, r *http.Request, user com
if err != nil {
return common.InternalError(err, w, r)
}
common.Gstore.Reload(gid)
common.Groups.Reload(gid)
http.Redirect(w, r, "/panel/groups/edit/"+strconv.Itoa(gid), http.StatusSeeOther)
return nil
@ -1427,7 +1375,7 @@ func routePanelGroupsEditPermsSubmit(w http.ResponseWriter, r *http.Request, use
return common.LocalError("The Group ID is not a valid integer.", w, r, user)
}
group, err := common.Gstore.Get(gid)
group, err := common.Groups.Get(gid)
if err == ErrNoRows {
//log.Print("aaaaa monsters o.o")
return common.NotFound(w, r)
@ -1508,7 +1456,7 @@ func routePanelGroupsCreateSubmit(w http.ResponseWriter, r *http.Request, user c
}
}
gid, err := common.Gstore.Create(groupName, groupTag, isAdmin, isMod, isBanned)
gid, err := common.Groups.Create(groupName, groupTag, isAdmin, isMod, isBanned)
if err != nil {
return common.InternalError(err, w, r)
}
@ -1538,7 +1486,7 @@ func routePanelThemes(w http.ResponseWriter, r *http.Request, user common.User)
}
pi := common.PanelThemesPage{"Theme Manager", user, headerVars, stats, pThemeList, vThemeList}
pi := common.PanelThemesPage{common.GetTitlePhrase("panel-themes"), user, headerVars, stats, pThemeList, vThemeList}
if common.PreRenderHooks["pre_render_panel_themes"] != nil {
if common.RunPreRenderHook("pre_render_panel_themes", w, r, &user, &pi) {
return nil
@ -1658,7 +1606,7 @@ func routePanelBackups(w http.ResponseWriter, r *http.Request, user common.User,
backupList = append(backupList, common.BackupItem{backupFile.Name(), backupFile.ModTime()})
}
pi := common.PanelBackupPage{"Backups", user, headerVars, stats, backupList}
pi := common.PanelBackupPage{common.GetTitlePhrase("panel-backups"), user, headerVars, stats, backupList}
err = common.Templates.ExecuteTemplate(w, "panel-backups.html", pi)
if err != nil {
return common.InternalError(err, w, r)
@ -1672,12 +1620,7 @@ func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user common.User)
return ferr
}
var logCount int
err := stmts.modlogCount.QueryRow().Scan(&logCount)
if err != nil {
return common.InternalError(err, w, r)
}
logCount := common.ModLogs.GlobalCount()
page, _ := strconv.Atoi(r.FormValue("page"))
perPage := 10
offset, page, lastPage := common.PageOffset(logCount, page, perPage)
@ -1688,6 +1631,21 @@ func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user common.User)
}
defer rows.Close()
// TODO: Log errors when something really screwy is going on?
handleUnknownUser := func(user *common.User, err error) *common.User {
if err != nil {
return &common.User{Name: "Unknown", Link: common.BuildProfileURL("unknown", 0)}
}
return user
}
handleUnknownTopic := func(topic *common.Topic, err error) *common.Topic {
if err != nil {
return &common.Topic{Title: "Unknown", Link: common.BuildProfileURL("unknown", 0)}
}
return topic
}
var logs []common.LogItem
var action, elementType, ipaddress, doneAt string
var elementID, actorID int
@ -1697,68 +1655,41 @@ func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user common.User)
return common.InternalError(err, w, r)
}
actor, err := common.Users.Get(actorID)
if err != nil {
actor = &common.User{Name: "Unknown", Link: common.BuildProfileURL("unknown", 0)}
}
actor := handleUnknownUser(common.Users.Get(actorID))
switch action {
case "lock":
topic, err := common.Topics.Get(elementID)
if err != nil {
topic = &common.Topic{Title: "Unknown", Link: common.BuildProfileURL("unknown", 0)}
}
action = "<a href='" + topic.Link + "'>" + topic.Title + "</a> was locked by <a href='" + actor.Link + "'>" + actor.Name + "</a>"
topic := handleUnknownTopic(common.Topics.Get(elementID))
action = fmt.Sprintf("<a href='%s'>%s</a> was locked by <a href='%s'>%s</a>", topic.Link, topic.Title, actor.Link, actor.Name)
case "unlock":
topic, err := common.Topics.Get(elementID)
if err != nil {
topic = &common.Topic{Title: "Unknown", Link: common.BuildProfileURL("unknown", 0)}
}
action = "<a href='" + topic.Link + "'>" + topic.Title + "</a> was reopened by <a href='" + actor.Link + "'>" + actor.Name + "</a>"
topic := handleUnknownTopic(common.Topics.Get(elementID))
action = fmt.Sprintf("<a href='%s'>%s</a> was reopened by <a href='%s'>%s</a>", topic.Link, topic.Title, actor.Link, actor.Name)
case "stick":
topic, err := common.Topics.Get(elementID)
if err != nil {
topic = &common.Topic{Title: "Unknown", Link: common.BuildProfileURL("unknown", 0)}
}
action = "<a href='" + topic.Link + "'>" + topic.Title + "</a> was pinned by <a href='" + actor.Link + "'>" + actor.Name + "</a>"
topic := handleUnknownTopic(common.Topics.Get(elementID))
action = fmt.Sprintf("<a href='%s'>%s</a> was pinned by <a href='%s'>%s</a>", topic.Link, topic.Title, actor.Link, actor.Name)
case "unstick":
topic, err := common.Topics.Get(elementID)
if err != nil {
topic = &common.Topic{Title: "Unknown", Link: common.BuildProfileURL("unknown", 0)}
}
action = "<a href='" + topic.Link + "'>" + topic.Title + "</a> was unpinned by <a href='" + actor.Link + "'>" + actor.Name + "</a>"
topic := handleUnknownTopic(common.Topics.Get(elementID))
action = fmt.Sprintf("<a href='%s'>%s</a> was unpinned by <a href='%s'>%s</a>", topic.Link, topic.Title, actor.Link, actor.Name)
case "delete":
if elementType == "topic" {
action = "Topic #" + strconv.Itoa(elementID) + " was deleted by <a href='" + actor.Link + "'>" + actor.Name + "</a>"
action = fmt.Sprintf("Topic #%d was deleted by <a href='%s'>%s</a>", elementID, actor.Link, actor.Name)
} else {
reply := common.BlankReply()
reply.ID = elementID
topic, err := reply.Topic()
if err != nil {
topic = &common.Topic{Title: "Unknown", Link: common.BuildProfileURL("unknown", 0)}
}
action = "A reply in <a href='" + topic.Link + "'>" + topic.Title + "</a> was deleted by <a href='" + actor.Link + "'>" + actor.Name + "</a>"
topic := handleUnknownTopic(reply.Topic())
action = fmt.Sprintf("A reply in <a href='%s'>%s</a> was deleted by <a href='%s'>%s</a>", topic.Link, topic.Title, actor.Link, actor.Name)
}
case "ban":
targetUser, err := common.Users.Get(elementID)
if err != nil {
targetUser = &common.User{Name: "Unknown", Link: common.BuildProfileURL("unknown", 0)}
}
action = "<a href='" + targetUser.Link + "'>" + targetUser.Name + "</a> was banned by <a href='" + actor.Link + "'>" + actor.Name + "</a>"
targetUser := handleUnknownUser(common.Users.Get(elementID))
action = fmt.Sprintf("<a href='%s'>%s</a> was banned by <a href='%s'>%s</a>", targetUser.Link, targetUser.Name, actor.Link, actor.Name)
case "unban":
targetUser, err := common.Users.Get(elementID)
if err != nil {
targetUser = &common.User{Name: "Unknown", Link: common.BuildProfileURL("unknown", 0)}
}
action = "<a href='" + targetUser.Link + "'>" + targetUser.Name + "</a> was unbanned by <a href='" + actor.Link + "'>" + actor.Name + "</a>"
targetUser := handleUnknownUser(common.Users.Get(elementID))
action = fmt.Sprintf("<a href='%s'>%s</a> was unbanned by <a href='%s'>%s</a>", targetUser.Link, targetUser.Name, actor.Link, actor.Name)
case "activate":
targetUser, err := common.Users.Get(elementID)
if err != nil {
targetUser = &common.User{Name: "Unknown", Link: common.BuildProfileURL("unknown", 0)}
}
action = "<a href='" + targetUser.Link + "'>" + targetUser.Name + "</a> was activated by <a href='" + actor.Link + "'>" + actor.Name + "</a>"
targetUser := handleUnknownUser(common.Users.Get(elementID))
action = fmt.Sprintf("<a href='%s'>%s</a> was activated by <a href='%s'>%s</a>", targetUser.Link, targetUser.Name, actor.Link, actor.Name)
default:
action = "Unknown action '" + action + "' by <a href='" + actor.Link + "'>" + actor.Name + "</a>"
action = fmt.Sprintf("Unknown action '%s' by <a href='%s'>%s</a>", action, actor.Link, actor.Name)
}
logs = append(logs, common.LogItem{Action: template.HTML(action), IPAddress: ipaddress, DoneAt: doneAt})
}
@ -1768,7 +1699,7 @@ func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user common.User)
}
pageList := common.Paginate(logCount, perPage, 5)
pi := common.PanelLogsPage{"Moderation Logs", user, headerVars, stats, logs, pageList, page, lastPage}
pi := common.PanelLogsPage{common.GetTitlePhrase("panel-mod-logs"), user, headerVars, stats, logs, pageList, page, lastPage}
if common.PreRenderHooks["pre_render_panel_mod_log"] != nil {
if common.RunPreRenderHook("pre_render_panel_mod_log", w, r, &user, &pi) {
return nil
@ -1792,7 +1723,7 @@ func routePanelDebug(w http.ResponseWriter, r *http.Request, user common.User) c
openConnCount := dbStats.OpenConnections
// Disk I/O?
pi := common.PanelDebugPage{"Debug", user, headerVars, stats, uptime, openConnCount, dbAdapter}
pi := common.PanelDebugPage{common.GetTitlePhrase("panel-debug"), user, headerVars, stats, uptime, openConnCount, dbAdapter}
err := common.Templates.ExecuteTemplate(w, "panel-debug.html", pi)
if err != nil {
return common.InternalError(err, w, r)

View File

@ -33,20 +33,21 @@ func initGuilds() (err error) {
router.HandleFunc("/guild/create/submit/", guilds.RouteCreateGuildSubmit)
router.HandleFunc("/guild/members/", guilds.RouteMemberList)
guilds.Gstore, err = guilds.NewSQLGuildStore()
if err != nil {
return err
}
acc := qgen.Builder.Accumulator()
guilds.ListStmt = acc.Select("guilds").Columns("guildID, name, desc, active, privacy, joinable, owner, memberCount, createdAt, lastUpdateTime").Prepare()
guilds.GetGuildStmt = acc.Select("guilds").Columns("name, desc, active, privacy, joinable, owner, memberCount, mainForum, backdrop, createdAt, lastUpdateTime").Where("guildID = ?").Prepare()
guilds.MemberListStmt = acc.Select("guilds_members").Columns("guildID, uid, rank, posts, joinedAt").Prepare()
guilds.MemberListJoinStmt = acc.SimpleLeftJoin("guilds_members", "users", "users.uid, guilds_members.rank, guilds_members.posts, guilds_members.joinedAt, users.name, users.avatar", "guilds_members.uid = users.uid", "guilds_members.guildID = ?", "guilds_members.rank DESC, guilds_members.joinedat ASC", "")
guilds.GetMemberStmt = acc.Select("guilds_members").Columns("rank, posts, joinedAt").Where("guildID = ? AND uid = ?").Prepare()
guilds.CreateGuildStmt = acc.Insert("guilds").Columns("name, desc, active, privacy, joinable, owner, memberCount, mainForum, backdrop, createdAt, lastUpdateTime").Fields("?,?,?,?,1,?,1,?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP()").Prepare()
guilds.AttachForumStmt = acc.Update("forums").Set("parentID = ?, parentType = 'guild'").Where("fid = ?").Prepare()
guilds.UnattachForumStmt = acc.Update("forums").Set("parentID = 0, parentType = ''").Where("fid = ?").Prepare()
@ -72,8 +73,6 @@ func deactivateGuilds() {
_ = guilds.MemberListStmt.Close()
_ = guilds.MemberListJoinStmt.Close()
_ = guilds.GetMemberStmt.Close()
_ = guilds.GetGuildStmt.Close()
_ = guilds.CreateGuildStmt.Close()
_ = guilds.AttachForumStmt.Close()
_ = guilds.UnattachForumStmt.Close()
_ = guilds.AddMemberStmt.Close()

View File

@ -1,4 +1,3 @@
/* WIP Under Construction */
package qgen
var Install *installer
@ -13,11 +12,21 @@ type DB_Install_Instruction struct {
Type string
}
// TODO: Add methods to this to construct it OO-like
type DB_Install_Table struct {
Name string
Charset string
Collation string
Columns []DBTableColumn
Keys []DBTableKey
}
// A set of wrappers around the generator methods, so we can use this in the installer
// TODO: Re-implement the query generation, query builder and installer adapters as layers on-top of a query text adapter
type installer struct {
adapter Adapter
instructions []DB_Install_Instruction
tables []*DB_Install_Table // TODO: Use this in Record() in the next commit to allow us to auto-migrate settings rather than manually patching them in on upgrade
plugins []QueryPlugin
}
@ -26,8 +35,7 @@ func (install *installer) SetAdapter(name string) error {
if err != nil {
return err
}
install.adapter = adap
install.instructions = []DB_Install_Instruction{}
install.SetAdapterInstance(adap)
return nil
}
@ -36,49 +44,54 @@ func (install *installer) SetAdapterInstance(adapter Adapter) {
install.instructions = []DB_Install_Instruction{}
}
func (install *installer) RegisterPlugin(plugin QueryPlugin) {
install.plugins = append(install.plugins, plugin)
func (install *installer) AddPlugins(plugins ...QueryPlugin) {
install.plugins = append(install.plugins, plugins...)
}
func (install *installer) CreateTable(table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) error {
for _, plugin := range install.plugins {
err := plugin.Hook("CreateTableStart", table, charset, collation, columns, keys)
tableStruct := &DB_Install_Table{table, charset, collation, columns, keys}
err := install.RunHook("CreateTableStart", tableStruct)
if err != nil {
return err
}
}
res, err := install.adapter.CreateTable("_installer", table, charset, collation, columns, keys)
if err != nil {
return err
}
for _, plugin := range install.plugins {
err := plugin.Hook("CreateTableAfter", table, charset, collation, columns, keys, res)
err = install.RunHook("CreateTableAfter", tableStruct)
if err != nil {
return err
}
}
install.instructions = append(install.instructions, DB_Install_Instruction{table, res, "create-table"})
install.tables = append(install.tables, tableStruct)
return nil
}
// TODO: Let plugins manipulate the parameters like in CreateTable
func (install *installer) SimpleInsert(table string, columns string, fields string) error {
for _, plugin := range install.plugins {
err := plugin.Hook("SimpleInsertStart", table, columns, fields)
err := install.RunHook("SimpleInsertStart", table, columns, fields)
if err != nil {
return err
}
}
res, err := install.adapter.SimpleInsert("_installer", table, columns, fields)
if err != nil {
return err
}
err = install.RunHook("SimpleInsertAfter", table, columns, fields, res)
if err != nil {
return err
}
install.instructions = append(install.instructions, DB_Install_Instruction{table, res, "insert"})
return nil
}
func (install *installer) RunHook(name string, args ...interface{}) error {
for _, plugin := range install.plugins {
err := plugin.Hook("SimpleInsertAfter", table, columns, fields, res)
err := plugin.Hook(name, args...)
if err != nil {
return err
}
}
install.instructions = append(install.instructions, DB_Install_Instruction{table, res, "insert"})
return nil
}

View File

@ -26,7 +26,7 @@ func main() {
for _, adapter := range qgen.Registry {
log.Printf("Building the queries for the %s adapter", adapter.GetName())
qgen.Install.SetAdapterInstance(adapter)
qgen.Install.RegisterPlugin(NewPrimaryKeySpitter()) // TODO: Do we really need to fill the spitter for every adapter?
qgen.Install.AddPlugins(NewPrimaryKeySpitter()) // TODO: Do we really need to fill the spitter for every adapter?
err := writeStatements(adapter)
if err != nil {
@ -75,16 +75,6 @@ func writeStatements(adapter qgen.Adapter) error {
return err
}
/*err = writeReplaces(adapter)
if err != nil {
return err
}
err = writeUpserts(adapter)
if err != nil {
return err
}*/
err = writeUpdates(adapter)
if err != nil {
return err
@ -125,6 +115,8 @@ func seedTables(adapter qgen.Adapter) error {
qgen.Install.SimpleInsert("settings", "name, content, type, constraints", "'activation_type','1','list','1-3'")
qgen.Install.SimpleInsert("settings", "name, content, type", "'bigpost_min_words','250','int'")
qgen.Install.SimpleInsert("settings", "name, content, type", "'megapost_min_words','1000','int'")
qgen.Install.SimpleInsert("settings", "name, content, type", "'about_segment_title','','text'")
qgen.Install.SimpleInsert("settings", "name, content, type", "'about_segment_body','','text'")
qgen.Install.SimpleInsert("themes", "uname, default", "'tempra-simple',1")
qgen.Install.SimpleInsert("emails", "email, uid, validated", "'admin@localhost',1,1") // ? - Use a different default email or let the admin input it during installation?
@ -168,6 +160,7 @@ func seedTables(adapter qgen.Adapter) error {
CloseTopic
*/
// TODO: Set the permissions on a struct and then serialize the struct and insert that instead of writing raw JSON
qgen.Install.SimpleInsert("users_groups", "name, permissions, plugin_perms, is_mod, is_admin, tag", `'Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditUserGroupAdmin":false,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"EditGroupAdmin":false,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true}','{}',1,1,"Admin"`)
qgen.Install.SimpleInsert("users_groups", "name, permissions, plugin_perms, is_mod, tag", `'Moderator','{"BanUsers":true,"ActivateUsers":false,"EditUser":true,"EditUserEmail":false,"EditUserGroup":true,"ViewIPs":true,"UploadFiles":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true}','{}',1,"Mod"`)
@ -226,8 +219,6 @@ func writeSelects(adapter qgen.Adapter) error {
build.Select("getPassword").Table("users").Columns("password, salt").Where("uid = ?").Parse()
build.Select("getSettings").Table("settings").Columns("name, content, type").Parse()
build.Select("isPluginActive").Table("plugins").Columns("active").Where("uname = ?").Parse()
//build.Select("isPluginInstalled").Table("plugins").Columns("installed").Where("uname = ?").Parse()
@ -314,25 +305,6 @@ func writeInserts(adapter qgen.Adapter) error {
return nil
}
func writeReplaces(adapter qgen.Adapter) (err error) {
return nil
}
// ! Upserts are broken atm
/*func writeUpserts(adapter qgen.Adapter) (err error) {
_, err = adapter.SimpleUpsert("addForumPermsToGroup", "forums_permissions", "gid, fid, preset, permissions", "?,?,?,?", "gid = ? AND fid = ?")
if err != nil {
return err
}
_, err = adapter.SimpleUpsert("replaceScheduleGroup", "users_groups_scheduler", "uid, set_group, issued_by, issued_at, revert_at, temporary", "?,?,?,UTC_TIMESTAMP(),?,?", "uid = ?")
if err != nil {
return err
}
return nil
}*/
func writeUpdates(adapter qgen.Adapter) error {
build := adapter.Builder()
@ -340,8 +312,6 @@ func writeUpdates(adapter qgen.Adapter) error {
build.Update("editProfileReply").Table("users_replies").Set("content = ?, parsed_content = ?").Where("rid = ?").Parse()
build.Update("updateSetting").Table("settings").Set("content = ?").Where("name = ?").Parse()
build.Update("updatePlugin").Table("plugins").Set("active = ?").Where("uname = ?").Parse()
build.Update("updatePluginInstall").Table("plugins").Set("installed = ?").Where("uname = ?").Parse()
@ -385,8 +355,6 @@ func writeDeletes(adapter qgen.Adapter) error {
func writeSimpleCounts(adapter qgen.Adapter) error {
adapter.SimpleCount("reportExists", "topics", "data = ? AND data != '' AND parentID = 1", "")
adapter.SimpleCount("modlogCount", "moderation_logs", "", "")
return nil
}

View File

@ -14,7 +14,8 @@ func NewPrimaryKeySpitter() *PrimaryKeySpitter {
func (spit *PrimaryKeySpitter) Hook(name string, args ...interface{}) error {
if name == "CreateTableStart" {
var found string
for _, key := range args[4].([]qgen.DBTableKey) {
var table = args[0].(*qgen.DB_Install_Table)
for _, key := range table.Keys {
if key.Type == "primary" {
expl := strings.Split(key.Columns, ",")
if len(expl) > 1 {
@ -23,7 +24,7 @@ func (spit *PrimaryKeySpitter) Hook(name string, args ...interface{}) error {
found = key.Columns
}
if found != "" {
table := args[0].(string)
table := table.Name
spit.keys[table] = found
}
}

View File

@ -68,8 +68,8 @@ func buildPanelRoutes() {
Action("routePanelForumsEditPermsSubmit", "/panel/forums/edit/perms/submit/", "extra_data"),
View("routePanelSettings", "/panel/settings/"),
View("routePanelSetting", "/panel/settings/edit/", "extra_data"),
Action("routePanelSettingEdit", "/panel/settings/edit/submit/", "extra_data"),
View("routePanelSettingEdit", "/panel/settings/edit/", "extra_data"),
Action("routePanelSettingEditSubmit", "/panel/settings/edit/submit/", "extra_data"),
View("routePanelWordFilters", "/panel/settings/word-filters/"),
Action("routePanelWordFiltersCreate", "/panel/settings/word-filters/create/"),

View File

@ -46,7 +46,7 @@ func routeStatic(w http.ResponseWriter, r *http.Request) {
file, ok := common.StaticFiles[r.URL.Path]
if !ok {
if common.Dev.DebugMode {
log.Print("Failed to find '" + r.URL.Path + "'")
log.Printf("Failed to find '%s'", r.URL.Path)
}
w.WriteHeader(http.StatusNotFound)
return
@ -103,7 +103,7 @@ func routeOverview(w http.ResponseWriter, r *http.Request, user common.User) com
}
common.BuildWidgets("overview", nil, headerVars, r)
pi := common.Page{"Overview", user, headerVars, tList, nil}
pi := common.Page{common.GetTitlePhrase("overview"), user, headerVars, tList, nil}
if common.PreRenderHooks["pre_render_overview"] != nil {
if common.RunPreRenderHook("pre_render_overview", w, r, &user, &pi) {
return nil
@ -129,7 +129,7 @@ func routeCustomPage(w http.ResponseWriter, r *http.Request, user common.User) c
}
common.BuildWidgets("custom_page", name, headerVars, r)
pi := common.Page{"Page", user, headerVars, tList, nil}
pi := common.Page{common.GetTitlePhrase("page"), user, headerVars, tList, nil}
if common.PreRenderHooks["pre_render_custom_page"] != nil {
if common.RunPreRenderHook("pre_render_custom_page", w, r, &user, &pi) {
return nil
@ -152,7 +152,7 @@ func routeTopics(w http.ResponseWriter, r *http.Request, user common.User) commo
// TODO: Add a function for the qlist stuff
var qlist string
group, err := common.Gstore.Get(user.Group)
group, err := common.Groups.Get(user.Group)
if err != nil {
log.Printf("Group #%d doesn't exist despite being used by common.User #%d", user.Group, user.ID)
return common.LocalError("Something weird happened", w, r, user)
@ -161,7 +161,7 @@ func routeTopics(w http.ResponseWriter, r *http.Request, user common.User) commo
// TODO: Make CanSee a method on *Group with a canSee field?
var canSee []int
if user.IsSuperAdmin {
canSee, err = common.Fstore.GetAllVisibleIDs()
canSee, err = common.Forums.GetAllVisibleIDs()
if err != nil {
return common.InternalError(err, w, r)
}
@ -174,7 +174,7 @@ func routeTopics(w http.ResponseWriter, r *http.Request, user common.User) commo
var argList []interface{}
for _, fid := range canSee {
forum := common.Fstore.DirtyGet(fid)
forum := common.Forums.DirtyGet(fid)
if forum.Name != "" && forum.Active {
if forum.ParentType == "" || forum.ParentType == "forum" {
// Optimise Quick Topic away for guests
@ -251,7 +251,7 @@ func routeTopics(w http.ResponseWriter, r *http.Request, user common.User) commo
topicItem.Link = common.BuildTopicURL(common.NameToSlug(topicItem.Title), topicItem.ID)
forum := common.Fstore.DirtyGet(topicItem.ParentID)
forum := common.Forums.DirtyGet(topicItem.ParentID)
topicItem.ForumName = forum.Name
topicItem.ForumLink = forum.Link
@ -291,13 +291,13 @@ func routeTopics(w http.ResponseWriter, r *http.Request, user common.User) commo
topicItem.LastUser = userList[topicItem.LastReplyBy]
}
pi := common.TopicsPage{"All Topics", user, headerVars, topicList, forumList, common.Config.DefaultForum}
pi := common.TopicsPage{common.GetTitlePhrase("topics"), user, headerVars, topicList, forumList, common.Config.DefaultForum}
if common.PreRenderHooks["pre_render_topic_list"] != nil {
if common.RunPreRenderHook("pre_render_topic_list", w, r, &user, &pi) {
return nil
}
}
err = common.RunThemeTemplate(headerVars.ThemeName, "topics", pi, w)
err = common.RunThemeTemplate(headerVars.Theme.Name, "topics", pi, w)
if err != nil {
return common.InternalError(err, w, r)
}
@ -327,7 +327,7 @@ func routeForum(w http.ResponseWriter, r *http.Request, user common.User, sfid s
}
// TODO: Fix this double-check
forum, err := common.Fstore.Get(fid)
forum, err := common.Forums.Get(fid)
if err == ErrNoRows {
return common.NotFound(w, r)
} else if err != nil {
@ -408,7 +408,7 @@ func routeForum(w http.ResponseWriter, r *http.Request, user common.User, sfid s
return nil
}
}
err = common.RunThemeTemplate(headerVars.ThemeName, "forum", pi, w)
err = common.RunThemeTemplate(headerVars.Theme.Name, "forum", pi, w)
if err != nil {
return common.InternalError(err, w, r)
}
@ -426,13 +426,12 @@ func routeForums(w http.ResponseWriter, r *http.Request, user common.User) commo
var forumList []common.Forum
var canSee []int
if user.IsSuperAdmin {
canSee, err = common.Fstore.GetAllVisibleIDs()
canSee, err = common.Forums.GetAllVisibleIDs()
if err != nil {
return common.InternalError(err, w, r)
}
//log.Print("canSee ", canSee)
} else {
group, err := common.Gstore.Get(user.Group)
group, err := common.Groups.Get(user.Group)
if err != nil {
log.Printf("Group #%d doesn't exist despite being used by common.User #%d", user.Group, user.ID)
return common.LocalError("Something weird happened", w, r, user)
@ -442,7 +441,7 @@ func routeForums(w http.ResponseWriter, r *http.Request, user common.User) commo
for _, fid := range canSee {
// Avoid data races by copying the struct into something we can freely mold without worrying about breaking something somewhere else
var forum = common.Fstore.DirtyGet(fid).Copy()
var forum = common.Forums.DirtyGet(fid).Copy()
if forum.ParentID == 0 && forum.Name != "" && forum.Active {
if forum.LastTopicID != 0 {
if forum.LastTopic.ID != 0 && forum.LastReplyer.ID != 0 {
@ -460,13 +459,13 @@ func routeForums(w http.ResponseWriter, r *http.Request, user common.User) commo
}
}
pi := common.ForumsPage{"Forum List", user, headerVars, forumList}
pi := common.ForumsPage{common.GetTitlePhrase("forums"), user, headerVars, forumList}
if common.PreRenderHooks["pre_render_forum_list"] != nil {
if common.RunPreRenderHook("pre_render_forum_list", w, r, &user, &pi) {
return nil
}
}
err = common.RunThemeTemplate(headerVars.ThemeName, "forums", pi, w)
err = common.RunThemeTemplate(headerVars.Theme.Name, "forums", pi, w)
if err != nil {
return common.InternalError(err, w, r)
}
@ -481,6 +480,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user common.User) comm
page, _ = strconv.Atoi(r.FormValue("page"))
// SEO URLs...
// TODO: Make a shared function for this
halves := strings.Split(r.URL.Path[len("/topic/"):], ".")
if len(halves) < 2 {
halves = append(halves, halves[0])
@ -520,7 +520,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user common.User) comm
user.Perms.CreateReply = false
}
postGroup, err := common.Gstore.Get(topic.Group)
postGroup, err := common.Groups.Get(topic.Group)
if err != nil {
return common.InternalError(err, w, r)
}
@ -574,7 +574,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user common.User) comm
replyItem.ContentHtml = common.ParseMessage(replyItem.Content, topic.ParentID, "forums")
replyItem.ContentLines = strings.Count(replyItem.Content, "\n")
postGroup, err = common.Gstore.Get(replyItem.Group)
postGroup, err = common.Groups.Get(replyItem.Group)
if err != nil {
return common.InternalError(err, w, r)
}
@ -635,7 +635,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user common.User) comm
return nil
}
}
err = common.RunThemeTemplate(headerVars.ThemeName, "topic", tpage, w)
err = common.RunThemeTemplate(headerVars.Theme.Name, "topic", tpage, w)
if err != nil {
return common.InternalError(err, w, r)
}
@ -671,6 +671,7 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user common.User) comm
puser = &user
} else {
// Fetch the user data
// TODO: Add a shared function for checking for ErrNoRows and internal erroring if it's not that case?
puser, err = common.Users.Get(pid)
if err == ErrNoRows {
return common.NotFound(w, r)
@ -692,7 +693,7 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user common.User) comm
return common.InternalError(err, w, r)
}
group, err := common.Gstore.Get(replyGroup)
group, err := common.Groups.Get(replyGroup)
if err != nil {
return common.InternalError(err, w, r)
}
@ -733,6 +734,7 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user common.User) comm
return common.InternalError(err, w, r)
}
// TODO: Add a phrase for this title
ppage := common.ProfilePage{puser.Name + "'s Profile", user, headerVars, replyList, *puser}
if common.PreRenderHooks["pre_render_profile"] != nil {
if common.RunPreRenderHook("pre_render_profile", w, r, &user, &ppage) {
@ -740,7 +742,7 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user common.User) comm
}
}
err = common.RunThemeTemplate(headerVars.ThemeName, "profile", ppage, w)
err = common.RunThemeTemplate(headerVars.Theme.Name, "profile", ppage, w)
if err != nil {
return common.InternalError(err, w, r)
}
@ -755,7 +757,7 @@ func routeLogin(w http.ResponseWriter, r *http.Request, user common.User) common
if user.Loggedin {
return common.LocalError("You're already logged in.", w, r, user)
}
pi := common.Page{"Login", user, headerVars, tList, nil}
pi := common.Page{common.GetTitlePhrase("login"), user, headerVars, tList, nil}
if common.PreRenderHooks["pre_render_login"] != nil {
if common.RunPreRenderHook("pre_render_login", w, r, &user, &pi) {
return nil
@ -803,7 +805,7 @@ func routeLoginSubmit(w http.ResponseWriter, r *http.Request, user common.User)
common.Auth.SetCookies(w, uid, session)
if user.IsAdmin {
// Is this error check reundant? We already check for the error in PreRoute for the same IP
// Is this error check redundant? We already check for the error in PreRoute for the same IP
// TODO: Should we be logging this?
log.Printf("#%d has logged in with IP %s", uid, user.LastIP)
}
@ -819,7 +821,7 @@ func routeRegister(w http.ResponseWriter, r *http.Request, user common.User) com
if user.Loggedin {
return common.LocalError("You're already logged in.", w, r, user)
}
pi := common.Page{"Registration", user, headerVars, tList, nil}
pi := common.Page{common.GetTitlePhrase("register"), user, headerVars, tList, nil}
if common.PreRenderHooks["pre_render_register"] != nil {
if common.RunPreRenderHook("pre_render_register", w, r, &user, &pi) {
return nil
@ -937,10 +939,12 @@ func routeChangeTheme(w http.ResponseWriter, r *http.Request, user common.User)
return nil
}
// TODO: We don't need support XML here to support sitemaps, we could handle those elsewhere
// TODO: Refactor this
var phraseLoginAlerts = []byte(`{"msgs":[{"msg":"Login to see your alerts","path":"/accounts/login"}]}`)
// TODO: Refactor this endpoint
func routeAPI(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 {
@ -1008,11 +1012,6 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.R
msglist = msglist[0 : len(msglist)-1]
}
_, _ = w.Write([]byte(`{"msgs":[` + msglist + `],"msgCount":` + strconv.Itoa(msgCount) + `}`))
//log.Print(`{"msgs":[` + msglist + `],"msgCount":` + strconv.Itoa(msgCount) + `}`)
//case "topics":
//case "forums":
//case "users":
//case "pages":
default:
return common.PreErrorJS("Invalid Module", w, r)
}

View File

@ -3,6 +3,8 @@ INSERT INTO [settings] ([name],[content],[type]) VALUES ('url_tags','1','bool');
INSERT INTO [settings] ([name],[content],[type],[constraints]) VALUES ('activation_type','1','list','1-3');
INSERT INTO [settings] ([name],[content],[type]) VALUES ('bigpost_min_words','250','int');
INSERT INTO [settings] ([name],[content],[type]) VALUES ('megapost_min_words','1000','int');
INSERT INTO [settings] ([name],[content],[type]) VALUES ('about_segment_title','','text');
INSERT INTO [settings] ([name],[content],[type]) VALUES ('about_segment_body','','text');
INSERT INTO [themes] ([uname],[default]) VALUES ('tempra-simple',1);
INSERT INTO [emails] ([email],[uid],[validated]) VALUES ('admin@localhost',1,1);
INSERT INTO [users_groups] ([name],[permissions],[plugin_perms],[is_mod],[is_admin],[tag]) VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditUserGroupAdmin":false,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"EditGroupAdmin":false,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true}','{}',1,1,'Admin');

View File

@ -3,6 +3,8 @@ INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('url_tags','1','bool');
INSERT INTO `settings`(`name`,`content`,`type`,`constraints`) VALUES ('activation_type','1','list','1-3');
INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('bigpost_min_words','250','int');
INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('megapost_min_words','1000','int');
INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('about_segment_title','','text');
INSERT INTO `settings`(`name`,`content`,`type`) VALUES ('about_segment_body','','text');
INSERT INTO `themes`(`uname`,`default`) VALUES ('tempra-simple',1);
INSERT INTO `emails`(`email`,`uid`,`validated`) VALUES ('admin@localhost',1,1);
INSERT INTO `users_groups`(`name`,`permissions`,`plugin_perms`,`is_mod`,`is_admin`,`tag`) VALUES ('Administrator','{"BanUsers":true,"ActivateUsers":true,"EditUser":true,"EditUserEmail":true,"EditUserPassword":true,"EditUserGroup":true,"EditUserGroupSuperMod":true,"EditUserGroupAdmin":false,"EditGroup":true,"EditGroupLocalPerms":true,"EditGroupGlobalPerms":true,"EditGroupSuperMod":true,"EditGroupAdmin":false,"ManageForums":true,"EditSettings":true,"ManageThemes":true,"ManagePlugins":true,"ViewAdminLogs":true,"ViewIPs":true,"UploadFiles":true,"ViewTopic":true,"LikeItem":true,"CreateTopic":true,"EditTopic":true,"DeleteTopic":true,"CreateReply":true,"EditReply":true,"DeleteReply":true,"PinTopic":true,"CloseTopic":true}','{}',1,1,'Admin');

View File

@ -27,3 +27,5 @@
;
;
;
;
;

View File

@ -22,7 +22,7 @@ w.Write([]byte(tmpl_forum_vars.Title))
w.Write(header_1)
w.Write([]byte(tmpl_forum_vars.Header.Site.Name))
w.Write(header_2)
w.Write([]byte(tmpl_forum_vars.Header.ThemeName))
w.Write([]byte(tmpl_forum_vars.Header.Theme.Name))
w.Write(header_3)
if len(tmpl_forum_vars.Header.Stylesheets) != 0 {
for _, item := range tmpl_forum_vars.Header.Stylesheets {
@ -203,28 +203,37 @@ w.Write(forum_57)
w.Write(forum_58)
}
w.Write(forum_59)
if tmpl_forum_vars.Header.Theme.AboutSegment {
w.Write(footer_0)
dispInt := tmpl_forum_vars.Header.Settings["about_segment_title"]
w.Write([]byte(dispInt.(string)))
w.Write(footer_1)
dispInt = tmpl_forum_vars.Header.Settings["about_segment_body"]
w.Write([]byte(dispInt.(string)))
w.Write(footer_2)
}
w.Write(footer_3)
if len(tmpl_forum_vars.Header.Themes) != 0 {
for _, item := range tmpl_forum_vars.Header.Themes {
if !item.HideFromThemes {
w.Write(footer_1)
w.Write([]byte(item.Name))
w.Write(footer_2)
if tmpl_forum_vars.Header.ThemeName == item.Name {
w.Write(footer_3)
}
w.Write(footer_4)
w.Write([]byte(item.FriendlyName))
w.Write([]byte(item.Name))
w.Write(footer_5)
}
}
}
if tmpl_forum_vars.Header.Theme.Name == item.Name {
w.Write(footer_6)
if tmpl_forum_vars.Header.Widgets.RightSidebar != "" {
}
w.Write(footer_7)
w.Write([]byte(string(tmpl_forum_vars.Header.Widgets.RightSidebar)))
w.Write([]byte(item.FriendlyName))
w.Write(footer_8)
}
}
}
w.Write(footer_9)
if tmpl_forum_vars.Header.Widgets.RightSidebar != "" {
w.Write(footer_10)
w.Write([]byte(string(tmpl_forum_vars.Header.Widgets.RightSidebar)))
w.Write(footer_11)
}
w.Write(footer_12)
return nil
}

View File

@ -21,7 +21,7 @@ w.Write([]byte(tmpl_forums_vars.Title))
w.Write(header_1)
w.Write([]byte(tmpl_forums_vars.Header.Site.Name))
w.Write(header_2)
w.Write([]byte(tmpl_forums_vars.Header.ThemeName))
w.Write([]byte(tmpl_forums_vars.Header.Theme.Name))
w.Write(header_3)
if len(tmpl_forums_vars.Header.Stylesheets) != 0 {
for _, item := range tmpl_forums_vars.Header.Stylesheets {
@ -118,28 +118,37 @@ w.Write(forums_18)
w.Write(forums_19)
}
w.Write(forums_20)
if tmpl_forums_vars.Header.Theme.AboutSegment {
w.Write(footer_0)
dispInt := tmpl_forums_vars.Header.Settings["about_segment_title"]
w.Write([]byte(dispInt.(string)))
w.Write(footer_1)
dispInt = tmpl_forums_vars.Header.Settings["about_segment_body"]
w.Write([]byte(dispInt.(string)))
w.Write(footer_2)
}
w.Write(footer_3)
if len(tmpl_forums_vars.Header.Themes) != 0 {
for _, item := range tmpl_forums_vars.Header.Themes {
if !item.HideFromThemes {
w.Write(footer_1)
w.Write([]byte(item.Name))
w.Write(footer_2)
if tmpl_forums_vars.Header.ThemeName == item.Name {
w.Write(footer_3)
}
w.Write(footer_4)
w.Write([]byte(item.FriendlyName))
w.Write([]byte(item.Name))
w.Write(footer_5)
}
}
}
if tmpl_forums_vars.Header.Theme.Name == item.Name {
w.Write(footer_6)
if tmpl_forums_vars.Header.Widgets.RightSidebar != "" {
}
w.Write(footer_7)
w.Write([]byte(string(tmpl_forums_vars.Header.Widgets.RightSidebar)))
w.Write([]byte(item.FriendlyName))
w.Write(footer_8)
}
}
}
w.Write(footer_9)
if tmpl_forums_vars.Header.Widgets.RightSidebar != "" {
w.Write(footer_10)
w.Write([]byte(string(tmpl_forums_vars.Header.Widgets.RightSidebar)))
w.Write(footer_11)
}
w.Write(footer_12)
return nil
}

View File

@ -20,7 +20,7 @@ w.Write([]byte(tmpl_guilds_guild_list_vars.Title))
w.Write(header_1)
w.Write([]byte(tmpl_guilds_guild_list_vars.Header.Site.Name))
w.Write(header_2)
w.Write([]byte(tmpl_guilds_guild_list_vars.Header.ThemeName))
w.Write([]byte(tmpl_guilds_guild_list_vars.Header.Theme.Name))
w.Write(header_3)
if len(tmpl_guilds_guild_list_vars.Header.Stylesheets) != 0 {
for _, item := range tmpl_guilds_guild_list_vars.Header.Stylesheets {
@ -91,28 +91,37 @@ w.Write(guilds_guild_list_6)
w.Write(guilds_guild_list_7)
}
w.Write(guilds_guild_list_8)
if tmpl_guilds_guild_list_vars.Header.Theme.AboutSegment {
w.Write(footer_0)
dispInt := tmpl_guilds_guild_list_vars.Header.Settings["about_segment_title"]
w.Write([]byte(dispInt.(string)))
w.Write(footer_1)
dispInt = tmpl_guilds_guild_list_vars.Header.Settings["about_segment_body"]
w.Write([]byte(dispInt.(string)))
w.Write(footer_2)
}
w.Write(footer_3)
if len(tmpl_guilds_guild_list_vars.Header.Themes) != 0 {
for _, item := range tmpl_guilds_guild_list_vars.Header.Themes {
if !item.HideFromThemes {
w.Write(footer_1)
w.Write([]byte(item.Name))
w.Write(footer_2)
if tmpl_guilds_guild_list_vars.Header.ThemeName == item.Name {
w.Write(footer_3)
}
w.Write(footer_4)
w.Write([]byte(item.FriendlyName))
w.Write([]byte(item.Name))
w.Write(footer_5)
}
}
}
if tmpl_guilds_guild_list_vars.Header.Theme.Name == item.Name {
w.Write(footer_6)
if tmpl_guilds_guild_list_vars.Header.Widgets.RightSidebar != "" {
}
w.Write(footer_7)
w.Write([]byte(string(tmpl_guilds_guild_list_vars.Header.Widgets.RightSidebar)))
w.Write([]byte(item.FriendlyName))
w.Write(footer_8)
}
}
}
w.Write(footer_9)
if tmpl_guilds_guild_list_vars.Header.Widgets.RightSidebar != "" {
w.Write(footer_10)
w.Write([]byte(string(tmpl_guilds_guild_list_vars.Header.Widgets.RightSidebar)))
w.Write(footer_11)
}
w.Write(footer_12)
return nil
}

View File

@ -264,27 +264,36 @@ var topic_100 = []byte(`
</main>
`)
var footer_0 = []byte(`<div class="footer">
<div id="poweredBy">Powered by Gosora - <span>Made with love by Azareal</span></div>
var footer_0 = []byte(`<div class="about">
<a id="aboutTitle">`)
var footer_1 = []byte(`</a>
<span id="aboutDesc">`)
var footer_2 = []byte(`</span>
</div>`)
var footer_3 = []byte(`
<div class="footer">
<div id="poweredBy">
<a id="poweredByName" href="https://github.com/Azareal/Gosora">Powered by Gosora</a><span id="poweredByDash"> - </span><span id="poweredByMaker">Made with love by Azareal</span>
</div>
<form action="/theme/" method="post">
<div id="themeSelector" style="float: right;">
<select id="themeSelectorSelect" name="themeSelector" aria-label="Change the site's appearance">
`)
var footer_1 = []byte(`<option val="`)
var footer_2 = []byte(`"`)
var footer_3 = []byte(` selected`)
var footer_4 = []byte(`>`)
var footer_5 = []byte(`</option>`)
var footer_6 = []byte(`
var footer_4 = []byte(`<option val="`)
var footer_5 = []byte(`"`)
var footer_6 = []byte(` selected`)
var footer_7 = []byte(`>`)
var footer_8 = []byte(`</option>`)
var footer_9 = []byte(`
</select>
</div>
</form>
</div>
</div>
`)
var footer_7 = []byte(`<aside class="sidebar">`)
var footer_8 = []byte(`</aside>`)
var footer_9 = []byte(`
var footer_10 = []byte(`<aside class="sidebar">`)
var footer_11 = []byte(`</aside>`)
var footer_12 = []byte(`
<div style="clear: both;"></div>
</div>
</div>
@ -498,7 +507,7 @@ var topic_alt_104 = []byte(`
`)
var profile_0 = []byte(`
<div id="profile_container">
<div id="profile_container" class="colstack">
<div id="profile_left_lane" class="colstack_left">
<!--<header class="colstack_item colstack_head rowhead">
@ -882,10 +891,11 @@ var forum_7 = []byte(`">&gt;</a></div>`)
var forum_8 = []byte(`
<main>
<div id="forum_head_block" class="rowblock rowhead topic_list_title_block">
<div id="forum_head_block" class="rowblock rowhead topic_list_title_block">
<div class="rowitem forum_title`)
var forum_9 = []byte(` has_opt`)
var forum_10 = []byte(`"><h1>`)
var forum_10 = []byte(`">
<h1>`)
var forum_11 = []byte(`</h1>
</div>
`)
@ -904,10 +914,10 @@ var forum_16 = []byte(`
<div style="clear: both;"></div>
`)
var forum_17 = []byte(`
</div>
`)
</div>
`)
var forum_18 = []byte(`
<div class="mod_floater auto_hide">
<div class="mod_floater auto_hide">
<form method="post">
<div class="mod_floater_head">
<span>What do you want to do with these 18 topics?</span>
@ -920,11 +930,10 @@ var forum_18 = []byte(`
<button>Run</button>
</div>
</form>
</div>
`)
</div>
`)
var forum_19 = []byte(`
<div id="forum_topic_create_form" class="rowblock topic_create_form quick_create_form" style="display: none;" aria-label="Quick Topic Form">
<div id="forum_topic_create_form" class="rowblock topic_create_form quick_create_form" style="display: none;" aria-label="Quick Topic Form">
<form id="topic_create_form_form" enctype="multipart/form-data" action="/topic/create/submit/" method="post"></form>
`)
var forum_20 = []byte(`<img class="little_row_avatar" src="`)
@ -958,10 +967,10 @@ var forum_25 = []byte(`
</div>
</div>
</div>
</div>
`)
</div>
`)
var forum_26 = []byte(`
<div id="forum_topic_list" class="rowblock topic_list">
<div id="forum_topic_list" class="rowblock topic_list">
`)
var forum_27 = []byte(`<div class="topic_row" data-tid="`)
var forum_28 = []byte(`">
@ -1017,8 +1026,7 @@ var forum_56 = []byte(` <a href="/topics/create/`)
var forum_57 = []byte(`">Start one?</a>`)
var forum_58 = []byte(`</div>`)
var forum_59 = []byte(`
</div>
</div>
</main>
`)
var guilds_guild_list_0 = []byte(`

View File

@ -3,9 +3,9 @@
// Code generated by Gosora. More below:
/* This file was automatically generated by the software. Please don't edit it as your changes may be overwritten at any moment. */
package main
import "net/http"
import "./common"
import "strconv"
import "net/http"
// nolint
func init() {
@ -22,7 +22,7 @@ w.Write([]byte(tmpl_profile_vars.Title))
w.Write(header_1)
w.Write([]byte(tmpl_profile_vars.Header.Site.Name))
w.Write(header_2)
w.Write([]byte(tmpl_profile_vars.Header.ThemeName))
w.Write([]byte(tmpl_profile_vars.Header.Theme.Name))
w.Write(header_3)
if len(tmpl_profile_vars.Header.Stylesheets) != 0 {
for _, item := range tmpl_profile_vars.Header.Stylesheets {
@ -161,28 +161,37 @@ w.Write(profile_41)
}
w.Write(profile_42)
w.Write(profile_43)
if tmpl_profile_vars.Header.Theme.AboutSegment {
w.Write(footer_0)
dispInt := tmpl_profile_vars.Header.Settings["about_segment_title"]
w.Write([]byte(dispInt.(string)))
w.Write(footer_1)
dispInt = tmpl_profile_vars.Header.Settings["about_segment_body"]
w.Write([]byte(dispInt.(string)))
w.Write(footer_2)
}
w.Write(footer_3)
if len(tmpl_profile_vars.Header.Themes) != 0 {
for _, item := range tmpl_profile_vars.Header.Themes {
if !item.HideFromThemes {
w.Write(footer_1)
w.Write([]byte(item.Name))
w.Write(footer_2)
if tmpl_profile_vars.Header.ThemeName == item.Name {
w.Write(footer_3)
}
w.Write(footer_4)
w.Write([]byte(item.FriendlyName))
w.Write([]byte(item.Name))
w.Write(footer_5)
}
}
}
if tmpl_profile_vars.Header.Theme.Name == item.Name {
w.Write(footer_6)
if tmpl_profile_vars.Header.Widgets.RightSidebar != "" {
}
w.Write(footer_7)
w.Write([]byte(string(tmpl_profile_vars.Header.Widgets.RightSidebar)))
w.Write([]byte(item.FriendlyName))
w.Write(footer_8)
}
}
}
w.Write(footer_9)
if tmpl_profile_vars.Header.Widgets.RightSidebar != "" {
w.Write(footer_10)
w.Write([]byte(string(tmpl_profile_vars.Header.Widgets.RightSidebar)))
w.Write(footer_11)
}
w.Write(footer_12)
return nil
}

View File

@ -3,9 +3,9 @@
// Code generated by Gosora. More below:
/* This file was automatically generated by the software. Please don't edit it as your changes may be overwritten at any moment. */
package main
import "strconv"
import "net/http"
import "./common"
import "strconv"
// nolint
func init() {
@ -22,7 +22,7 @@ w.Write([]byte(tmpl_topic_vars.Title))
w.Write(header_1)
w.Write([]byte(tmpl_topic_vars.Header.Site.Name))
w.Write(header_2)
w.Write([]byte(tmpl_topic_vars.Header.ThemeName))
w.Write([]byte(tmpl_topic_vars.Header.Theme.Name))
w.Write(header_3)
if len(tmpl_topic_vars.Header.Stylesheets) != 0 {
for _, item := range tmpl_topic_vars.Header.Stylesheets {
@ -125,7 +125,7 @@ if tmpl_topic_vars.Topic.Avatar != "" {
w.Write(topic_22)
w.Write([]byte(tmpl_topic_vars.Topic.Avatar))
w.Write(topic_23)
w.Write([]byte(tmpl_topic_vars.Header.ThemeName))
w.Write([]byte(tmpl_topic_vars.Header.Theme.Name))
w.Write(topic_24)
if tmpl_topic_vars.Topic.ContentLines <= 5 {
w.Write(topic_25)
@ -223,7 +223,7 @@ if item.Avatar != "" {
w.Write(topic_65)
w.Write([]byte(item.Avatar))
w.Write(topic_66)
w.Write([]byte(tmpl_topic_vars.Header.ThemeName))
w.Write([]byte(tmpl_topic_vars.Header.Theme.Name))
w.Write(topic_67)
if item.ContentLines <= 5 {
w.Write(topic_68)
@ -296,28 +296,37 @@ w.Write(topic_98)
w.Write(topic_99)
}
w.Write(topic_100)
if tmpl_topic_vars.Header.Theme.AboutSegment {
w.Write(footer_0)
dispInt := tmpl_topic_vars.Header.Settings["about_segment_title"]
w.Write([]byte(dispInt.(string)))
w.Write(footer_1)
dispInt = tmpl_topic_vars.Header.Settings["about_segment_body"]
w.Write([]byte(dispInt.(string)))
w.Write(footer_2)
}
w.Write(footer_3)
if len(tmpl_topic_vars.Header.Themes) != 0 {
for _, item := range tmpl_topic_vars.Header.Themes {
if !item.HideFromThemes {
w.Write(footer_1)
w.Write([]byte(item.Name))
w.Write(footer_2)
if tmpl_topic_vars.Header.ThemeName == item.Name {
w.Write(footer_3)
}
w.Write(footer_4)
w.Write([]byte(item.FriendlyName))
w.Write([]byte(item.Name))
w.Write(footer_5)
}
}
}
if tmpl_topic_vars.Header.Theme.Name == item.Name {
w.Write(footer_6)
if tmpl_topic_vars.Header.Widgets.RightSidebar != "" {
}
w.Write(footer_7)
w.Write([]byte(string(tmpl_topic_vars.Header.Widgets.RightSidebar)))
w.Write([]byte(item.FriendlyName))
w.Write(footer_8)
}
}
}
w.Write(footer_9)
if tmpl_topic_vars.Header.Widgets.RightSidebar != "" {
w.Write(footer_10)
w.Write([]byte(string(tmpl_topic_vars.Header.Widgets.RightSidebar)))
w.Write(footer_11)
}
w.Write(footer_12)
return nil
}

View File

@ -3,9 +3,9 @@
// Code generated by Gosora. More below:
/* This file was automatically generated by the software. Please don't edit it as your changes may be overwritten at any moment. */
package main
import "strconv"
import "net/http"
import "./common"
import "strconv"
// nolint
func init() {
@ -22,7 +22,7 @@ w.Write([]byte(tmpl_topic_alt_vars.Title))
w.Write(header_1)
w.Write([]byte(tmpl_topic_alt_vars.Header.Site.Name))
w.Write(header_2)
w.Write([]byte(tmpl_topic_alt_vars.Header.ThemeName))
w.Write([]byte(tmpl_topic_alt_vars.Header.Theme.Name))
w.Write(header_3)
if len(tmpl_topic_alt_vars.Header.Stylesheets) != 0 {
for _, item := range tmpl_topic_alt_vars.Header.Stylesheets {
@ -302,28 +302,37 @@ w.Write(topic_alt_102)
w.Write(topic_alt_103)
}
w.Write(topic_alt_104)
if tmpl_topic_alt_vars.Header.Theme.AboutSegment {
w.Write(footer_0)
dispInt := tmpl_topic_alt_vars.Header.Settings["about_segment_title"]
w.Write([]byte(dispInt.(string)))
w.Write(footer_1)
dispInt = tmpl_topic_alt_vars.Header.Settings["about_segment_body"]
w.Write([]byte(dispInt.(string)))
w.Write(footer_2)
}
w.Write(footer_3)
if len(tmpl_topic_alt_vars.Header.Themes) != 0 {
for _, item := range tmpl_topic_alt_vars.Header.Themes {
if !item.HideFromThemes {
w.Write(footer_1)
w.Write([]byte(item.Name))
w.Write(footer_2)
if tmpl_topic_alt_vars.Header.ThemeName == item.Name {
w.Write(footer_3)
}
w.Write(footer_4)
w.Write([]byte(item.FriendlyName))
w.Write([]byte(item.Name))
w.Write(footer_5)
}
}
}
if tmpl_topic_alt_vars.Header.Theme.Name == item.Name {
w.Write(footer_6)
if tmpl_topic_alt_vars.Header.Widgets.RightSidebar != "" {
}
w.Write(footer_7)
w.Write([]byte(string(tmpl_topic_alt_vars.Header.Widgets.RightSidebar)))
w.Write([]byte(item.FriendlyName))
w.Write(footer_8)
}
}
}
w.Write(footer_9)
if tmpl_topic_alt_vars.Header.Widgets.RightSidebar != "" {
w.Write(footer_10)
w.Write([]byte(string(tmpl_topic_alt_vars.Header.Widgets.RightSidebar)))
w.Write(footer_11)
}
w.Write(footer_12)
return nil
}

View File

@ -22,7 +22,7 @@ w.Write([]byte(tmpl_topics_vars.Title))
w.Write(header_1)
w.Write([]byte(tmpl_topics_vars.Header.Site.Name))
w.Write(header_2)
w.Write([]byte(tmpl_topics_vars.Header.ThemeName))
w.Write([]byte(tmpl_topics_vars.Header.Theme.Name))
w.Write(header_3)
if len(tmpl_topics_vars.Header.Stylesheets) != 0 {
for _, item := range tmpl_topics_vars.Header.Stylesheets {
@ -199,28 +199,37 @@ w.Write(topics_55)
w.Write(topics_56)
}
w.Write(topics_57)
if tmpl_topics_vars.Header.Theme.AboutSegment {
w.Write(footer_0)
dispInt := tmpl_topics_vars.Header.Settings["about_segment_title"]
w.Write([]byte(dispInt.(string)))
w.Write(footer_1)
dispInt = tmpl_topics_vars.Header.Settings["about_segment_body"]
w.Write([]byte(dispInt.(string)))
w.Write(footer_2)
}
w.Write(footer_3)
if len(tmpl_topics_vars.Header.Themes) != 0 {
for _, item := range tmpl_topics_vars.Header.Themes {
if !item.HideFromThemes {
w.Write(footer_1)
w.Write([]byte(item.Name))
w.Write(footer_2)
if tmpl_topics_vars.Header.ThemeName == item.Name {
w.Write(footer_3)
}
w.Write(footer_4)
w.Write([]byte(item.FriendlyName))
w.Write([]byte(item.Name))
w.Write(footer_5)
}
}
}
if tmpl_topics_vars.Header.Theme.Name == item.Name {
w.Write(footer_6)
if tmpl_topics_vars.Header.Widgets.RightSidebar != "" {
}
w.Write(footer_7)
w.Write([]byte(string(tmpl_topics_vars.Header.Widgets.RightSidebar)))
w.Write([]byte(item.FriendlyName))
w.Write(footer_8)
}
}
}
w.Write(footer_9)
if tmpl_topics_vars.Header.Widgets.RightSidebar != "" {
w.Write(footer_10)
w.Write([]byte(string(tmpl_topics_vars.Header.Widgets.RightSidebar)))
w.Write(footer_11)
}
w.Write(footer_12)
return nil
}

View File

@ -1,6 +1,8 @@
<nav class="colstack_left">
<div class="colstack_item colstack_head rowhead">
<div class="rowitem"><h1>My Account</h1></div>
<div class="rowitem">
<a href="/user/edit/critical/"><h1>My Account</h1></a>
</div>
</div>
<div class="colstack_item rowmenu">
<div class="rowitem passive"><a href="/user/edit/avatar/">Avatar</a></div>

View File

@ -1,7 +1,7 @@
{{template "header.html" . }}
<main class="colstack account">
<div class="colstack account">
{{template "account-menu.html" . }}
<div class="colstack_right">
<main class="colstack_right">
<div class="colstack_item colstack_head rowhead">
<div class="rowitem"><h1>Edit Avatar</h1></div>
</div>
@ -10,7 +10,7 @@
<div class="rowitem"><img src="{{.CurrentUser.Avatar}}" height="128px" max-width="128px" /></div>
</div>
{{end}}
<div class="colstack_item form_item">
<div class="colstack_item the_form">
<form action="/user/edit/avatar/submit/" method="post" enctype="multipart/form-data">
<div class="formrow real_first_child">
<div class="formitem formlabel"><a>Upload Avatar</a></div>
@ -21,6 +21,6 @@
</div>
</form>
</div>
</div>
</main>
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,7 +1,7 @@
{{template "header.html" . }}
<main class="colstack account">
<div class="colstack account">
{{template "account-menu.html" . }}
<div class="colstack_right">
<main class="colstack_right">
<div class="colstack_item colstack_head rowhead">
<div class="rowitem"><h1>Emails</h1></div>
</div>
@ -17,6 +17,6 @@
</div>
{{end}}
</div>
</div>
</main>
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,11 +1,11 @@
{{template "header.html" . }}
<main class="colstack account">
<div class="colstack account">
{{template "account-menu.html" . }}
<div class="colstack_right">
<main class="colstack_right">
<div class="colstack_item colstack_head rowhead">
<div class="rowitem"><h1>Edit Username</h1></div>
</div>
<div class="colstack_item form_item">
<div class="colstack_item the_form">
<form action="/user/edit/username/submit/" method="post">
<div class="formrow real_first_child">
<div class="formitem formlabel"><a>Current Username</a></div>
@ -20,6 +20,6 @@
</div>
</form>
</div>
</div>
</main>
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,11 +1,11 @@
{{template "header.html" . }}
<main class="colstack account">
<div class="colstack account">
{{template "account-menu.html" . }}
<div class="colstack_right">
<main class="colstack_right">
<div class="colstack_item colstack_head rowhead">
<div class="rowitem"><h1>Edit Password</h1></div>
</div>
<div class="colstack_item form_item">
<div class="colstack_item the_form">
<form action="/user/edit/critical/submit/" method="post">
<div class="formrow real_first_child">
<div class="formitem formlabel"><a>Current Password</a></div>
@ -24,6 +24,6 @@
</div>
</form>
</div>
</div>
</main>
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,10 +1,16 @@
{{if .Header.Theme.AboutSegment}}<div class="about">
<a id="aboutTitle">{{.Header.Settings.about_segment_title}}</a>
<span id="aboutDesc">{{.Header.Settings.about_segment_body}}</span>
</div>{{end}}
<div class="footer">
<div id="poweredBy">Powered by Gosora - <span>Made with love by Azareal</span></div>
<div id="poweredBy">
<a id="poweredByName" href="https://github.com/Azareal/Gosora">Powered by Gosora</a><span id="poweredByDash"> - </span><span id="poweredByMaker">Made with love by Azareal</span>
</div>
<form action="/theme/" method="post">
<div id="themeSelector" style="float: right;">
<select id="themeSelectorSelect" name="themeSelector" aria-label="Change the site's appearance">
{{range .Header.Themes}}
{{if not .HideFromThemes}}<option val="{{.Name}}"{{if eq $.Header.ThemeName .Name}} selected{{end}}>{{.FriendlyName}}</option>{{end}}
{{if not .HideFromThemes}}<option val="{{.Name}}"{{if eq $.Header.Theme.Name .Name}} selected{{end}}>{{.FriendlyName}}</option>{{end}}
{{end}}
</select>
</div>

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<title>{{.Title}} | {{.Header.Site.Name}}</title>
<link href="/static/{{.Header.ThemeName}}/main.css" rel="stylesheet" type="text/css">
<link href="/static/{{.Header.Theme.Name}}/main.css" rel="stylesheet" type="text/css">
{{range .Header.Stylesheets}}
<link href="/static/{{.}}" rel="stylesheet" type="text/css">
{{end}}

View File

@ -1,4 +1,5 @@
{{template "header.html" . }}
<div class="colstack">
<nav class="colstack_left" aria-label="The control panel menu">
<div class="colstack_item colstack_head">
<div class="rowitem"><a href="/panel/logs/mod/">Logs</a></div>
@ -25,4 +26,5 @@
{{end}}
</div>
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,4 +1,5 @@
{{template "header.html" . }}
<div class="colstack">
{{template "panel-menu.html" . }}
<main class="colstack_right">
<div class="colstack_item colstack_head">
@ -17,4 +18,5 @@
{{end}}
</div>
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,14 +1,16 @@
{{template "header.html" . }}
<div class="colstack">
{{template "panel-menu.html" . }}
<main id="panel_dashboard_right" class="colstack_right">
<main id="panel_dashboard_right" class="colstack_right">
<div class="colstack_item colstack_head">
<div class="rowitem"><a>Dashboard</a></div>
</div>
<div id="panel_dashboard" class="colstack_grid">
{{range .GridItems}}
<div id="{{.ID}}" class="grid_item {{.Class}}" title="{{.Note}}" style="{{if .TextColour}}color: {{.TextColour}};{{end}}
{{if .Background}}background-color: {{.Background}};{{end}}">{{.Body}}</div>
{{if .Background}}background-color: {{.Background}};{{end}}"><span>{{.Body}}</span></div>
{{end}}
</div>
</main>
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,4 +1,5 @@
{{template "header.html" . }}
<div class="colstack">
{{template "panel-menu.html" . }}
<main id="panel_dashboard_right" class="colstack_right">
<div class="colstack_item colstack_head">
@ -14,4 +15,5 @@
<div class="grid_item grid_stat">{{.DBAdapter}}</div>
</div>
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,4 +1,6 @@
{{template "header.html" . }}
<div class="colstack">
{{template "panel-menu.html" . }}
<script>
var form_vars = {'perm_preset': ['can_moderate','can_post','read_only','no_access','default','custom']};
@ -63,4 +65,5 @@ var form_vars = {'perm_preset': ['can_moderate','can_post','read_only','no_acces
{{end}}
</div>
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,4 +1,6 @@
{{template "header.html" . }}
<div class="colstack">
{{template "panel-menu.html" . }}
<script>var form_vars = {
'forum_active': ['Hide','Show'],
@ -72,4 +74,5 @@
</div>
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,4 +1,5 @@
{{template "header.html" . }}
<div class="colstack">
<nav class="colstack_left" aria-label="The control panel menu">
<div class="colstack_item colstack_head">
<div class="rowitem"><a href="/panel/groups/edit/{{.ID}}">Group Editor</a></div>
@ -60,4 +61,5 @@
</div>
</form>
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,4 +1,5 @@
{{template "header.html" . }}
<div class="colstack">
<nav class="colstack_left" aria-label="The control panel menu">
<div class="colstack_item colstack_head">
<div class="rowitem"><a href="/panel/groups/edit/{{.ID}}">Group Editor</a></div>
@ -43,4 +44,5 @@
</form>
</div>
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,6 +1,7 @@
{{template "header.html" . }}
{{template "panel-menu.html" . }}
<div class="colstack">
{{template "panel-menu.html" . }}
<main class="colstack_right">
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>Groups</h1></div>
@ -57,4 +58,6 @@
</form>
</div>
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,4 +1,6 @@
{{template "header.html" . }}
<div class="colstack">
<nav class="colstack_left" aria-label="The control panel menu">
<div class="colstack_item colstack_head">
<div class="rowitem"><a href="/panel/logs/mod/">Logs</a></div>
@ -37,4 +39,6 @@
</div>
{{end}}
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,4 +1,6 @@
{{template "header.html" . }}
<div class="colstack">
{{template "panel-menu.html" . }}
<main class="colstack_right">
<div class="colstack_item colstack_head">
@ -21,4 +23,6 @@
{{end}}
</div>
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,4 +1,6 @@
{{template "header.html" . }}
<div class="colstack">
{{template "panel-menu.html" . }}
<main class="colstack_right">
<div class="colstack_item colstack_head">
@ -34,4 +36,6 @@
</form>
</div>
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,4 +1,6 @@
{{template "header.html" . }}
<div class="colstack">
{{template "panel-menu.html" . }}
<main class="colstack_right">
<div class="colstack_item colstack_head">
@ -13,4 +15,6 @@
{{end}}
</div>
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,4 +1,6 @@
{{template "header.html" . }}
<div class="colstack">
<nav class="colstack_left" aria-label="The control panel menu">
<div class="colstack_item colstack_head">
<div class="rowitem"><a href="/panel/themes/">Theme Manager</a></div>
@ -56,4 +58,6 @@
{{end}}
</div>
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,4 +1,6 @@
{{template "header.html" . }}
<div class="colstack">
{{template "panel-menu.html" . }}
<main class="colstack_right">
<div class="colstack_item colstack_head">
@ -33,4 +35,6 @@
</form>
</div>
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,6 +1,7 @@
{{template "header.html" . }}
{{template "panel-menu.html" . }}
<div class="colstack">
{{template "panel-menu.html" . }}
<main class="colstack_right">
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>Users</h1></div>
@ -29,4 +30,6 @@
</div>
{{end}}
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,4 +1,6 @@
{{template "header.html" . }}
<div class="colstack">
{{template "panel-menu.html" . }}
<main class="colstack_right">
<div class="colstack_item colstack_head">
@ -42,4 +44,6 @@
</form>
</div>
</main>
</div>
{{template "footer.html" . }}

View File

@ -1,6 +1,6 @@
{{template "header.html" . }}
<div id="profile_container">
<div id="profile_container" class="colstack">
<div id="profile_left_lane" class="colstack_left">
<!--<header class="colstack_item colstack_head rowhead">

View File

@ -23,7 +23,7 @@
</div>
<article itemscope itemtype="http://schema.org/CreativeWork" class="rowblock post_container top_post" aria-label="The opening post for this topic">
<div class="rowitem passive editable_parent post_item {{.Topic.ClassName}}" style="{{if .Topic.Avatar}}background-image: url({{.Topic.Avatar}}), url(/static/{{.Header.ThemeName}}/post-avatar-bg.jpg);background-position: 0px {{if le .Topic.ContentLines 5}}-1{{end}}0px;background-repeat:no-repeat, repeat-y;{{end}}">
<div class="rowitem passive editable_parent post_item {{.Topic.ClassName}}" style="{{if .Topic.Avatar}}background-image: url({{.Topic.Avatar}}), url(/static/{{.Header.Theme.Name}}/post-avatar-bg.jpg);background-position: 0px {{if le .Topic.ContentLines 5}}-1{{end}}0px;background-repeat:no-repeat, repeat-y;{{end}}">
<p class="hide_on_edit topic_content user_content" itemprop="text" style="margin:0;padding:0;">{{.Topic.ContentHTML}}</p>
<textarea name="topic_content" class="show_on_edit topic_content_input">{{.Topic.Content}}</textarea>
@ -57,7 +57,7 @@
<span itemprop="text">{{.ActionType}}</span>
</article>
{{else}}
<article itemscope itemtype="http://schema.org/CreativeWork" class="rowitem passive deletable_block editable_parent post_item {{.ClassName}}" style="{{if .Avatar}}background-image: url({{.Avatar}}), url(/static/{{$.Header.ThemeName}}/post-avatar-bg.jpg);background-position: 0px {{if le .ContentLines 5}}-1{{end}}0px;background-repeat:no-repeat, repeat-y;{{end}}">
<article itemscope itemtype="http://schema.org/CreativeWork" class="rowitem passive deletable_block editable_parent post_item {{.ClassName}}" style="{{if .Avatar}}background-image: url({{.Avatar}}), url(/static/{{$.Header.Theme.Name}}/post-avatar-bg.jpg);background-position: 0px {{if le .ContentLines 5}}-1{{end}}0px;background-repeat:no-repeat, repeat-y;{{end}}">
{{/** TODO: We might end up with <br>s in the inline editor, fix this **/}}
<p class="editable_block user_content" itemprop="text" style="margin:0;padding:0;">{{.ContentHtml}}</p>

View File

@ -1,6 +1,6 @@
:root {
--header-border-color: hsl(0,0%,85%);
--element-border-color: hsl(0,0%,90%);
--header-border-color: hsl(0,0%,80%);
--element-border-color: hsl(0,0%,85%);
--element-background-color: white;
--replies-lang-string: " replies";
--topics-lang-string: " topics";
@ -45,8 +45,10 @@ a {
padding-top: 14px;
display: flex;
/*background-color: hsl(0,0%,97%);*/
background-color: hsl(0,0%,98%);
padding-left: 0px;
padding-right: 0px;
padding-bottom: 0px;
}
#main {
@ -163,14 +165,22 @@ ul {
border-bottom: 2px solid var(--header-border-color);
background-color: var(--element-background-color);
margin-left: 12px;
}
.rowblock {
margin-right: 12px;
}
.colstack_right {
padding-right: 12px;
}
.rowhead, .opthead, .colstack_head {
padding: 13px;
padding-top: 14px;
padding-bottom: 14px;
}
.rowhead:not(:first-child), .opthead:not(:first-child), .colstack_head:not(:first-child) {
margin-top: 8px;
}
.rowhead h1, .opthead h1, .colstack_head h1 {
font-size: 20px;
font-weight: normal;
@ -181,14 +191,18 @@ ul {
margin-block-end: 0;
display: inline-block;
}
.colstack_head a h1 {
font-size: 16px;
color: var(--primary-link-color);
}
.colstack {
display: flex;
}
#main .colstack_left {
.colstack:not(#profile_container) .colstack_left {
width: 300px;
}
#main .colstack_right {
.colstack:not(#profile_container) .colstack_right {
width: calc(90% - 300px);
}
@ -412,8 +426,9 @@ label.uploadItem {
padding-left: 33px;
}
select, input, textarea {
select, input, textarea, button {
border: 1px solid var(--header-border-color);
background: var(--element-background-color);
padding: 5px;
color: hsl(0,0%,30%);
}
@ -713,7 +728,7 @@ select, input, textarea {
margin-right: 10px;
}
#profile_container, #profile_left_pane .topBlock {
#profile_left_pane .topBlock {
display: flex;
}
#profile_left_lane {
@ -725,6 +740,7 @@ select, input, textarea {
padding-bottom: 18px;
border: 1px solid var(--element-border-color);
border-bottom: 2px solid var(--element-border-color);
background-color: var(--element-background-color);
}
#profile_left_pane .avatarRow {
padding: 24px;
@ -748,17 +764,21 @@ select, input, textarea {
.rowmenu .passive {
border: 1px solid var(--element-border-color);
border-bottom: 2px solid var(--element-border-color);
background-color: var(--element-background-color);
margin-top: 6px;
padding: 12px;
padding-top: 10px;
padding-bottom: 10px;
}
.rowmenu {
.colstack:not(#profile_container) .rowmenu {
padding-left: 12px;
padding-right: 12px;
}
.rowmenu .passive:hover {
margin-left: 4px;
.colstack:not(#profile_container) .rowmenu .passive {
margin-top: 0px;
border-bottom: none;
}
.colstack:not(#profile_container) .rowmenu .passive:last-child {
border-bottom: 2px solid var(--element-border-color);
}
#profile_left_pane .passiveBlock .passive {
padding-left: 12px;
@ -771,50 +791,158 @@ select, input, textarea {
#profile_right_lane .colstack_item, .colstack_right .colstack_item {
border: 1px solid var(--element-border-color);
border-bottom: 2px solid var(--element-border-color);
}
#profile_right_lane .colstack_item, .colstack_right .colstack_item, .colstack_right .colstack_grid {
margin-left: 16px;
}
.form_item {
display: flex;
#profile_right_lane .topic_reply_form {
width: auto;
}
.form_item form, .colstack_item .formrow {
display: contents;
.colstack_item .formrow {
display: flex;
}
.colstack_right .formrow {
padding-left: 16px;
padding-right: 16px;
padding-bottom: 4px;
border-right: 1px solid var(--element-border-color);
}
.colstack_right .formrow:first-child {
padding-top: 16px;
}
.colstack_right .formrow .formlabel {
padding-top: 5px;
}
.colstack_right .formrow:last-child {
padding-bottom: 16px;
}
.colstack_item:not(#profile_right_lane) .formrow .formlabel {
width: min-content;
width: 40%;
margin-right: 12px;
white-space: nowrap;
}
.formitem:only-child {
width: 100%;
}
.the_form .formitem:only-child button {
margin-left: auto;
margin-right: auto;
}
.quick_reply_form, .topic_reply_form, .the_form {
background: var(--element-background-color);
}
.formrow {
border-right: none !important;
}
.footer {
.about, .footer {
border-top: 1px solid var(--element-border-color);
padding: 12px;
padding-top: 10px;
padding-bottom: 10px;
margin-top: 14px;
margin-left: -8px;
margin-right: -8px;
background-color: var(--element-background-color);
display: flex;
}
.footer #poweredBy {
.about, #poweredBy {
font-size: 17px;
display: flex;
flex-direction: column;
}
.footer #poweredBy span {
.about {
margin-top: 14px;
}
#poweredBy {
margin-right: auto;
}
#poweredBy span {
font-size: 16px;
}
#aboutTitle {
font-size: 18px;
margin: 8px;
}
#poweredByName {
font-size: 17px;
margin: 4px;
}
#aboutDesc {
margin-left: 8px;
margin-top: 8px;
width: 60%;
}
#aboutDesc p:last-child {
-webkit-margin-after: 8px;
}
#aboutDesc p:first-child {
-webkit-margin-before: 3px;
}
#poweredByDash, #poweredByMaker {
display: none;
}
#themeSelectorSelect {
padding: 3px;
margin-top: 2px;
}
.colstack_grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.grid_item {
background: var(--element-background-color);
border: 1px solid var(--element-border-color);
border-bottom: 2px solid var(--element-border-color);
margin: 8px;
padding: 16px;
padding-left: 0px;
display: flex;
padding-top: 0px;
padding-bottom: 0px;
padding-right: 10px;
}
.grid_item span {
margin-top: 16px;
margin-bottom: 16px;
margin-left: auto;
margin-right: auto;
text-align: center;
}
#dash-version:before, #dash-cpu:before, #dash-ram:before {
display: inline-block;
background: hsl(0,0%,98%);
font: normal normal normal 14px/1 FontAwesome;
font-size: 20px;
padding-left: 17px;
padding-top: 16px;
padding-right: 19px;
color: hsl(0,0%,20%);
border-bottom: 1px solid var(--element-border-color);
}
#dash-version:before {
content: "\f126";
}
#dash-cpu:before {
content: "\f2db";
}
#dash-ram:before {
content: "\f233";
}
@media(max-width: 670px) {
.topic_inner_right {
display: none;
}
}
/*#dash-cpu:before {
content: "\f2db";
width: 30px;
margin-right: 8px;
display: inline-block;
padding-left: 20px;
background: hsl(0,0%,98%);
font: normal normal normal 14px/1 FontAwesome;
}*/

View File

@ -0,0 +1,6 @@
.about {
display: none;
}
.footer {
margin-top: 14px;
}

View File

@ -6,6 +6,7 @@
"URL": "github.com/Azareal/Gosora",
"Tag": "WIP",
"Sidebars":"right",
"AboutSegment":true,
"Templates": [
{
"Name": "topic",

View File

@ -352,11 +352,12 @@ AdminStatLoop:
} else {
calcperc := int(cpuPerc[0]) / runtime.NumCPU()
cpustr = strconv.Itoa(calcperc)
if calcperc < 30 {
switch {
case calcperc < 30:
cpuColour = "stat_green"
} else if calcperc < 75 {
case calcperc < 75:
cpuColour = "stat_orange"
} else {
default:
cpuColour = "stat_red"
}
}
@ -409,20 +410,20 @@ AdminStatLoop:
// nolint
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"))
w.Write([]byte("set #dash-uonline " + strconv.Itoa(uonline) + uunit + " users online\r"))
w.Write([]byte("set #dash-totonline <span>" + strconv.Itoa(totonline) + totunit + " online</span>\r"))
w.Write([]byte("set #dash-gonline <span>" + strconv.Itoa(gonline) + gunit + " guests online</span>\r"))
w.Write([]byte("set #dash-uonline <span>" + strconv.Itoa(uonline) + uunit + " users online</span>\r"))
w.Write([]byte("set-class #dash-totonline grid_item grid_stat " + onlineColour + "\r"))
w.Write([]byte("set-class #dash-gonline grid_item grid_stat " + onlineGuestsColour + "\r"))
w.Write([]byte("set-class #dash-uonline grid_item grid_stat " + onlineUsersColour + "\r"))
}
w.Write([]byte("set #dash-cpu CPU: " + cpustr + "%\r"))
w.Write([]byte("set #dash-cpu <span>CPU: " + cpustr + "%</span>\r"))
w.Write([]byte("set-class #dash-cpu grid_item grid_istat " + cpuColour + "\r"))
if !noRAMUpdates {
w.Write([]byte("set #dash-ram RAM: " + ramstr + "\r"))
w.Write([]byte("set #dash-ram <span>RAM: " + ramstr + "</span>\r"))
w.Write([]byte("set-class #dash-ram grid_item grid_istat " + ramColour + "\r"))
}