diff --git a/README.md b/README.md index 760971bc..3d5d78f3 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Other modern features like alerts, likes, advanced dashboard with live stats (CP # Dependencies -Go 1.8 - You will need to install this. Pick the .msi, if you want everything sorted out for you rather than having to go around updating the environment settings. https://golang.org/doc/install +Go 1.9 - You will need to install this. Pick the .msi, if you want everything sorted out for you rather than having to go around updating the environment settings. https://golang.org/doc/install MySQL Database - You will need to setup a MySQL Database somewhere. A MariaDB Database works equally well and is much faster than MySQL. You could use something like WNMP / XAMPP which have a little PHP script called PhpMyAdmin for managing MySQL databases or you could install MariaDB directly. diff --git a/alerts.go b/alerts.go index 224dba90..902e076d 100644 --- a/alerts.go +++ b/alerts.go @@ -29,13 +29,13 @@ import "errors" func buildAlert(asid int, event string, elementType string, actorID int, targetUserID int, elementID int, user User /* The current user */) (string, error) { var targetUser *User - actor, err := users.CascadeGet(actorID) + actor, err := users.Get(actorID) if err != nil { return "", errors.New("Unable to find the actor") } /*if elementType != "forum" { - targetUser, err = users.CascadeGet(targetUser_id) + targetUser, err = users.Get(targetUser_id) if err != nil { LocalErrorJS("Unable to find the target user",w,r) return @@ -52,7 +52,7 @@ func buildAlert(asid int, event string, elementType string, actorID int, targetU case "forum": if event == "reply" { act = "created a new topic" - topic, err := topics.CascadeGet(elementID) + topic, err := topics.Get(elementID) if err != nil { return "", errors.New("Unable to find the linked topic") } @@ -64,7 +64,7 @@ func buildAlert(asid int, event string, elementType string, actorID int, targetU act = "did something in a forum" } case "topic": - topic, err := topics.CascadeGet(elementID) + topic, err := topics.Get(elementID) if err != nil { return "", errors.New("Unable to find the linked topic") } @@ -75,7 +75,7 @@ func buildAlert(asid int, event string, elementType string, actorID int, targetU postAct = " your topic" } case "user": - targetUser, err = users.CascadeGet(elementID) + targetUser, err = users.Get(elementID) if err != nil { return "", errors.New("Unable to find the target user") } diff --git a/auth.go b/auth.go index dbd9862a..dd6388e9 100644 --- a/auth.go +++ b/auth.go @@ -93,7 +93,7 @@ func (auth *DefaultAuth) ForceLogout(uid int) error { } // Flush the user out of the cache and reload - err = users.Load(uid) + err = users.Reload(uid) if err != nil { return errors.New("Your account no longer exists.") } @@ -141,7 +141,7 @@ func (auth *DefaultAuth) SessionCheck(w http.ResponseWriter, r *http.Request) (u } // Is this session valid..? - user, err = users.CascadeGet(uid) + user, err = users.Get(uid) if err == ErrNoRows { return &guestUser, false } else if err != nil { @@ -168,6 +168,6 @@ func (auth *DefaultAuth) CreateSession(uid int) (session string, err error) { } // Reload the user data - _ = users.Load(uid) + _ = users.Reload(uid) return session, nil } diff --git a/cache.go b/cache.go index 9f85944b..f5a0c397 100644 --- a/cache.go +++ b/cache.go @@ -18,14 +18,18 @@ var ErrStoreCapacityOverflow = errors.New("This datastore has reached it's maxim type DataStore interface { Load(id int) error Get(id int) (interface{}, error) - GetUnsafe(id int) (interface{}, error) - CascadeGet(id int) (interface{}, error) BypassGet(id int) (interface{}, error) - Set(item interface{}) error - Add(item interface{}) error - AddUnsafe(item interface{}) error - Remove(id int) error - RemoveUnsafe(id int) error + //GetGlobalCount() +} + +type DataCache interface { + CacheGet(id int) (interface{}, error) + CacheGetUnsafe(id int) (interface{}, error) + CacheSet(item interface{}) error + CacheAdd(item interface{}) error + CacheAddUnsafe(item interface{}) error + CacheRemove(id int) error + CacheRemoveUnsafe(id int) error GetLength() int GetCapacity() int } diff --git a/database.go b/database.go index e52ac8f8..4c52352f 100644 --- a/database.go +++ b/database.go @@ -1,7 +1,7 @@ package main import "log" -import "encoding/json" + import "database/sql" var db *sql.DB @@ -19,57 +19,11 @@ func initDatabase() (err error) { } log.Print("Loading the usergroups.") - groups = append(groups, Group{ID: 0, Name: "System"}) - - rows, err := get_groups_stmt.Query() + gstore = NewMemoryGroupStore() + err = gstore.LoadGroups() if err != nil { return err } - defer rows.Close() - - i := 1 - for ; rows.Next(); i++ { - group := Group{ID: 0} - err := rows.Scan(&group.ID, &group.Name, &group.PermissionsText, &group.PluginPermsText, &group.IsMod, &group.IsAdmin, &group.IsBanned, &group.Tag) - if err != nil { - return err - } - - // Ugh, you really shouldn't physically delete these items, it makes a big mess of things - if group.ID != i { - log.Print("Stop physically deleting groups. You are messing up the IDs. Use the Group Manager or delete_group() instead x.x") - fillGroupIDGap(i, group.ID) - } - - err = json.Unmarshal(group.PermissionsText, &group.Perms) - if err != nil { - return err - } - if dev.DebugMode { - log.Print(group.Name + ": ") - log.Printf("%+v\n", group.Perms) - } - - err = json.Unmarshal(group.PluginPermsText, &group.PluginPerms) - if err != nil { - return err - } - if dev.DebugMode { - log.Print(group.Name + ": ") - log.Printf("%+v\n", group.PluginPerms) - } - - //group.Perms.ExtData = make(map[string]bool) - groups = append(groups, group) - } - err = rows.Err() - if err != nil { - return err - } - groupCapCount = i - - log.Print("Binding the Not Loggedin Group") - GuestPerms = groups[6].Perms log.Print("Loading the forums.") fstore = NewMemoryForumStore() diff --git a/forum_store.go b/forum_store.go index 6b868e40..4ee7a15a 100644 --- a/forum_store.go +++ b/forum_store.go @@ -17,7 +17,7 @@ import ( var forumUpdateMutex sync.Mutex var forumCreateMutex sync.Mutex -var forumPerms map[int]map[int]ForumPerms // [gid][fid]Perms +var forumPerms map[int]map[int]ForumPerms // [gid][fid]Perms // TODO: Add an abstraction around this and make it more thread-safe var fstore ForumStore // ForumStore is an interface for accessing the forums and the metadata stored on them @@ -25,15 +25,11 @@ type ForumStore interface { LoadForums() error DirtyGet(id int) *Forum Get(id int) (*Forum, error) - CascadeGet(id int) (*Forum, error) - CascadeGetCopy(id int) (Forum, error) + GetCopy(id int) (Forum, error) BypassGet(id int) (*Forum, error) - Load(id int) error - Set(forum *Forum) error + Reload(id int) error // ? - Should we move this to TopicCache? Might require us to do a lot more casting in Gosora though... //Update(Forum) error - //CascadeUpdate(Forum) error - Delete(id int) - CascadeDelete(id int) error + Delete(id int) error IncrementTopicCount(id int) error DecrementTopicCount(id int) error UpdateLastTopic(topicName string, tid int, username string, uid int, time string, fid int) error @@ -44,11 +40,17 @@ type ForumStore interface { GetAllVisibleIDs() ([]int, error) //GetChildren(parentID int, parentType string) ([]*Forum,error) //GetFirstChild(parentID int, parentType string) (*Forum,error) - CreateForum(forumName string, forumDesc string, active bool, preset string) (int, error) + Create(forumName string, forumDesc string, active bool, preset string) (int, error) GetGlobalCount() int } +type ForumCache interface { + CacheGet(id int) (*Forum, error) + CacheSet(forum *Forum) error + CacheDelete(id int) +} + // MemoryForumStore is a struct which holds an arbitrary number of forums in memory, usually all of them, although we might introduce functionality to hold a smaller subset in memory for sites with an extremely large number of forums type MemoryForumStore struct { forums sync.Map // map[int]*Forum @@ -158,39 +160,38 @@ func (mfs *MemoryForumStore) DirtyGet(id int) *Forum { return forum } -func (mfs *MemoryForumStore) Get(id int) (*Forum, error) { +func (mfs *MemoryForumStore) CacheGet(id int) (*Forum, error) { fint, ok := mfs.forums.Load(id) - forum := fint.(*Forum) - if !ok || forum.Name == "" { + if !ok || fint.(*Forum).Name == "" { return nil, ErrNoRows } - return forum, nil + return fint.(*Forum), nil } -func (mfs *MemoryForumStore) CascadeGet(id int) (*Forum, error) { +func (mfs *MemoryForumStore) Get(id int) (*Forum, error) { fint, ok := mfs.forums.Load(id) - forum := fint.(*Forum) - if !ok || forum.Name == "" { + if !ok || fint.(*Forum).Name == "" { + var forum = &Forum{ID: id} err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.TopicCount, &forum.LastTopic, &forum.LastTopicID, &forum.LastReplyer, &forum.LastReplyerID, &forum.LastTopicTime) forum.Link = buildForumURL(nameToSlug(forum.Name), forum.ID) forum.LastTopicLink = buildTopicURL(nameToSlug(forum.LastTopic), forum.LastTopicID) return forum, err } - return forum, nil + return fint.(*Forum), nil } -func (mfs *MemoryForumStore) CascadeGetCopy(id int) (Forum, error) { +func (mfs *MemoryForumStore) GetCopy(id int) (Forum, error) { fint, ok := mfs.forums.Load(id) - forum := fint.(*Forum) - if !ok || forum.Name == "" { + if !ok || fint.(*Forum).Name == "" { + var forum = Forum{ID: id} err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.TopicCount, &forum.LastTopic, &forum.LastTopicID, &forum.LastReplyer, &forum.LastReplyerID, &forum.LastTopicTime) forum.Link = buildForumURL(nameToSlug(forum.Name), forum.ID) forum.LastTopicLink = buildTopicURL(nameToSlug(forum.LastTopic), forum.LastTopicID) - return *forum, err + return forum, err } - return *forum, nil + return *fint.(*Forum), nil } func (mfs *MemoryForumStore) BypassGet(id int) (*Forum, error) { @@ -202,7 +203,7 @@ func (mfs *MemoryForumStore) BypassGet(id int) (*Forum, error) { return &forum, err } -func (mfs *MemoryForumStore) Load(id int) error { +func (mfs *MemoryForumStore) Reload(id int) error { var forum = Forum{ID: id} err := mfs.get.QueryRow(id).Scan(&forum.Name, &forum.Desc, &forum.Active, &forum.Preset, &forum.TopicCount, &forum.LastTopic, &forum.LastTopicID, &forum.LastReplyer, &forum.LastReplyerID, &forum.LastTopicTime) if err != nil { @@ -211,11 +212,11 @@ func (mfs *MemoryForumStore) Load(id int) error { forum.Link = buildForumURL(nameToSlug(forum.Name), forum.ID) forum.LastTopicLink = buildTopicURL(nameToSlug(forum.LastTopic), forum.LastTopicID) - mfs.Set(&forum) + mfs.CacheSet(&forum) return nil } -func (mfs *MemoryForumStore) Set(forum *Forum) error { +func (mfs *MemoryForumStore) CacheSet(forum *Forum) error { if !mfs.Exists(forum.ID) { return ErrNoRows } @@ -261,30 +262,31 @@ func (mfs *MemoryForumStore) GetFirstChild(parentID int, parentType string) (*Fo return nil, nil }*/ +// TODO: Add a query for this rather than hitting cache func (mfs *MemoryForumStore) Exists(id int) bool { forum, ok := mfs.forums.Load(id) return ok && forum.(*Forum).Name != "" } // TODO: Batch deletions with name blanking? Is this necessary? -func (mfs *MemoryForumStore) Delete(id int) { +func (mfs *MemoryForumStore) CacheDelete(id int) { mfs.forums.Delete(id) mfs.rebuildView() } -func (mfs *MemoryForumStore) CascadeDelete(id int) error { +func (mfs *MemoryForumStore) Delete(id int) error { forumUpdateMutex.Lock() defer forumUpdateMutex.Unlock() _, err := mfs.delete.Exec(id) if err != nil { return err } - mfs.Delete(id) + mfs.CacheDelete(id) return nil } func (mfs *MemoryForumStore) IncrementTopicCount(id int) error { - forum, err := mfs.CascadeGet(id) + forum, err := mfs.Get(id) if err != nil { return err } @@ -297,7 +299,7 @@ func (mfs *MemoryForumStore) IncrementTopicCount(id int) error { } func (mfs *MemoryForumStore) DecrementTopicCount(id int) error { - forum, err := mfs.CascadeGet(id) + forum, err := mfs.Get(id) if err != nil { return err } @@ -311,7 +313,7 @@ func (mfs *MemoryForumStore) DecrementTopicCount(id int) error { // TODO: Have a pointer to the last topic rather than storing it on the forum itself func (mfs *MemoryForumStore) UpdateLastTopic(topicName string, tid int, username string, uid int, time string, fid int) error { - forum, err := mfs.CascadeGet(fid) + forum, err := mfs.Get(fid) if err != nil { return err } @@ -330,7 +332,7 @@ func (mfs *MemoryForumStore) UpdateLastTopic(topicName string, tid int, username return nil } -func (mfs *MemoryForumStore) CreateForum(forumName string, forumDesc string, active bool, preset string) (int, error) { +func (mfs *MemoryForumStore) Create(forumName string, forumDesc string, active bool, preset string) (int, error) { forumCreateMutex.Lock() res, err := create_forum_stmt.Exec(forumName, forumDesc, active, preset) if err != nil { diff --git a/general_test.go b/general_test.go index dae0b06d..818c7c80 100644 --- a/general_test.go +++ b/general_test.go @@ -100,7 +100,7 @@ func BenchmarkTopicAdminRouteParallel(b *testing.B) { } b.RunParallel(func(pb *testing.PB) { - admin, err := users.CascadeGet(1) + admin, err := users.Get(1) if err != nil { b.Fatal(err) } @@ -157,7 +157,7 @@ func BenchmarkForumsAdminRouteParallel(b *testing.B) { } b.RunParallel(func(pb *testing.PB) { - admin, err := users.CascadeGet(1) + admin, err := users.Get(1) if err != nil { panic(err) } @@ -188,7 +188,7 @@ func BenchmarkForumsAdminRouteParallelProf(b *testing.B) { } b.RunParallel(func(pb *testing.PB) { - admin, err := users.CascadeGet(1) + admin, err := users.Get(1) if err != nil { panic(err) } @@ -237,7 +237,7 @@ func BenchmarkForumsGuestRouteParallel(b *testing.B) { /*func BenchmarkRoutesSerial(b *testing.B) { b.ReportAllocs() - admin, err := users.CascadeGet(1) + admin, err := users.Get(1) if err != nil { panic(err) } @@ -1125,7 +1125,7 @@ func TestStaticRoute(t *testing.T) { init_plugins() } - admin, err := users.CascadeGet(1) + admin, err := users.Get(1) if err != nil { panic(err) } @@ -1181,7 +1181,7 @@ func TestForumsAdminRoute(t *testing.T) { init_plugins() } - admin, err := users.CascadeGet(1) + admin, err := users.Get(1) if err != nil { t.Fatal(err) } @@ -1231,7 +1231,7 @@ func TestForumsGuestRoute(t *testing.T) { init_plugins() } - admin, err := users.CascadeGet(1) + admin, err := users.Get(1) if err != nil { panic(err) } diff --git a/group.go b/group.go index e9c4c6b0..799c4bbd 100644 --- a/group.go +++ b/group.go @@ -1,9 +1,6 @@ package main -import "sync" -import "encoding/json" - -var groupUpdateMutex sync.Mutex +var blankGroup = Group{ID: 0, Name: ""} type GroupAdmin struct { ID int @@ -28,102 +25,3 @@ type Group struct { Forums []ForumPerms CanSee []int // The IDs of the forums this group can see } - -var groupCreateMutex sync.Mutex - -func createGroup(groupName string, tag string, isAdmin bool, isMod bool, isBanned bool) (int, error) { - var gid int - err := group_entry_exists_stmt.QueryRow().Scan(&gid) - if err != nil && err != ErrNoRows { - return 0, err - } - if err != ErrNoRows { - groupUpdateMutex.Lock() - _, err = update_group_rank_stmt.Exec(isAdmin, isMod, isBanned, gid) - if err != nil { - return gid, err - } - _, err = update_group_stmt.Exec(groupName, tag, gid) - if err != nil { - return gid, err - } - - groups[gid].Name = groupName - groups[gid].Tag = tag - groups[gid].IsBanned = isBanned - groups[gid].IsMod = isMod - groups[gid].IsAdmin = isAdmin - - groupUpdateMutex.Unlock() - return gid, nil - } - - groupCreateMutex.Lock() - var permstr = "{}" - res, err := create_group_stmt.Exec(groupName, tag, isAdmin, isMod, isBanned, permstr) - if err != nil { - return 0, err - } - - gid64, err := res.LastInsertId() - if err != nil { - return 0, err - } - gid = int(gid64) - - var perms = BlankPerms - var blankForums []ForumPerms - var blankIntList []int - var pluginPerms = make(map[string]bool) - var pluginPermsBytes = []byte("{}") - if vhooks["create_group_preappend"] != nil { - runVhook("create_group_preappend", &pluginPerms, &pluginPermsBytes) - } - - groups = append(groups, Group{gid, groupName, isMod, isAdmin, isBanned, tag, perms, []byte(permstr), pluginPerms, pluginPermsBytes, blankForums, blankIntList}) - groupCreateMutex.Unlock() - - // Generate the forum permissions based on the presets... - fdata, err := fstore.GetAll() - if err != nil { - return 0, err - } - - permUpdateMutex.Lock() - for _, forum := range fdata { - var thePreset string - if isAdmin { - thePreset = "admins" - } else if isMod { - thePreset = "staff" - } else if isBanned { - thePreset = "banned" - } else { - thePreset = "members" - } - - permmap := presetToPermmap(forum.Preset) - permitem := permmap[thePreset] - permitem.Overrides = true - permstr, err := json.Marshal(permitem) - if err != nil { - return gid, err - } - perms := string(permstr) - _, err = add_forum_perms_to_group_stmt.Exec(gid, forum.ID, forum.Preset, perms) - if err != nil { - return gid, err - } - - err = rebuildForumPermissions(forum.ID) - if err != nil { - return gid, err - } - } - permUpdateMutex.Unlock() - return gid, nil -} - -func groupExists(gid int) bool { - return (gid <= groupCapCount) && (gid > 0) && groups[gid].Name != "" -} diff --git a/group_store.go b/group_store.go index 9ae4f57a..70d9340d 100644 --- a/group_store.go +++ b/group_store.go @@ -1,3 +1,205 @@ +/* Under Heavy Construction */ package main -// TODO: Coming Soon. Probably in the next commit or two +import ( + "encoding/json" + "errors" + "log" + "sync" +) + +var groupCreateMutex sync.Mutex +var groupUpdateMutex sync.Mutex +var gstore GroupStore + +type GroupStore interface { + LoadGroups() error + DirtyGet(id int) *Group + Get(id int) (*Group, error) + GetCopy(id int) (Group, error) + Exists(id int) bool + Create(groupName string, tag string, isAdmin bool, isMod bool, isBanned bool) (int, error) + GetAll() ([]*Group, error) + GetRange(lower int, higher int) ([]*Group, error) +} + +type MemoryGroupStore struct { + groups []*Group // TODO: Use a sync.Map instead of a slice + groupCapCount int +} + +func NewMemoryGroupStore() *MemoryGroupStore { + return &MemoryGroupStore{} +} + +func (mgs *MemoryGroupStore) LoadGroups() error { + mgs.groups = []*Group{&Group{ID: 0, Name: "Unknown"}} + + rows, err := get_groups_stmt.Query() + if err != nil { + return err + } + defer rows.Close() + + i := 1 + for ; rows.Next(); i++ { + group := Group{ID: 0} + err := rows.Scan(&group.ID, &group.Name, &group.PermissionsText, &group.PluginPermsText, &group.IsMod, &group.IsAdmin, &group.IsBanned, &group.Tag) + if err != nil { + return err + } + + err = json.Unmarshal(group.PermissionsText, &group.Perms) + if err != nil { + return err + } + if dev.DebugMode { + log.Print(group.Name + ": ") + log.Printf("%+v\n", group.Perms) + } + + err = json.Unmarshal(group.PluginPermsText, &group.PluginPerms) + if err != nil { + return err + } + if dev.DebugMode { + log.Print(group.Name + ": ") + log.Printf("%+v\n", group.PluginPerms) + } + + //group.Perms.ExtData = make(map[string]bool) + mgs.groups = append(mgs.groups, &group) + } + err = rows.Err() + if err != nil { + return err + } + mgs.groupCapCount = i + + if dev.DebugMode { + log.Print("Binding the Not Loggedin Group") + } + GuestPerms = mgs.groups[6].Perms + return nil +} + +func (mgs *MemoryGroupStore) DirtyGet(gid int) *Group { + if !mgs.Exists(gid) { + return &blankGroup + } + return mgs.groups[gid] +} + +func (mgs *MemoryGroupStore) Get(gid int) (*Group, error) { + if !mgs.Exists(gid) { + return nil, ErrNoRows + } + return mgs.groups[gid], nil +} + +func (mgs *MemoryGroupStore) GetCopy(gid int) (Group, error) { + if !mgs.Exists(gid) { + return blankGroup, ErrNoRows + } + return *mgs.groups[gid], nil +} + +func (mgs *MemoryGroupStore) Exists(gid int) bool { + return (gid <= mgs.groupCapCount) && (gid > -1) && mgs.groups[gid].Name != "" +} + +func (mgs *MemoryGroupStore) Create(groupName string, tag string, isAdmin bool, isMod bool, isBanned bool) (int, error) { + groupCreateMutex.Lock() + defer groupCreateMutex.Unlock() + + var permstr = "{}" + res, err := create_group_stmt.Exec(groupName, tag, isAdmin, isMod, isBanned, permstr) + if err != nil { + return 0, err + } + + gid64, err := res.LastInsertId() + if err != nil { + return 0, err + } + var gid = int(gid64) + + var perms = BlankPerms + var blankForums []ForumPerms + var blankIntList []int + var pluginPerms = make(map[string]bool) + var pluginPermsBytes = []byte("{}") + if vhooks["create_group_preappend"] != nil { + runVhook("create_group_preappend", &pluginPerms, &pluginPermsBytes) + } + + mgs.groups = append(mgs.groups, &Group{gid, groupName, isMod, isAdmin, isBanned, tag, perms, []byte(permstr), pluginPerms, pluginPermsBytes, blankForums, blankIntList}) + + // Generate the forum permissions based on the presets... + fdata, err := fstore.GetAll() + if err != nil { + return 0, err + } + + permUpdateMutex.Lock() + defer permUpdateMutex.Unlock() + for _, forum := range fdata { + var thePreset string + if isAdmin { + thePreset = "admins" + } else if isMod { + thePreset = "staff" + } else if isBanned { + thePreset = "banned" + } else { + thePreset = "members" + } + + permmap := presetToPermmap(forum.Preset) + permitem := permmap[thePreset] + permitem.Overrides = true + permstr, err := json.Marshal(permitem) + if err != nil { + return gid, err + } + perms := string(permstr) + _, err = add_forum_perms_to_group_stmt.Exec(gid, forum.ID, forum.Preset, perms) + if err != nil { + return gid, err + } + + err = rebuildForumPermissions(forum.ID) + if err != nil { + return gid, err + } + } + return gid, nil +} + +// ! NOT CONCURRENT +func (mgs *MemoryGroupStore) GetAll() ([]*Group, error) { + return mgs.groups, nil +} + +// ? - It's currently faster to use GetAll(), but we'll be dropping the guarantee that the slices are ordered soon +// ? - Set the lower and higher numbers to 0 to remove the bounds +// ? - Currently uses slicing for efficiency, so it might behave a little weirdly +func (mgs *MemoryGroupStore) GetRange(lower int, higher int) (groups []*Group, err error) { + if lower == 0 && higher == 0 { + return mgs.GetAll() + } else if lower == 0 { + if higher < 0 { + return nil, errors.New("higher may not be lower than 0") + } + if higher > len(mgs.groups) { + higher = len(mgs.groups) + } + groups = mgs.groups[:higher] + } else if higher == 0 { + if lower < 0 { + return nil, errors.New("lower may not be lower than 0") + } + groups = mgs.groups[lower:] + } + return groups, nil +} diff --git a/main.go b/main.go index ab91e574..5f49027d 100644 --- a/main.go +++ b/main.go @@ -38,8 +38,6 @@ var startTime time.Time var externalSites = map[string]string{ "YT": "https://www.youtube.com/", } -var groups []Group -var groupCapCount int var staticFiles = make(map[string]SFile) var logWriter = io.MultiWriter(os.Stderr) diff --git a/member_routes.go b/member_routes.go index 4639f00d..763985b1 100644 --- a/member_routes.go +++ b/member_routes.go @@ -13,10 +13,9 @@ import ( "time" ) -// TODO: Replace some of these !user.Loggedin's with !user.Perms.ViewTopic // ? - Should we add a new permission or permission zone (like per-forum permissions) specifically for profile comment creation // ? - Should we allow banned users to make reports? How should we handle report abuse? -// TODO: Add a permission to stop certain users from using avatars +// TODO: Add a permission to stop certain users from using custom avatars // ? - Log username changes and put restrictions on this? func routeTopicCreate(w http.ResponseWriter, r *http.Request, user User, sfid string) { @@ -34,7 +33,7 @@ func routeTopicCreate(w http.ResponseWriter, r *http.Request, user User, sfid st if !ok { return } - if !user.Loggedin || !user.Perms.CreateTopic { + if !user.Perms.ViewTopic || !user.Perms.CreateTopic { NoPermissions(w, r, user) return } @@ -58,7 +57,12 @@ func routeTopicCreate(w http.ResponseWriter, r *http.Request, user User, sfid st return } } else { - group := groups[user.Group] + group, err := gstore.Get(user.Group) + if err != nil { + LocalError("Something weird happened behind the scenes", w, r, user) + log.Print("Group #" + strconv.Itoa(user.Group) + " doesn't exist, but it's set on User #" + strconv.Itoa(user.ID)) + return + } canSee = group.CanSee } @@ -110,7 +114,7 @@ func routeTopicCreateSubmit(w http.ResponseWriter, r *http.Request, user User) { if !ok { return } - if !user.Loggedin || !user.Perms.CreateTopic { + if !user.Perms.ViewTopic || !user.Perms.CreateTopic { NoPermissions(w, r, user) return } @@ -172,7 +176,7 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) { return } - topic, err := topics.CascadeGet(tid) + topic, err := topics.Get(tid) if err == ErrNoRows { PreError("Couldn't find the parent topic", w, r) return @@ -186,7 +190,7 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) { if !ok { return } - if !user.Loggedin || !user.Perms.CreateReply { + if !user.Perms.ViewTopic || !user.Perms.CreateReply { NoPermissions(w, r, user) return } @@ -239,7 +243,7 @@ func routeCreateReply(w http.ResponseWriter, r *http.Request, user User) { } // Reload the topic... - err = topics.Load(tid) + err = topics.Reload(tid) if err != nil && err == ErrNoRows { LocalError("The destination no longer exists", w, r, user) return @@ -269,7 +273,7 @@ func routeLikeTopic(w http.ResponseWriter, r *http.Request, user User) { return } - topic, err := topics.CascadeGet(tid) + topic, err := topics.Get(tid) if err == ErrNoRows { PreError("The requested topic doesn't exist.", w, r) return @@ -302,7 +306,7 @@ func routeLikeTopic(w http.ResponseWriter, r *http.Request, user User) { return } - _, err = users.CascadeGet(topic.CreatedBy) + _, err = users.Get(topic.CreatedBy) if err != nil && err == ErrNoRows { LocalError("The target user doesn't exist", w, r, user) return @@ -345,7 +349,7 @@ func routeLikeTopic(w http.ResponseWriter, r *http.Request, user User) { _ = wsHub.pushAlert(topic.CreatedBy, int(lastID), "like", "topic", user.ID, topic.CreatedBy, tid) // Reload the topic... - err = topics.Load(tid) + err = topics.Reload(tid) if err != nil && err == ErrNoRows { LocalError("The liked topic no longer exists", w, r, user) return @@ -413,7 +417,7 @@ func routeReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user User) { return } - _, err = users.CascadeGet(reply.CreatedBy) + _, err = users.Get(reply.CreatedBy) if err != nil && err != ErrNoRows { LocalError("The target user doesn't exist", w, r, user) return @@ -459,7 +463,7 @@ func routeReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user User) { } func routeProfileReplyCreate(w http.ResponseWriter, r *http.Request, user User) { - if !user.Loggedin || !user.Perms.CreateReply { + if !user.Perms.ViewTopic || !user.Perms.CreateReply { NoPermissions(w, r, user) return } @@ -540,7 +544,7 @@ func routeReportSubmit(w http.ResponseWriter, r *http.Request, user User, sitemI return } - topic, err := topics.CascadeGet(reply.ParentID) + topic, err := topics.Get(reply.ParentID) if err == ErrNoRows { LocalError("We weren't able to find the topic the reported post is supposed to be in", w, r, user) return @@ -813,7 +817,7 @@ func routeAccountOwnEditAvatarSubmit(w http.ResponseWriter, r *http.Request, use return } user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + "." + ext - err = users.Load(user.ID) + err = users.Reload(user.ID) if err != nil { LocalError("This user no longer exists!", w, r, user) return @@ -871,7 +875,7 @@ func routeAccountOwnEditUsernameSubmit(w http.ResponseWriter, r *http.Request, u // TODO: Use the reloaded data instead for the name? user.Name = newUsername - err = users.Load(user.ID) + err = users.Reload(user.ID) if err != nil { LocalError("Your account doesn't exist!", w, r, user) return diff --git a/misc_test.go b/misc_test.go index 13f77b9c..7bd7b401 100644 --- a/misc_test.go +++ b/misc_test.go @@ -15,24 +15,30 @@ func TestUserStore(t *testing.T) { initPlugins() } + users = NewMemoryUserStore(config.UserCacheCapacity) + userStoreTest(t) + users = NewSQLUserStore() + userStoreTest(t) +} +func userStoreTest(t *testing.T) { var user *User var err error - _, err = users.CascadeGet(-1) + _, err = users.Get(-1) if err == nil { t.Error("UID #-1 shouldn't exist") } else if err != ErrNoRows { t.Fatal(err) } - _, err = users.CascadeGet(0) + _, err = users.Get(0) if err == nil { t.Error("UID #0 shouldn't exist") } else if err != ErrNoRows { t.Fatal(err) } - user, err = users.CascadeGet(1) + user, err = users.Get(1) if err == ErrNoRows { t.Error("Couldn't find UID #1") } else if err != nil { @@ -45,38 +51,122 @@ func TestUserStore(t *testing.T) { // TODO: Lock onto the specific error type. Is this even possible without sacrificing the detailed information in the error message? var userList map[int]*User - _, err = users.BulkCascadeGetMap([]int{-1}) - if err == nil { - t.Error("UID #-1 shouldn't exist") + userList, _ = users.BulkGetMap([]int{-1}) + if len(userList) > 0 { + t.Error("There shouldn't be any results for UID #-1") } - _, err = users.BulkCascadeGetMap([]int{0}) - if err == nil { - t.Error("UID #0 shouldn't exist") - } - - userList, err = users.BulkCascadeGetMap([]int{1}) - if err == ErrNoRows { - t.Error("Couldn't find UID #1") - } else if err != nil { - t.Fatal(err) + userList, _ = users.BulkGetMap([]int{0}) + if len(userList) > 0 { + t.Error("There shouldn't be any results for UID #0") } + userList, _ = users.BulkGetMap([]int{1}) if len(userList) == 0 { - t.Error("The returned map is empty for UID #0") + t.Error("The returned map is empty for UID #1") } else if len(userList) > 1 { - t.Error("Too many results were returned for UID #0") + t.Error("Too many results were returned for UID #1") } user, ok := userList[1] if !ok { - t.Error("We couldn't find UID #0 in the returned map") + 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.") } + + ok = users.Exists(-1) + if ok { + t.Error("UID #-1 shouldn't exist") + } + + ok = users.Exists(0) + if ok { + t.Error("UID #0 shouldn't exist") + } + + ok = users.Exists(1) + if !ok { + t.Error("UID #1 should exist") + } + + count := users.GetGlobalCount() + if count <= 0 { + t.Error("The number of users should be bigger than zero") + t.Error("count", count) + } +} + +func TestTopicStore(t *testing.T) { + if !gloinited { + err := gloinit() + if err != nil { + t.Fatal(err) + } + } + if !pluginsInited { + initPlugins() + } + + topics = NewMemoryTopicStore(config.TopicCacheCapacity) + topicStoreTest(t) + topics = NewSQLTopicStore() + topicStoreTest(t) +} +func topicStoreTest(t *testing.T) { + var topic *Topic + var err error + + _, err = topics.Get(-1) + if err == nil { + t.Error("TID #-1 shouldn't exist") + } else if err != ErrNoRows { + t.Fatal(err) + } + + _, err = topics.Get(0) + if err == nil { + t.Error("TID #0 shouldn't exist") + } else if err != ErrNoRows { + t.Fatal(err) + } + + topic, err = topics.Get(1) + if err == ErrNoRows { + t.Error("Couldn't find TID #1") + } else if err != nil { + t.Fatal(err) + } + + if topic.ID != 1 { + t.Error("topic.ID does not match the requested TID. Got '" + strconv.Itoa(topic.ID) + "' instead.") + } + + // TODO: Add BulkGetMap() to the TopicStore + + ok := topics.Exists(-1) + if ok { + t.Error("TID #-1 shouldn't exist") + } + + ok = topics.Exists(0) + if ok { + t.Error("TID #0 shouldn't exist") + } + + ok = topics.Exists(1) + if !ok { + t.Error("TID #1 should exist") + } + + count := topics.GetGlobalCount() + if count <= 0 { + t.Error("The number of topics should be bigger than zero") + t.Error("count", count) + } } func TestForumStore(t *testing.T) { @@ -90,14 +180,14 @@ func TestForumStore(t *testing.T) { var forum *Forum var err error - _, err = fstore.CascadeGet(-1) + _, err = fstore.Get(-1) if err == nil { t.Error("FID #-1 shouldn't exist") } else if err != ErrNoRows { t.Fatal(err) } - forum, err = fstore.CascadeGet(0) + forum, err = fstore.Get(0) if err == ErrNoRows { t.Error("Couldn't find FID #0") } else if err != nil { @@ -111,7 +201,7 @@ func TestForumStore(t *testing.T) { t.Error("FID #0 is named '" + forum.Name + "' and not 'Uncategorised'") } - forum, err = fstore.CascadeGet(1) + forum, err = fstore.Get(1) if err == ErrNoRows { t.Error("Couldn't find FID #1") } else if err != nil { @@ -119,13 +209,13 @@ func TestForumStore(t *testing.T) { } if forum.ID != 1 { - t.Error("forum.ID doesn't not match the requested UID. Got '" + strconv.Itoa(forum.ID) + "' instead.'") + t.Error("forum.ID doesn't not match the requested FID. Got '" + strconv.Itoa(forum.ID) + "' instead.'") } if forum.Name != "Reports" { t.Error("FID #0 is named '" + forum.Name + "' and not 'Reports'") } - forum, err = fstore.CascadeGet(2) + forum, err = fstore.Get(2) if err == ErrNoRows { t.Error("Couldn't find FID #2") } else if err != nil { @@ -133,6 +223,84 @@ func TestForumStore(t *testing.T) { } _ = forum + + ok := fstore.Exists(-1) + if ok { + t.Error("FID #-1 shouldn't exist") + } + + ok = fstore.Exists(0) + if !ok { + t.Error("FID #0 should exist") + } + + ok = fstore.Exists(1) + if !ok { + t.Error("FID #1 should exist") + } +} + +func TestGroupStore(t *testing.T) { + if !gloinited { + gloinit() + } + if !pluginsInited { + initPlugins() + } + + var group *Group + var err error + + _, err = gstore.Get(-1) + if err == nil { + t.Error("GID #-1 shouldn't exist") + } else if err != ErrNoRows { + t.Fatal(err) + } + + group, err = gstore.Get(0) + if err == ErrNoRows { + t.Error("Couldn't find GID #0") + } else if err != nil { + t.Fatal(err) + } + + if group.ID != 0 { + t.Error("group.ID doesn't not match the requested GID. Got '" + strconv.Itoa(group.ID) + "' instead.") + } + if group.Name != "Unknown" { + t.Error("GID #0 is named '" + group.Name + "' and not 'Unknown'") + } + + // ? - What if they delete this group? x.x + // ? - Maybe, pick a random group ID? That would take an extra query, and I'm not sure if I want to be rewriting custom test queries. Possibly, a Random() method on the GroupStore? Seems useless for normal use, it might have some merit for the TopicStore though + group, err = gstore.Get(1) + if err == ErrNoRows { + t.Error("Couldn't find GID #1") + } else if err != nil { + t.Fatal(err) + } + + if group.ID != 1 { + t.Error("group.ID doesn't not match the requested GID. Got '" + strconv.Itoa(group.ID) + "' instead.'") + } + + _ = group + + ok := gstore.Exists(-1) + if ok { + t.Error("GID #-1 shouldn't exist") + } + + ok = gstore.Exists(0) + if !ok { + t.Error("GID #0 should exist") + } + + ok = gstore.Exists(1) + if !ok { + t.Error("GID #1 should exist") + } } func TestSlugs(t *testing.T) { diff --git a/mod_routes.go b/mod_routes.go index 379b19ed..6f8f8114 100644 --- a/mod_routes.go +++ b/mod_routes.go @@ -28,7 +28,7 @@ func routeEditTopic(w http.ResponseWriter, r *http.Request, user User) { return } - oldTopic, err := topics.CascadeGet(tid) + oldTopic, err := topics.Get(tid) if err == ErrNoRows { PreErrorJSQ("The topic you tried to edit doesn't exist.", w, r, isJs) return @@ -50,8 +50,9 @@ func routeEditTopic(w http.ResponseWriter, r *http.Request, user User) { topicName := r.PostFormValue("topic_name") topicStatus := r.PostFormValue("topic_status") isClosed := (topicStatus == "closed") - topicContent := html.EscapeString(r.PostFormValue("topic_content")) + + // TODO: Move this bit to the TopicStore _, err = edit_topic_stmt.Exec(topicName, preparseMessage(topicContent), parseMessage(html.EscapeString(preparseMessage(topicContent))), isClosed, tid) if err != nil { InternalErrorJSQ(err, w, r, isJs) @@ -94,7 +95,7 @@ func routeEditTopic(w http.ResponseWriter, r *http.Request, user User) { } } - err = topics.Load(tid) + err = topics.Reload(tid) if err == ErrNoRows { LocalErrorJSQ("This topic no longer exists!", w, r, user, isJs) return @@ -110,6 +111,7 @@ func routeEditTopic(w http.ResponseWriter, r *http.Request, user User) { } } +// TODO: Add support for soft-deletion and add a permission just for hard delete // TODO: Disable stat updates in posts handled by plugin_socialgroups func routeDeleteTopic(w http.ResponseWriter, r *http.Request, user User) { tid, err := strconv.Atoi(r.URL.Path[len("/topic/delete/submit/"):]) @@ -118,7 +120,7 @@ func routeDeleteTopic(w http.ResponseWriter, r *http.Request, user User) { return } - topic, err := topics.CascadeGet(tid) + topic, err := topics.Get(tid) if err == ErrNoRows { PreError("The topic you tried to delete doesn't exist.", w, r) return @@ -137,7 +139,8 @@ func routeDeleteTopic(w http.ResponseWriter, r *http.Request, user User) { return } - _, err = delete_topic_stmt.Exec(tid) + // We might be able to handle this err better + err = topics.Delete(topic.CreatedBy) if err != nil { InternalError(err, w) return @@ -154,7 +157,7 @@ func routeDeleteTopic(w http.ResponseWriter, r *http.Request, user User) { return } - // Might need soft-delete before we can do an action reply for this + // ? - We might need to add soft-delete before we can do an action reply for this /*_, err = create_action_reply_stmt.Exec(tid,"delete",ipaddress,user.ID) if err != nil { InternalError(err,w) @@ -163,26 +166,6 @@ func routeDeleteTopic(w http.ResponseWriter, r *http.Request, user User) { //log.Print("Topic #" + strconv.Itoa(tid) + " was deleted by User #" + strconv.Itoa(user.ID)) http.Redirect(w, r, "/", http.StatusSeeOther) - - topicCreator, err := users.CascadeGet(topic.CreatedBy) - if err == nil { - wcount := wordCount(topic.Content) - err = topicCreator.decreasePostStats(wcount, true) - if err != nil { - InternalError(err, w) - return - } - } else if err != ErrNoRows { - InternalError(err, w) - return - } - - err = fstore.DecrementTopicCount(topic.ParentID) - if err != nil && err != ErrNoRows { - InternalError(err, w) - return - } - topics.Remove(tid) } func routeStickTopic(w http.ResponseWriter, r *http.Request, user User) { @@ -192,7 +175,7 @@ func routeStickTopic(w http.ResponseWriter, r *http.Request, user User) { return } - topic, err := topics.CascadeGet(tid) + topic, err := topics.Get(tid) if err == ErrNoRows { PreError("The topic you tried to pin doesn't exist.", w, r) return @@ -233,7 +216,7 @@ func routeStickTopic(w http.ResponseWriter, r *http.Request, user User) { return } - err = topics.Load(tid) + err = topics.Reload(tid) if err != nil { LocalError("This topic doesn't exist!", w, r, user) return @@ -248,7 +231,7 @@ func routeUnstickTopic(w http.ResponseWriter, r *http.Request, user User) { return } - topic, err := topics.CascadeGet(tid) + topic, err := topics.Get(tid) if err == ErrNoRows { PreError("The topic you tried to unpin doesn't exist.", w, r) return @@ -289,7 +272,7 @@ func routeUnstickTopic(w http.ResponseWriter, r *http.Request, user User) { return } - err = topics.Load(tid) + err = topics.Reload(tid) if err != nil { LocalError("This topic doesn't exist!", w, r, user) return @@ -411,7 +394,7 @@ func routeReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user User) { w.Write(successJSONBytes) } - replyCreator, err := users.CascadeGet(reply.CreatedBy) + replyCreator, err := users.Get(reply.CreatedBy) if err == nil { wcount := wordCount(reply.Content) err = replyCreator.decreasePostStats(wcount, false) @@ -439,7 +422,7 @@ func routeReplyDeleteSubmit(w http.ResponseWriter, r *http.Request, user User) { return } - err = topics.Load(reply.ParentID) + err = topics.Reload(reply.ParentID) if err != nil { LocalError("This topic no longer exists!", w, r, user) return @@ -616,7 +599,7 @@ func routeIps(w http.ResponseWriter, r *http.Request, user User) { } // TODO: What if a user is deleted via the Control Panel? - userList, err := users.BulkCascadeGetMap(idSlice) + userList, err := users.BulkGetMap(idSlice) if err != nil { InternalError(err, w) return @@ -693,7 +676,7 @@ func routeBanSubmit(w http.ResponseWriter, r *http.Request, user User) { return }*/ - targetUser, err := users.CascadeGet(uid) + targetUser, err := users.Get(uid) if err == ErrNoRows { LocalError("The user you're trying to ban no longer exists.", w, r, user) return @@ -784,7 +767,7 @@ func routeUnban(w http.ResponseWriter, r *http.Request, user User) { return } - targetUser, err := users.CascadeGet(uid) + targetUser, err := users.Get(uid) if err == ErrNoRows { LocalError("The user you're trying to unban no longer exists.", w, r, user) return @@ -874,7 +857,7 @@ func routeActivate(w http.ResponseWriter, r *http.Request, user User) { return } - err = users.Load(uid) + err = users.Reload(uid) if err != nil { LocalError("This user no longer exists!", w, r, user) return diff --git a/pages.go b/pages.go index 29ff778c..cd57ba56 100644 --- a/pages.go +++ b/pages.go @@ -190,7 +190,7 @@ type PanelEditGroupPage struct { } type GroupForumPermPreset struct { - Group Group + Group *Group Preset string } @@ -474,7 +474,7 @@ func parseMessage(msg string /*, user User*/) string { tid, intLen := coerceIntBytes(msgbytes[start:]) i += intLen - topic, err := topics.CascadeGet(tid) + topic, err := topics.Get(tid) if err != nil || !fstore.Exists(topic.ParentID) { outbytes = append(outbytes, invalidTopic...) lastItem = i @@ -550,7 +550,7 @@ func parseMessage(msg string /*, user User*/) string { uid, intLen := coerceIntBytes(msgbytes[start:]) i += intLen - menUser, err := users.CascadeGet(uid) + menUser, err := users.Get(uid) if err != nil { outbytes = append(outbytes, invalidProfile...) lastItem = i diff --git a/panel_routes.go b/panel_routes.go index 2defe9d4..ddaea3bd 100644 --- a/panel_routes.go +++ b/panel_routes.go @@ -244,7 +244,7 @@ func routePanelForumsCreateSubmit(w http.ResponseWriter, r *http.Request, user U factive := r.PostFormValue("forum-name") active := (factive == "on" || factive == "1") - _, err = fstore.CreateForum(fname, fdesc, active, fpreset) + _, err = fstore.Create(fname, fdesc, active, fpreset) if err != nil { InternalError(err, w) return @@ -274,7 +274,7 @@ func routePanelForumsDelete(w http.ResponseWriter, r *http.Request, user User, s return } - forum, err := fstore.CascadeGet(fid) + forum, err := fstore.Get(fid) if err == ErrNoRows { LocalError("The forum you're trying to delete doesn't exist.", w, r, user) return @@ -318,7 +318,7 @@ func routePanelForumsDeleteSubmit(w http.ResponseWriter, r *http.Request, user U return } - err = fstore.CascadeDelete(fid) + err = fstore.Delete(fid) if err == ErrNoRows { LocalError("The forum you're trying to delete doesn't exist.", w, r, user) return @@ -346,7 +346,7 @@ func routePanelForumsEdit(w http.ResponseWriter, r *http.Request, user User, sfi return } - forum, err := fstore.CascadeGet(fid) + forum, err := fstore.Get(fid) if err == ErrNoRows { LocalError("The forum you're trying to edit doesn't exist.", w, r, user) return @@ -359,7 +359,12 @@ func routePanelForumsEdit(w http.ResponseWriter, r *http.Request, user User, sfi forum.Preset = "custom" } - var glist = groups + glist, err := gstore.GetAll() + if err != nil { + InternalError(err, w) + return + } + var gplist []GroupForumPermPreset for gid, group := range glist { if gid == 0 { @@ -412,7 +417,7 @@ func routePanelForumsEditSubmit(w http.ResponseWriter, r *http.Request, user Use forumPreset := stripInvalidPreset(r.PostFormValue("forum_preset")) forumActive := r.PostFormValue("forum_active") - forum, err := fstore.CascadeGet(fid) + forum, err := fstore.Get(fid) if err == ErrNoRows { LocalErrorJSQ("The forum you're trying to edit doesn't exist.", w, r, user, isJs) return @@ -499,7 +504,7 @@ func routePanelForumsEditPermsSubmit(w http.ResponseWriter, r *http.Request, use permPreset := stripInvalidGroupForumPreset(r.PostFormValue("perm_preset")) fperms, changed := groupForumPresetToForumPerms(permPreset) - forum, err := fstore.CascadeGet(fid) + forum, err := fstore.Get(fid) if err == ErrNoRows { LocalErrorJSQ("This forum doesn't exist", w, r, user, isJs) return @@ -512,7 +517,13 @@ func routePanelForumsEditPermsSubmit(w http.ResponseWriter, r *http.Request, use defer forumUpdateMutex.Unlock() if changed { permUpdateMutex.Lock() - groups[gid].Forums[fid] = fperms + defer permUpdateMutex.Unlock() + group, err := gstore.Get(gid) + if err != nil { + LocalError("The group whose permissions you're updating doesn't exist.", w, r, user) + return + } + group.Forums[fid] = fperms perms, err := json.Marshal(fperms) if err != nil { @@ -525,7 +536,6 @@ func routePanelForumsEditPermsSubmit(w http.ResponseWriter, r *http.Request, use InternalErrorJSQ(err, w, r, isJs) return } - permUpdateMutex.Unlock() _, err = update_forum_stmt.Exec(forum.Name, forum.Desc, forum.Active, "", fid) if err != nil { @@ -1159,8 +1169,8 @@ func routePanelUsers(w http.ResponseWriter, r *http.Request, user User) { puser.Avatar = strings.Replace(config.Noavatar, "{id}", strconv.Itoa(puser.ID), 1) } - if groups[puser.Group].Tag != "" { - puser.Tag = groups[puser.Group].Tag + if gstore.DirtyGet(puser.Group).Tag != "" { + puser.Tag = gstore.DirtyGet(puser.Group).Tag } else { puser.Tag = "" } @@ -1202,7 +1212,7 @@ func routePanelUsersEdit(w http.ResponseWriter, r *http.Request, user User, suid return } - targetUser, err := users.CascadeGet(uid) + targetUser, err := users.Get(uid) if err == ErrNoRows { LocalError("The user you're trying to edit doesn't exist.", w, r, user) return @@ -1216,6 +1226,12 @@ func routePanelUsersEdit(w http.ResponseWriter, r *http.Request, user User, suid return } + groups, err := gstore.GetRange(1, 0) // ? - 0 = Go to the end + if err != nil { + InternalError(err, w) + return // ? - Should we stop admins from deleting all the groups? Maybe, protect the group they're currently using? + } + var groupList []interface{} for _, group := range groups[1:] { if !user.Perms.EditUserGroupAdmin && group.IsAdmin { @@ -1259,7 +1275,7 @@ func routePanelUsersEditSubmit(w http.ResponseWriter, r *http.Request, user User return } - targetUser, err := users.CascadeGet(uid) + targetUser, err := users.Get(uid) if err == ErrNoRows { LocalError("The user you're trying to edit doesn't exist.", w, r, user) return @@ -1301,16 +1317,20 @@ func routePanelUsersEditSubmit(w http.ResponseWriter, r *http.Request, user User return } - if (newgroup > groupCapCount) || (newgroup < 0) || groups[newgroup].Name == "" { + group, err := gstore.Get(newgroup) + if err == ErrNoRows { LocalError("The group you're trying to place this user in doesn't exist.", w, r, user) return + } else if err != nil { + InternalError(err, w) + return } - if !user.Perms.EditUserGroupAdmin && groups[newgroup].IsAdmin { + if !user.Perms.EditUserGroupAdmin && group.IsAdmin { LocalError("You need the EditUserGroupAdmin permission to assign someone to an administrator group.", w, r, user) return } - if !user.Perms.EditUserGroupSuperMod && groups[newgroup].IsMod { + if !user.Perms.EditUserGroupSuperMod && group.IsMod { LocalError("You need the EditUserGroupAdmin permission to assign someone to a super mod group.", w, r, user) return } @@ -1325,7 +1345,7 @@ func routePanelUsersEditSubmit(w http.ResponseWriter, r *http.Request, user User SetPassword(targetUser.ID, newpassword) } - err = users.Load(targetUser.ID) + err = users.Reload(targetUser.ID) if err != nil { LocalError("This user no longer exists!", w, r, user) return @@ -1349,7 +1369,8 @@ func routePanelGroups(w http.ResponseWriter, r *http.Request, user User) { var count int var groupList []GroupAdmin - for _, group := range groups[offset:] { + groups, _ := gstore.GetRange(offset, 0) + for _, group := range groups { if count == perPage { break } @@ -1412,13 +1433,16 @@ func routePanelGroupsEdit(w http.ResponseWriter, r *http.Request, user User, sgi return } - if !groupExists(gid) { + group, err := gstore.Get(gid) + if err == ErrNoRows { //log.Print("aaaaa monsters") NotFound(w, r) return + } else if err != nil { + InternalError(err, w) + return } - group := groups[gid] if group.IsAdmin && !user.Perms.EditGroupAdmin { LocalError("You need the EditGroupAdmin permission to edit an admin group.", w, r, user) return @@ -1471,13 +1495,16 @@ func routePanelGroupsEditPerms(w http.ResponseWriter, r *http.Request, user User return } - if !groupExists(gid) { + group, err := gstore.Get(gid) + if err == ErrNoRows { //log.Print("aaaaa monsters") NotFound(w, r) return + } else if err != nil { + InternalError(err, w) + return } - group := groups[gid] if group.IsAdmin && !user.Perms.EditGroupAdmin { LocalError("You need the EditGroupAdmin permission to edit an admin group.", w, r, user) return @@ -1554,13 +1581,16 @@ func routePanelGroupsEditSubmit(w http.ResponseWriter, r *http.Request, user Use return } - if !groupExists(gid) { + group, err := gstore.Get(gid) + if err == ErrNoRows { //log.Print("aaaaa monsters") NotFound(w, r) return + } else if err != nil { + InternalError(err, w) + return } - group := groups[gid] if group.IsAdmin && !user.Perms.EditGroupAdmin { LocalError("You need the EditGroupAdmin permission to edit an admin group.", w, r, user) return @@ -1611,9 +1641,9 @@ func routePanelGroupsEditSubmit(w http.ResponseWriter, r *http.Request, user Use InternalError(err, w) return } - groups[gid].IsAdmin = true - groups[gid].IsMod = true - groups[gid].IsBanned = false + group.IsAdmin = true + group.IsMod = true + group.IsBanned = false case "Mod": if !user.Perms.EditGroupSuperMod { LocalError("You need the EditGroupSuperMod permission to designate this group as a super-mod group.", w, r, user) @@ -1625,18 +1655,18 @@ func routePanelGroupsEditSubmit(w http.ResponseWriter, r *http.Request, user Use InternalError(err, w) return } - groups[gid].IsAdmin = false - groups[gid].IsMod = true - groups[gid].IsBanned = false + group.IsAdmin = false + group.IsMod = true + group.IsBanned = false case "Banned": _, err = update_group_rank_stmt.Exec(0, 0, 1, gid) if err != nil { InternalError(err, w) return } - groups[gid].IsAdmin = false - groups[gid].IsMod = false - groups[gid].IsBanned = true + group.IsAdmin = false + group.IsMod = false + group.IsBanned = true case "Guest": LocalError("You can't designate a group as a guest group.", w, r, user) return @@ -1646,9 +1676,9 @@ func routePanelGroupsEditSubmit(w http.ResponseWriter, r *http.Request, user Use InternalError(err, w) return } - groups[gid].IsAdmin = false - groups[gid].IsMod = false - groups[gid].IsBanned = false + group.IsAdmin = false + group.IsMod = false + group.IsBanned = false default: LocalError("Invalid group type.", w, r, user) return @@ -1660,8 +1690,8 @@ func routePanelGroupsEditSubmit(w http.ResponseWriter, r *http.Request, user Use InternalError(err, w) return } - groups[gid].Name = gname - groups[gid].Tag = gtag + group.Name = gname + group.Tag = gtag http.Redirect(w, r, "/panel/groups/edit/"+strconv.Itoa(gid), http.StatusSeeOther) } @@ -1686,13 +1716,16 @@ func routePanelGroupsEditPermsSubmit(w http.ResponseWriter, r *http.Request, use return } - if !groupExists(gid) { + group, err := gstore.Get(gid) + if err == ErrNoRows { //log.Print("aaaaa monsters o.o") NotFound(w, r) return + } else if err != nil { + InternalError(err, w) + return } - group := groups[gid] if group.IsAdmin && !user.Perms.EditGroupAdmin { LocalError("You need the EditGroupAdmin permission to edit an admin group.", w, r, user) return @@ -1784,7 +1817,7 @@ func routePanelGroupsCreateSubmit(w http.ResponseWriter, r *http.Request, user U } } - gid, err := createGroup(groupName, groupTag, isAdmin, isMod, isBanned) + gid, err := gstore.Create(groupName, groupTag, isAdmin, isMod, isBanned) if err != nil { InternalError(err, w) return @@ -1942,32 +1975,32 @@ func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user User) { return } - actor, err := users.CascadeGet(actorID) + actor, err := users.Get(actorID) if err != nil { actor = &User{Name: "Unknown", Link: buildProfileURL("unknown", 0)} } switch action { case "lock": - topic, err := topics.CascadeGet(elementID) + topic, err := topics.Get(elementID) if err != nil { topic = &Topic{Title: "Unknown", Link: buildProfileURL("unknown", 0)} } action = "" + topic.Title + " was locked by " + actor.Name + "" case "unlock": - topic, err := topics.CascadeGet(elementID) + topic, err := topics.Get(elementID) if err != nil { topic = &Topic{Title: "Unknown", Link: buildProfileURL("unknown", 0)} } action = "" + topic.Title + " was reopened by " + actor.Name + "" case "stick": - topic, err := topics.CascadeGet(elementID) + topic, err := topics.Get(elementID) if err != nil { topic = &Topic{Title: "Unknown", Link: buildProfileURL("unknown", 0)} } action = "" + topic.Title + " was pinned by " + actor.Name + "" case "unstick": - topic, err := topics.CascadeGet(elementID) + topic, err := topics.Get(elementID) if err != nil { topic = &Topic{Title: "Unknown", Link: buildProfileURL("unknown", 0)} } @@ -1983,19 +2016,19 @@ func routePanelLogsMod(w http.ResponseWriter, r *http.Request, user User) { action = "A reply in " + topic.Title + " was deleted by " + actor.Name + "" } case "ban": - targetUser, err := users.CascadeGet(elementID) + targetUser, err := users.Get(elementID) if err != nil { targetUser = &User{Name: "Unknown", Link: buildProfileURL("unknown", 0)} } action = "" + targetUser.Name + " was banned by " + actor.Name + "" case "unban": - targetUser, err := users.CascadeGet(elementID) + targetUser, err := users.Get(elementID) if err != nil { targetUser = &User{Name: "Unknown", Link: buildProfileURL("unknown", 0)} } action = "" + targetUser.Name + " was unbanned by " + actor.Name + "" case "activate": - targetUser, err := users.CascadeGet(elementID) + targetUser, err := users.Get(elementID) if err != nil { targetUser = &User{Name: "Unknown", Link: buildProfileURL("unknown", 0)} } diff --git a/permissions.go b/permissions.go index f127a213..094216b4 100644 --- a/permissions.go +++ b/permissions.go @@ -298,6 +298,7 @@ func permmapToQuery(permmap map[string]ForumPerms, fid int) error { return rebuildForumPermissions(fid) } +// TODO: Need a more thread-safe way of doing this. Possibly with sync.Map? func rebuildForumPermissions(fid int) error { if dev.DebugMode { log.Print("Loading the forum permissions") @@ -336,38 +337,44 @@ func rebuildForumPermissions(fid int) error { } forumPerms[gid][fid] = pperms } - for gid := range groups { + + groups, err := gstore.GetAll() + if err != nil { + return err + } + + for _, group := range groups { if dev.DebugMode { - log.Print("Updating the forum permissions for Group #" + strconv.Itoa(gid)) + log.Print("Updating the forum permissions for Group #" + strconv.Itoa(group.ID)) } var blankList []ForumPerms var blankIntList []int - groups[gid].Forums = blankList - groups[gid].CanSee = blankIntList + group.Forums = blankList + group.CanSee = blankIntList for ffid := range forums { - forumPerm, ok := forumPerms[gid][ffid] + forumPerm, ok := forumPerms[group.ID][ffid] if ok { //log.Print("Overriding permissions for forum #" + strconv.Itoa(fid)) - groups[gid].Forums = append(groups[gid].Forums, forumPerm) + group.Forums = append(group.Forums, forumPerm) } else { //log.Print("Inheriting from default for forum #" + strconv.Itoa(fid)) forumPerm = BlankForumPerms - groups[gid].Forums = append(groups[gid].Forums, forumPerm) + group.Forums = append(group.Forums, forumPerm) } if forumPerm.Overrides { if forumPerm.ViewTopic { - groups[gid].CanSee = append(groups[gid].CanSee, ffid) + group.CanSee = append(group.CanSee, ffid) } - } else if groups[gid].Perms.ViewTopic { - groups[gid].CanSee = append(groups[gid].CanSee, ffid) + } else if group.Perms.ViewTopic { + group.CanSee = append(group.CanSee, ffid) } } if dev.SuperDebug { - log.Printf("groups[gid].CanSee %+v\n", groups[gid].CanSee) - log.Printf("groups[gid].Forums %+v\n", groups[gid].Forums) - log.Print("len(groups[gid].Forums)", len(groups[gid].Forums)) + log.Printf("group.CanSee %+v\n", group.CanSee) + log.Printf("group.Forums %+v\n", group.Forums) + log.Print("len(group.Forums)", len(group.Forums)) } } return nil @@ -410,30 +417,36 @@ func buildForumPermissions() error { } forumPerms[gid][fid] = pperms } - for gid := range groups { + + groups, err := gstore.GetAll() + if err != nil { + return err + + } + for _, group := range groups { if dev.DebugMode { - log.Print("Adding the forum permissions for Group #" + strconv.Itoa(gid) + " - " + groups[gid].Name) + log.Print("Adding the forum permissions for Group #" + strconv.Itoa(group.ID) + " - " + group.Name) } //groups[gid].Forums = append(groups[gid].Forums,BlankForumPerms) // GID 0. No longer needed now that Uncategorised occupies that slot for fid := range forums { - forumPerm, ok := forumPerms[gid][fid] + forumPerm, ok := forumPerms[group.ID][fid] if ok { // Override group perms //log.Print("Overriding permissions for forum #" + strconv.Itoa(fid)) - groups[gid].Forums = append(groups[gid].Forums, forumPerm) + group.Forums = append(group.Forums, forumPerm) } else { // Inherit from Group //log.Print("Inheriting from default for forum #" + strconv.Itoa(fid)) forumPerm = BlankForumPerms - groups[gid].Forums = append(groups[gid].Forums, forumPerm) + group.Forums = append(group.Forums, forumPerm) } if forumPerm.Overrides { if forumPerm.ViewTopic { - groups[gid].CanSee = append(groups[gid].CanSee, fid) + group.CanSee = append(group.CanSee, fid) } - } else if groups[gid].Perms.ViewTopic { - groups[gid].CanSee = append(groups[gid].CanSee, fid) + } else if group.Perms.ViewTopic { + group.CanSee = append(group.CanSee, fid) } } if dev.SuperDebug { @@ -547,7 +560,11 @@ func rebuildGroupPermissions(gid int) error { return err } - groups[gid].Perms = tmpPerms + group, err := gstore.Get(gid) + if err != nil { + return err + } + group.Perms = tmpPerms return nil } diff --git a/plugin_socialgroups.go b/plugin_socialgroups.go index ae54db21..f0b488f7 100644 --- a/plugin_socialgroups.go +++ b/plugin_socialgroups.go @@ -395,7 +395,7 @@ func socialgroupsCreateGroupSubmit(w http.ResponseWriter, r *http.Request, user } // Create the backing forum - fid, err := fstore.CreateForum(groupName, "", true, "") + fid, err := fstore.Create(groupName, "", true, "") if err != nil { InternalError(err, w) return diff --git a/reply.go b/reply.go index 201d13a2..ff1adfb6 100644 --- a/reply.go +++ b/reply.go @@ -6,7 +6,7 @@ */ package main -// Should we add a reply store to centralise all the reply logic? Would this cover profile replies too or would that be seperate? +// ? - Should we add a reply store to centralise all the reply logic? Would this cover profile replies too or would that be seperate? type Reply struct /* Should probably rename this to ReplyUser and rename ReplyShort to Reply */ { diff --git a/routes.go b/routes.go index 4f5057d6..e68194b9 100644 --- a/routes.go +++ b/routes.go @@ -157,7 +157,13 @@ func routeTopics(w http.ResponseWriter, r *http.Request, user User) { var qlist string var fidList []interface{} - group := groups[user.Group] + group, err := gstore.Get(user.Group) + if err != nil { + LocalError("Something weird happened", w, r, user) + log.Print("Group #" + strconv.Itoa(user.Group) + " doesn't exist despite being used by User #" + strconv.Itoa(user.ID)) + return + } + for _, fid := range group.CanSee { if fstore.DirtyGet(fid).Name != "" { fidList = append(fidList, strconv.Itoa(fid)) @@ -232,7 +238,7 @@ func routeTopics(w http.ResponseWriter, r *http.Request, user User) { } // TODO: What if a user is deleted via the Control Panel? - userList, err := users.BulkCascadeGetMap(idSlice) + userList, err := users.BulkGetMap(idSlice) if err != nil { InternalError(err, w) return @@ -279,7 +285,7 @@ func routeForum(w http.ResponseWriter, r *http.Request, user User, sfid string) } // TODO: Fix this double-check - forum, err := fstore.CascadeGet(fid) + forum, err := fstore.Get(fid) if err == ErrNoRows { NotFound(w, r) return @@ -347,7 +353,7 @@ func routeForum(w http.ResponseWriter, r *http.Request, user User, sfid string) } // TODO: What if a user is deleted via the Control Panel? - userList, err := users.BulkCascadeGetMap(idSlice) + userList, err := users.BulkGetMap(idSlice) if err != nil { InternalError(err, w) return @@ -387,7 +393,12 @@ func routeForums(w http.ResponseWriter, r *http.Request, user User) { } //log.Print("canSee",canSee) } else { - group := groups[user.Group] + group, err := gstore.Get(user.Group) + if err != nil { + LocalError("Something weird happened", w, r, user) + log.Print("Group #" + strconv.Itoa(user.Group) + " doesn't exist despite being used by User #" + strconv.Itoa(user.ID)) + return + } canSee = group.CanSee //log.Print("group.CanSee",group.CanSee) } @@ -471,8 +482,14 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) { user.Perms.CreateReply = false } - topic.Tag = groups[topic.Group].Tag - if groups[topic.Group].IsMod || groups[topic.Group].IsAdmin { + postGroup, err := gstore.Get(topic.Group) + if err != nil { + InternalError(err, w) + return + } + + topic.Tag = postGroup.Tag + if postGroup.IsMod || postGroup.IsAdmin { topic.ClassName = config.StaffCss } @@ -527,7 +544,13 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) { replyItem.ContentHtml = parseMessage(replyItem.Content) replyItem.ContentLines = strings.Count(replyItem.Content, "\n") - if groups[replyItem.Group].IsMod || groups[replyItem.Group].IsAdmin { + postGroup, err = gstore.Get(replyItem.Group) + if err != nil { + InternalError(err, w) + return + } + + if postGroup.IsMod || postGroup.IsAdmin { replyItem.ClassName = config.StaffCss } else { replyItem.ClassName = "" @@ -541,7 +564,7 @@ func routeTopicID(w http.ResponseWriter, r *http.Request, user User) { replyItem.Avatar = strings.Replace(config.Noavatar, "{id}", strconv.Itoa(replyItem.CreatedBy), 1) } - replyItem.Tag = groups[replyItem.Group].Tag + replyItem.Tag = postGroup.Tag /*if headerVars.Settings["url_tags"] == false { replyItem.URLName = "" @@ -631,7 +654,7 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user User) { puser = &user } else { // Fetch the user data - puser, err = users.CascadeGet(pid) + puser, err = users.Get(pid) if err == ErrNoRows { NotFound(w, r) return @@ -656,8 +679,14 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user User) { return } + group, err := gstore.Get(replyGroup) + if err != nil { + InternalError(err, w) + return + } + replyLines = strings.Count(replyContent, "\n") - if groups[replyGroup].IsMod || groups[replyGroup].IsAdmin { + if group.IsMod || group.IsAdmin { replyClassName = config.StaffCss } else { replyClassName = "" @@ -670,8 +699,8 @@ func routeProfile(w http.ResponseWriter, r *http.Request, user User) { replyAvatar = strings.Replace(config.Noavatar, "{id}", strconv.Itoa(replyCreatedBy), 1) } - if groups[replyGroup].Tag != "" { - replyTag = groups[replyGroup].Tag + if group.Tag != "" { + replyTag = group.Tag } else if puser.ID == replyCreatedBy { replyTag = "Profile Owner" } else { @@ -739,7 +768,7 @@ func routeLoginSubmit(w http.ResponseWriter, r *http.Request, user User) { return } - userPtr, err := users.CascadeGet(uid) + userPtr, err := users.Get(uid) if err != nil { LocalError("Bad account", w, r, user) return @@ -848,7 +877,7 @@ func routeRegisterSubmit(w http.ResponseWriter, r *http.Request, user User) { group = config.ActivationGroup } - uid, err := users.CreateUser(username, password, email, group, active) + uid, err := users.Create(username, password, email, group, active) if err == errAccountExists { LocalError("This username isn't available. Try another.", w, r, user) return diff --git a/routes_common.go b/routes_common.go index f8007d23..5fdf8c7d 100644 --- a/routes_common.go +++ b/routes_common.go @@ -3,19 +3,23 @@ package main import ( "html" "html/template" + "log" "net" "net/http" + "strconv" "strings" ) // nolint var PreRoute func(http.ResponseWriter, *http.Request) (User, bool) = preRoute +// TODO: Come up with a better middleware solution // nolint We need these types so people can tell what they are without scrolling to the bottom of the file var PanelUserCheck func(http.ResponseWriter, *http.Request, *User) (*HeaderVars, PanelStats, bool) = panelUserCheck var SimplePanelUserCheck func(http.ResponseWriter, *http.Request, *User) (*HeaderLite, bool) = simplePanelUserCheck var SimpleForumUserCheck func(w http.ResponseWriter, r *http.Request, user *User, fid int) (headerLite *HeaderLite, success bool) = simpleForumUserCheck var ForumUserCheck func(w http.ResponseWriter, r *http.Request, user *User, fid int) (headerVars *HeaderVars, success bool) = forumUserCheck +var MemberCheck func(w http.ResponseWriter, r *http.Request, user *User) (headerVars *HeaderVars, success bool) = memberCheck var SimpleUserCheck func(w http.ResponseWriter, r *http.Request, user *User) (headerLite *HeaderLite, success bool) = simpleUserCheck var UserCheck func(w http.ResponseWriter, r *http.Request, user *User) (headerVars *HeaderVars, success bool) = userCheck @@ -58,7 +62,14 @@ func simpleForumUserCheck(w http.ResponseWriter, r *http.Request, user *User, fi } } - fperms := groups[user.Group].Forums[fid] + group, err := gstore.Get(user.Group) + if err != nil { + PreError("Something weird happened", w, r) + log.Print("Group #" + strconv.Itoa(user.Group) + " doesn't exist despite being used by User #" + strconv.Itoa(user.ID)) + return + } + + fperms := group.Forums[fid] if fperms.Overrides && !user.IsSuperAdmin { user.Perms.ViewTopic = fperms.ViewTopic user.Perms.LikeItem = fperms.LikeItem @@ -93,7 +104,14 @@ func forumUserCheck(w http.ResponseWriter, r *http.Request, user *User, fid int) } } - fperms := groups[user.Group].Forums[fid] + group, err := gstore.Get(user.Group) + if err != nil { + PreError("Something weird happened", w, r) + log.Print("Group #" + strconv.Itoa(user.Group) + " doesn't exist despite being used by User #" + strconv.Itoa(user.ID)) + return + } + + fperms := group.Forums[fid] //log.Printf("user.Perms: %+v\n", user.Perms) //log.Printf("fperms: %+v\n", fperms) if fperms.Overrides && !user.IsSuperAdmin { @@ -201,6 +219,16 @@ func simplePanelUserCheck(w http.ResponseWriter, r *http.Request, user *User) (h return headerLite, true } +// TODO: Add this to the member routes +func memberCheck(w http.ResponseWriter, r *http.Request, user *User) (headerVars *HeaderVars, success bool) { + headerVars, success = UserCheck(w, r, user) + if !user.Loggedin { + NoPermissions(w, r, *user) + return headerVars, false + } + return headerVars, success +} + // SimpleUserCheck is back from the grave, yay :D func simpleUserCheck(w http.ResponseWriter, r *http.Request, user *User) (headerLite *HeaderLite, success bool) { headerLite = &HeaderLite{ diff --git a/tasks.go b/tasks.go index 11a5e71f..5ad4a56b 100644 --- a/tasks.go +++ b/tasks.go @@ -35,7 +35,7 @@ func handleExpiredScheduledGroups() error { if err != nil { return err } - _ = users.Load(uid) + _ = users.Reload(uid) } return rows.Err() } diff --git a/template_list.go b/template_list.go index 4d3c982e..0ed3f9c2 100644 --- a/template_list.go +++ b/template_list.go @@ -453,47 +453,48 @@ var profile_0 = []byte(` -
+
`) +var profile_2 = []byte(` `) -var profile_2 = []byte(``) -var profile_3 = []byte(``) -var profile_4 = []byte(``) -var profile_5 = []byte(` +var profile_3 = []byte(``) +var profile_4 = []byte(``) +var profile_5 = []byte(``) +var profile_6 = []byte(`
Add Friend
`) -var profile_6 = []byte(`
+var profile_7 = []byte(`
`) -var profile_7 = []byte(`Unban +var profile_8 = []byte(`Unban `) -var profile_10 = []byte(`Ban`) -var profile_11 = []byte(` -
`) +var profile_11 = []byte(`Ban`) var profile_12 = []byte(` +
`) +var profile_13 = []byte(`
Report +var profile_14 = []byte(`?session=`) +var profile_15 = []byte(`&type=user" class="profile_menu_item report_item">Report
`) -var profile_15 = []byte(` +var profile_16 = []byte(`