Refactored the GroupStore to make it more thread-safe.

Refactored the store initialisers to better propogate errors upwards.
Moved the user initialisation logic to a method on *User.
Added the Reload and GlobalCount methods to the GroupStore.
Added the CacheSet method to the GroupCache.

Renamed plugin_socialgroups to plugin_guilds 3/3
This commit is contained in:
Azareal 2017-11-02 13:35:19 +00:00
parent 3c13f4da7f
commit d0363f3eb1
18 changed files with 347 additions and 295 deletions

View File

@ -21,7 +21,10 @@ func initDatabase() (err error) {
}
log.Print("Loading the usergroups.")
gstore = NewMemoryGroupStore()
gstore, err = NewMemoryGroupStore()
if err != nil {
return err
}
err = gstore.LoadGroups()
if err != nil {
return err
@ -30,15 +33,30 @@ func initDatabase() (err error) {
// We have to put this here, otherwise LoadForums() won't be able to get the last poster data when building it's forums
log.Print("Initialising the user and topic stores")
if config.CacheTopicUser == CACHE_STATIC {
users = NewMemoryUserStore(config.UserCacheCapacity)
topics = NewMemoryTopicStore(config.TopicCacheCapacity)
users, err = NewMemoryUserStore(config.UserCacheCapacity)
if err != nil {
return err
}
topics, err = NewMemoryTopicStore(config.TopicCacheCapacity)
if err != nil {
return err
}
} else {
users = NewSQLUserStore()
topics = NewSQLTopicStore()
users, err = NewSQLUserStore()
if err != nil {
return err
}
topics, err = NewSQLTopicStore()
if err != nil {
return err
}
}
log.Print("Loading the forums.")
fstore = NewMemoryForumStore()
fstore, err = NewMemoryForumStore()
if err != nil {
return err
}
err = fstore.LoadForums()
if err != nil {
return err

View File

@ -37,9 +37,8 @@ func (err *RouteErrorImpl) Type() string {
// System errors may contain sensitive information we don't want the user to see
if err.system {
return "system"
} else {
return "user"
}
return "user"
}
func (err *RouteErrorImpl) Error() string {
@ -101,9 +100,8 @@ func InternalError(err error, w http.ResponseWriter, r *http.Request) RouteError
func InternalErrorJSQ(err error, w http.ResponseWriter, r *http.Request, isJs bool) RouteError {
if !isJs {
return InternalError(err, w, r)
} else {
return InternalErrorJS(err, w, r)
}
return InternalErrorJS(err, w, r)
}
// InternalErrorJS is the JSON version of InternalError on routes we know will only be requested via JSON. E.g. An API.
@ -160,9 +158,8 @@ func PreErrorJS(errmsg string, w http.ResponseWriter, r *http.Request) RouteErro
func PreErrorJSQ(errmsg string, w http.ResponseWriter, r *http.Request, isJs bool) RouteError {
if !isJs {
return PreError(errmsg, w, r)
} else {
return PreErrorJS(errmsg, w, r)
}
return PreErrorJS(errmsg, w, r)
}
// LocalError is an error shown to the end-user when something goes wrong and it's not the software's fault
@ -184,9 +181,8 @@ func LocalError(errmsg string, w http.ResponseWriter, r *http.Request, user User
func LocalErrorJSQ(errmsg string, w http.ResponseWriter, r *http.Request, user User, isJs bool) RouteError {
if !isJs {
return LocalError(errmsg, w, r, user)
} else {
return LocalErrorJS(errmsg, w, r)
}
return LocalErrorJS(errmsg, w, r)
}
func LocalErrorJS(errmsg string, w http.ResponseWriter, r *http.Request) RouteError {
@ -216,9 +212,8 @@ func NoPermissions(w http.ResponseWriter, r *http.Request, user User) RouteError
func NoPermissionsJSQ(w http.ResponseWriter, r *http.Request, user User, isJs bool) RouteError {
if !isJs {
return NoPermissions(w, r, user)
} else {
return NoPermissionsJS(w, r, user)
}
return NoPermissionsJS(w, r, user)
}
func NoPermissionsJS(w http.ResponseWriter, r *http.Request, user User) RouteError {
@ -248,9 +243,8 @@ func Banned(w http.ResponseWriter, r *http.Request, user User) RouteError {
func BannedJSQ(w http.ResponseWriter, r *http.Request, user User, isJs bool) RouteError {
if !isJs {
return Banned(w, r, user)
} else {
return BannedJS(w, r, user)
}
return BannedJS(w, r, user)
}
func BannedJS(w http.ResponseWriter, r *http.Request, user User) RouteError {
@ -331,9 +325,8 @@ func CustomError(errmsg string, errcode int, errtitle string, w http.ResponseWri
func CustomErrorJSQ(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, user User, isJs bool) RouteError {
if !isJs {
return CustomError(errmsg, errcode, errtitle, w, r, user)
} else {
return CustomErrorJS(errmsg, errcode, errtitle, w, r, user)
}
return CustomErrorJS(errmsg, errcode, errtitle, w, r, user)
}
// CustomErrorJS is the pure JSON version of CustomError

View File

@ -0,0 +1,7 @@
{
"UName":"guilds",
"Name":"Guilds",
"Author":"Azareal",
"URL":"https://github.com/Azareal/Gosora",
"Skip":true
}

View File

@ -66,30 +66,30 @@ type MemoryForumStore struct {
}
// NewMemoryForumStore gives you a new instance of MemoryForumStore
func NewMemoryForumStore() *MemoryForumStore {
func NewMemoryForumStore() (*MemoryForumStore, error) {
getStmt, err := qgen.Builder.SimpleSelect("forums", "name, desc, active, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID", "fid = ?", "", "")
if err != nil {
log.Fatal(err)
return nil, err
}
getAllStmt, err := qgen.Builder.SimpleSelect("forums", "fid, name, desc, active, preset, parentID, parentType, topicCount, lastTopicID, lastReplyerID", "", "fid ASC", "")
if err != nil {
log.Fatal(err)
return nil, err
}
// TODO: Do a proper delete
deleteStmt, err := qgen.Builder.SimpleUpdate("forums", "name= '', active = 0", "fid = ?")
if err != nil {
log.Fatal(err)
return nil, err
}
forumCountStmt, err := qgen.Builder.SimpleCount("forums", "name != ''", "")
if err != nil {
log.Fatal(err)
return nil, err
}
return &MemoryForumStore{
get: getStmt,
getAll: getAllStmt,
delete: deleteStmt,
getForumCount: forumCountStmt,
}
}, nil
}
// TODO: Add support for subforums

View File

@ -11,7 +11,7 @@ type GroupAdmin struct {
CanDelete bool
}
// ! Fix the data races
// ! Fix the data races in the fperms
type Group struct {
ID int
Name string
@ -27,26 +27,31 @@ type Group struct {
CanSee []int // The IDs of the forums this group can see
}
// TODO: Reload the group from the database rather than modifying it via it's pointer
func (group *Group) ChangeRank(isAdmin bool, isMod bool, isBanned bool) (err error) {
_, err = updateGroupRankStmt.Exec(isAdmin, isMod, isBanned, group.ID)
if err != nil {
return err
}
group.IsAdmin = isAdmin
group.IsMod = isMod
if isAdmin || isMod {
group.IsBanned = false
} else {
group.IsBanned = isBanned
}
gstore.Reload(group.ID)
return nil
}
// ! Ahem, don't listen to the comment below. It's not concurrency safe right now.
// Copy gives you a non-pointer concurrency safe copy of the group
func (group *Group) Copy() Group {
return *group
}
// TODO: Replace this sorting mechanism with something a lot more efficient
// ? - Use sort.Slice instead?
type SortGroup []*Group
func (sg SortGroup) Len() int {
return len(sg)
}
func (sg SortGroup) Swap(i, j int) {
sg[i], sg[j] = sg[j], sg[i]
}
func (sg SortGroup) Less(i, j int) bool {
return sg[i].ID < sg[j].ID
}

View File

@ -2,16 +2,16 @@
package main
import (
"database/sql"
"encoding/json"
"errors"
"log"
"sort"
"sync"
"./query_gen/lib"
)
var groupCreateMutex sync.Mutex
var groupUpdateMutex sync.Mutex
var gstore GroupStore
// ? - We could fallback onto the database when an item can't be found in the cache?
@ -24,23 +24,40 @@ type GroupStore interface {
Create(name string, tag string, isAdmin bool, isMod bool, isBanned bool) (int, error)
GetAll() ([]*Group, error)
GetRange(lower int, higher int) ([]*Group, error)
Reload(id int) error // ? - Should we move this to GroupCache? It might require us to do some unnecessary casting though
GlobalCount() int
}
type GroupCache interface {
CacheSet(group *Group) error
Length() int
}
type MemoryGroupStore struct {
groups []*Group // TODO: Use a sync.Map instead of a slice
groupCapCount int
groups map[int]*Group // TODO: Use a sync.Map instead of a map?
groupCount int
get *sql.Stmt
sync.RWMutex
}
func NewMemoryGroupStore() *MemoryGroupStore {
return &MemoryGroupStore{}
func NewMemoryGroupStore() (*MemoryGroupStore, error) {
getGroupStmt, err := qgen.Builder.SimpleSelect("users_groups", "name, permissions, plugin_perms, is_mod, is_admin, is_banned, tag", "gid = ?", "", "")
if err != nil {
return nil, err
}
return &MemoryGroupStore{
groups: make(map[int]*Group),
groupCount: 0,
get: getGroupStmt,
}, nil
}
func (mgs *MemoryGroupStore) LoadGroups() error {
mgs.groups = []*Group{&Group{ID: 0, Name: "Unknown"}}
mgs.Lock()
defer mgs.Unlock()
mgs.groups[0] = &Group{ID: 0, Name: "Unknown"}
rows, err := getGroupsStmt.Query()
if err != nil {
@ -50,13 +67,95 @@ func (mgs *MemoryGroupStore) LoadGroups() error {
i := 1
for ; rows.Next(); i++ {
group := Group{ID: 0}
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)
err = mgs.initGroup(group)
if err != nil {
return err
}
mgs.groups[group.ID] = group
}
err = rows.Err()
if err != nil {
return err
}
mgs.groupCount = i
if dev.DebugMode {
log.Print("Binding the Not Loggedin Group")
}
GuestPerms = mgs.dirtyGetUnsafe(6).Perms
return nil
}
// TODO: Hit the database when the item isn't in memory
func (mgs *MemoryGroupStore) dirtyGetUnsafe(gid int) *Group {
group, ok := mgs.groups[gid]
if !ok {
return &blankGroup
}
return group
}
// TODO: Hit the database when the item isn't in memory
func (mgs *MemoryGroupStore) DirtyGet(gid int) *Group {
mgs.RLock()
group, ok := mgs.groups[gid]
mgs.RUnlock()
if !ok {
return &blankGroup
}
return group
}
// TODO: Hit the database when the item isn't in memory
func (mgs *MemoryGroupStore) Get(gid int) (*Group, error) {
mgs.RLock()
group, ok := mgs.groups[gid]
mgs.RUnlock()
if !ok {
return nil, ErrNoRows
}
return group, nil
}
// TODO: Hit the database when the item isn't in memory
func (mgs *MemoryGroupStore) GetCopy(gid int) (Group, error) {
mgs.RLock()
group, ok := mgs.groups[gid]
mgs.RUnlock()
if !ok {
return blankGroup, ErrNoRows
}
return *group, nil
}
func (mgs *MemoryGroupStore) Reload(id int) error {
var group = &Group{ID: id}
err := mgs.get.QueryRow(id).Scan(&group.Name, &group.PermissionsText, &group.PluginPermsText, &group.IsMod, &group.IsAdmin, &group.IsBanned, &group.Tag)
if err != nil {
return err
}
err = mgs.initGroup(group)
if err != nil {
LogError(err)
}
mgs.CacheSet(group)
err = rebuildGroupPermissions(id)
if err != nil {
LogError(err)
}
return nil
}
func (mgs *MemoryGroupStore) initGroup(group *Group) error {
err := json.Unmarshal(group.PermissionsText, &group.Perms)
if err != nil {
return err
}
@ -78,51 +177,26 @@ func (mgs *MemoryGroupStore) LoadGroups() error {
if group.IsAdmin || group.IsMod {
group.IsBanned = false
}
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) CacheSet(group *Group) error {
mgs.Lock()
mgs.groups[group.ID] = group
mgs.Unlock()
return nil
}
// TODO: Hit the database when the item isn't in memory
func (mgs *MemoryGroupStore) Exists(gid int) bool {
return (gid <= mgs.groupCapCount) && (gid >= 0) && mgs.groups[gid].Name != ""
mgs.RLock()
group, ok := mgs.groups[gid]
mgs.RUnlock()
return ok && group.Name != ""
}
// ? Allow two groups with the same name?
func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod bool, isBanned bool) (int, error) {
groupCreateMutex.Lock()
defer groupCreateMutex.Unlock()
func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod bool, isBanned bool) (gid int, err error) {
var permstr = "{}"
tx, err := db.Begin()
if err != nil {
@ -143,7 +217,7 @@ func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod
if err != nil {
return 0, err
}
var gid = int(gid64)
gid = int(gid64)
var perms = BlankPerms
var blankForums []ForumPerms
@ -199,8 +273,10 @@ func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod
isBanned = false
}
mgs.groups = append(mgs.groups, &Group{gid, name, isMod, isAdmin, isBanned, tag, perms, []byte(permstr), pluginPerms, pluginPermsBytes, blankForums, blankIntList})
mgs.groupCapCount++
mgs.Lock()
mgs.groups[gid] = &Group{gid, name, isMod, isAdmin, isBanned, tag, perms, []byte(permstr), pluginPerms, pluginPermsBytes, blankForums, blankIntList}
mgs.groupCount++
mgs.Unlock()
for _, forum := range fdata {
err = rebuildForumPermissions(forum.ID)
@ -212,34 +288,62 @@ func (mgs *MemoryGroupStore) Create(name string, tag string, isAdmin bool, isMod
return gid, nil
}
// ! NOT CONCURRENT
func (mgs *MemoryGroupStore) GetAll() ([]*Group, error) {
func (mgs *MemoryGroupStore) GetAll() (results []*Group, err error) {
var i int
mgs.RLock()
results = make([]*Group, len(mgs.groups))
for _, group := range mgs.groups {
results[i] = group
i++
}
mgs.RUnlock()
sort.Sort(SortGroup(results))
return results, nil
}
func (mgs *MemoryGroupStore) GetAllMap() (map[int]*Group, error) {
mgs.RLock()
defer mgs.RUnlock()
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
// TODO: Might be a little slow right now, maybe we can cache the groups in a slice or break the map up into chunks
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 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:]
}
mgs.RLock()
for gid, group := range mgs.groups {
if gid >= lower && (gid <= higher || higher == 0) {
groups = append(groups, group)
}
}
mgs.RUnlock()
sort.Sort(SortGroup(groups))
return groups, nil
}
func (mgs *MemoryGroupStore) Length() int {
return len(mgs.groups)
mgs.RLock()
defer mgs.RUnlock()
return mgs.groupCount
}
func (mgs *MemoryGroupStore) GlobalCount() int {
mgs.RLock()
defer mgs.RUnlock()
return mgs.groupCount
}

View File

@ -37,10 +37,13 @@ func TestUserStore(t *testing.T) {
initPlugins()
}
users = NewMemoryUserStore(config.UserCacheCapacity)
var err error
users, err = NewMemoryUserStore(config.UserCacheCapacity)
expectNilErr(t, err)
users.(UserCache).Flush()
userStoreTest(t, 2)
users = NewSQLUserStore()
users, err = NewSQLUserStore()
expectNilErr(t, err)
userStoreTest(t, 3)
}
func userStoreTest(t *testing.T, newUserID int) {
@ -453,9 +456,12 @@ func TestTopicStore(t *testing.T) {
initPlugins()
}
topics = NewMemoryTopicStore(config.TopicCacheCapacity)
var err error
topics, err = NewMemoryTopicStore(config.TopicCacheCapacity)
expectNilErr(t, err)
topicStoreTest(t)
topics = NewSQLTopicStore()
topics, err = NewSQLTopicStore()
expectNilErr(t, err)
topicStoreTest(t)
}
func topicStoreTest(t *testing.T) {

View File

@ -1485,8 +1485,6 @@ func routePanelGroupsEditSubmit(w http.ResponseWriter, r *http.Request, user Use
originalRank = "Member"
}
groupUpdateMutex.Lock()
defer groupUpdateMutex.Unlock()
if rank != originalRank {
if !user.Perms.EditGroupGlobalPerms {
return LocalError("You need the EditGroupGlobalPerms permission to change the group type.", w, r, user)
@ -1517,12 +1515,12 @@ func routePanelGroupsEditSubmit(w http.ResponseWriter, r *http.Request, user Use
}
}
// TODO: Move this to *Group
_, err = updateGroupStmt.Exec(gname, gtag, gid)
if err != nil {
return InternalError(err, w, r)
}
group.Name = gname
group.Tag = gtag
gstore.Reload(gid)
http.Redirect(w, r, "/panel/groups/edit/"+strconv.Itoa(gid), http.StatusSeeOther)
return nil

View File

@ -4,7 +4,6 @@ import (
"database/sql"
"encoding/json"
"log"
"strconv"
"sync"
"./query_gen/lib"
@ -434,47 +433,9 @@ func rebuildForumPermissions(fid int) error {
forumPerms[gid][fid] = pperms
}
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(group.ID))
}
group.Forums = []ForumPerms{BlankForumPerms}
group.CanSee = []int{}
for _, ffid := range fids {
forumPerm, ok := forumPerms[group.ID][ffid]
if ok {
//log.Print("Overriding permissions for forum #" + strconv.Itoa(fid))
group.Forums = append(group.Forums, forumPerm)
} else {
//log.Print("Inheriting from default for forum #" + strconv.Itoa(fid))
forumPerm = BlankForumPerms
group.Forums = append(group.Forums, forumPerm)
}
if forumPerm.Overrides {
if forumPerm.ViewTopic {
group.CanSee = append(group.CanSee, ffid)
}
} else if group.Perms.ViewTopic {
group.CanSee = append(group.CanSee, ffid)
}
}
if dev.SuperDebug {
log.Printf("group.CanSee %+v\n", group.CanSee)
log.Printf("group.Forums %+v\n", group.Forums)
log.Print("len(group.CanSee)", len(group.CanSee))
log.Print("len(group.Forums)", len(group.Forums)) // This counts blank aka 0
}
}
return nil
return cascadePermSetToGroups(forumPerms, fids)
}
// ? - We could have buildForumPermissions and rebuildForumPermissions call a third function containing common logic?
func buildForumPermissions() error {
fids, err := fstore.GetAllIDs()
if err != nil {
@ -496,6 +457,7 @@ func buildForumPermissions() error {
log.Print("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() {
@ -529,6 +491,10 @@ func buildForumPermissions() error {
forumPerms[gid][fid] = pperms
}
return cascadePermSetToGroups(forumPerms, fids)
}
func cascadePermSetToGroups(forumPerms map[int]map[int]ForumPerms, fids []int) error {
groups, err := gstore.GetAll()
if err != nil {
return err
@ -536,10 +502,23 @@ func buildForumPermissions() error {
for _, group := range groups {
if dev.DebugMode {
log.Print("Adding the forum permissions for Group #" + strconv.Itoa(group.ID) + " - " + group.Name)
log.Printf("Updating the forum permissions for Group #%d", group.ID)
}
group.Forums = []ForumPerms{BlankForumPerms}
group.CanSee = []int{}
cascadePermSetToGroup(forumPerms, group, fids)
if dev.SuperDebug {
log.Printf("group.CanSee %+v\n", group.CanSee)
log.Printf("group.Forums %+v\n", group.Forums)
log.Print("len(group.CanSee): ", len(group.CanSee))
log.Print("len(group.Forums): ", len(group.Forums)) // This counts blank aka 0
}
}
return nil
}
func cascadePermSetToGroup(forumPerms map[int]map[int]ForumPerms, group *Group, fids []int) {
for _, fid := range fids {
if dev.SuperDebug {
log.Printf("Forum #%+v\n", fid)
@ -569,14 +548,6 @@ func buildForumPermissions() error {
log.Print("group.CanSee: ", group.CanSee)
}
}
if dev.SuperDebug {
log.Printf("group.CanSee %+v\n", group.CanSee)
log.Printf("group.Forums %+v\n", group.Forums)
log.Print("len(group.CanSee)", len(group.CanSee))
log.Print("len(group.Forums)", len(group.Forums)) // This counts blank aka 0
}
}
return nil
}
func forumPermsToGroupForumPreset(fperms ForumPerms) string {

View File

@ -67,7 +67,7 @@ type GuildListPage struct {
Title string
CurrentUser User
Header *HeaderVars
GroupList []*Guild
GuildList []*Guild
}
type GuildMemberListPage struct {

View File

@ -18,6 +18,7 @@ type PluginMeta struct {
Settings string
Tag string
Skip bool // Skip this folder?
Main string // The main file
Hooks map[string]string // Hooks mapped to functions
}
@ -62,6 +63,9 @@ func InitPluginLangs() error {
if err != nil {
return err
}
if plugin.Skip {
continue
}
if plugin.UName == "" {
return errors.New("The UName field must not be blank on plugin '" + pluginItem + "'")

View File

@ -2,12 +2,12 @@
<main>
<div class="rowblock rowhead">
<div class="rowitem"><h1>Create Group</h1></div>
<div class="rowitem"><h1>Create Guild</h1></div>
</div>
<div class="rowblock">
<form action="/group/create/submit/" method="post">
<form action="/guild/create/submit/" method="post">
<div class="formrow">
<div class="formitem formlabel"><a>Group Name</a></div>
<div class="formitem formlabel"><a>Guild Name</a></div>
<div class="formitem"><input name="group_name" type="text" placeholder="Group Name" /></div>
</div>
<div class="formrow">
@ -25,7 +25,7 @@
</div>
</div>
<div class="formrow">
<div class="formitem"><button name="group_button" class="formbutton">Create Group</button></div>
<div class="formitem"><button name="group_button" class="formbutton">Create Guild</button></div>
</div>
</form>
</div>

View File

@ -1,10 +1,10 @@
{{template "header.html" . }}
<main>
<div class="rowblock opthead">
<div class="rowitem"><a>Group List</a></div>
<div class="rowitem"><a>Guild List</a></div>
</div>
<div class="rowblock">
{{range .GroupList}}<div class="rowitem datarow">
{{range .GuildList}}<div class="rowitem datarow">
<span style="float: left;">
<a href="{{.Link}}" style="">{{.Name}}</a>
<br /><span class="rowsmall">{{.Desc}}</span>
@ -15,7 +15,7 @@
</span>
<div style="clear: both;"></div>
</div>
{{else}}<div class="rowitem passive">There aren't any visible groups.</div>{{end}}
{{else}}<div class="rowitem passive">There aren't any visible guilds.</div>{{end}}
</div>
</main>
{{template "footer.html" . }}

View File

@ -1,19 +1,20 @@
{{template "header.html" . }}
{{/** TODO: Move this into a CSS file **/}}
{{template "socialgroups_css.html" . }}
{{/** TODO: Move this into a per-theme CSS file **/}}
{{template "guilds_css.html" . }}
{{/** TODO: Add <link> next / prev bits **/}}
{{/** TODO: Port the page template functions to the template interpreter **/}}
{{if gt .Page 1}}<div id="prevFloat" class="prev_button"><a class="prev_link" href="/group/members/{{.SocialGroup.ID}}?page={{subtract .Page 1}}">&lt;</a></div>{{end}}
{{if ne .LastPage .Page}}<link rel="prerender" href="/group/members/{{.SocialGroup.ID}}?page={{add .Page 1}}" />
<div id="nextFloat" class="next_button"><a class="next_link" href="/group/members/{{.SocialGroup.ID}}?page={{add .Page 1}}">&gt;</a></div>{{end}}
{{if gt .Page 1}}<div id="prevFloat" class="prev_button"><a class="prev_link" href="/guild/members/{{.Guild.ID}}?page={{subtract .Page 1}}">&lt;</a></div>{{end}}
{{if ne .LastPage .Page}}<link rel="prerender" href="/guild/members/{{.Guild.ID}}?page={{add .Page 1}}" />
<div id="nextFloat" class="next_button"><a class="next_link" href="/guild/members/{{.Guild.ID}}?page={{add .Page 1}}">&gt;</a></div>{{end}}
<div class="sgBackdrop">
<nav class="miniMenu">
<div class="menuItem"><a href="/group/{{.SocialGroup.ID}}">{{.SocialGroup.Name}}</a></div>
<div class="menuItem"><a href="/guild/{{.Guild.ID}}">{{.Guild.Name}}</a></div>
<div class="menuItem"><a href="#">About</a></div>
<div class="menuItem"><a href="/group/members/{{.SocialGroup.ID}}">Members</a></div>
<div class="menuItem"><a href="/guild/members/{{.Guild.ID}}">Members</a></div>
<div class="menuItem rightMenu"><a href="#">Edit</a></div>
<div class="menuItem rightMenu"><a href="/group/join/{{.SocialGroup.ID}}">Join</a></div>
<div class="menuItem rightMenu"><a href="/guild/join/{{.Guild.ID}}">Join</a></div>
</nav>
<div style="clear: both;"></div>
</div>
@ -25,7 +26,7 @@
</span>
<span>
<a class="rowtopic" href="{{.Link}}">{{.User.Name}}</a>
{{/** Use this for badges instead of rank? Both? Group Titles? **/}}
{{/** Use this for badges instead of rank? Both? Guild Titles? **/}}
<br /><span class="rowsmall postCount">{{.PostCount}} posts</span>
</span>
</div>

View File

@ -3,18 +3,18 @@
{{template "socialgroups_css.html" . }}
{{/** TODO: Port the page template functions to the template interpreter **/}}
{{if gt .Page 1}}<div id="prevFloat" class="prev_button"><a class="prev_link" href="/group/{{.SocialGroup.ID}}?page={{subtract .Page 1}}">&lt;</a></div>{{end}}
{{if ne .LastPage .Page}}<link rel="prerender" href="/group/{{.SocialGroup.ID}}?page={{add .Page 1}}" />
<div id="nextFloat" class="next_button"><a class="next_link" href="/group/{{.SocialGroup.ID}}?page={{add .Page 1}}">&gt;</a></div>{{end}}
{{if gt .Page 1}}<div id="prevFloat" class="prev_button"><a class="prev_link" href="/guild/{{.Guild.ID}}?page={{subtract .Page 1}}">&lt;</a></div>{{end}}
{{if ne .LastPage .Page}}<link rel="prerender" href="/guild/{{.Guild.ID}}?page={{add .Page 1}}" />
<div id="nextFloat" class="next_button"><a class="next_link" href="/guild/{{.Guild.ID}}?page={{add .Page 1}}">&gt;</a></div>{{end}}
<div class="sgBackdrop">
<nav class="miniMenu">
<div class="menuItem"><a href="/group/{{.SocialGroup.ID}}">{{.SocialGroup.Name}}</a></div>
<div class="menuItem"><a href="/guild/{{.Guild.ID}}">{{.Guild.Name}}</a></div>
<div class="menuItem"><a href="#">About</a></div>
<div class="menuItem"><a href="/group/members/{{.SocialGroup.ID}}">Members</a></div>
<div class="menuItem"><a href="/guild/members/{{.Guild.ID}}">Members</a></div>
<div class="menuItem rightMenu"><a href="#">Edit</a></div>
<div class="menuItem rightMenu"><a href="/topics/create/{{.Forum.ID}}">Reply</a></div>
<div class="menuItem rightMenu"><a href="/group/join/{{.SocialGroup.ID}}">Join</a></div>
<div class="menuItem rightMenu"><a href="/guild/join/{{.Guild.ID}}">Join</a></div>
</nav>
<div style="clear: both;"></div>
</div>

View File

@ -9,7 +9,6 @@ package main
import (
"database/sql"
"errors"
"log"
"strings"
"sync"
"sync/atomic"
@ -63,18 +62,18 @@ type MemoryTopicStore struct {
}
// NewMemoryTopicStore gives you a new instance of MemoryTopicStore
func NewMemoryTopicStore(capacity int) *MemoryTopicStore {
func NewMemoryTopicStore(capacity int) (*MemoryTopicStore, error) {
getStmt, err := qgen.Builder.SimpleSelect("topics", "title, content, createdBy, createdAt, lastReplyAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, data", "tid = ?", "", "")
if err != nil {
log.Fatal(err)
return nil, err
}
existsStmt, err := qgen.Builder.SimpleSelect("topics", "tid", "tid = ?", "", "")
if err != nil {
log.Fatal(err)
return nil, err
}
topicCountStmt, err := qgen.Builder.SimpleCount("topics", "", "")
if err != nil {
log.Fatal(err)
return nil, err
}
return &MemoryTopicStore{
items: make(map[int]*Topic),
@ -82,7 +81,7 @@ func NewMemoryTopicStore(capacity int) *MemoryTopicStore {
get: getStmt,
exists: existsStmt,
topicCount: topicCountStmt,
}
}, nil
}
func (mts *MemoryTopicStore) CacheGet(id int) (*Topic, error) {
@ -267,24 +266,24 @@ type SQLTopicStore struct {
topicCount *sql.Stmt
}
func NewSQLTopicStore() *SQLTopicStore {
func NewSQLTopicStore() (*SQLTopicStore, error) {
getStmt, err := qgen.Builder.SimpleSelect("topics", "title, content, createdBy, createdAt, lastReplyAt, is_closed, sticky, parentID, ipaddress, postCount, likeCount, data", "tid = ?", "", "")
if err != nil {
log.Fatal(err)
return nil, err
}
existsStmt, err := qgen.Builder.SimpleSelect("topics", "tid", "tid = ?", "", "")
if err != nil {
log.Fatal(err)
return nil, err
}
topicCountStmt, err := qgen.Builder.SimpleCount("topics", "", "")
if err != nil {
log.Fatal(err)
return nil, err
}
return &SQLTopicStore{
get: getStmt,
exists: existsStmt,
topicCount: topicCountStmt,
}
}, nil
}
func (sts *SQLTopicStore) Get(id int) (*Topic, error) {

14
user.go
View File

@ -12,6 +12,7 @@ import (
"database/sql"
"errors"
"strconv"
"strings"
"time"
"./query_gen/lib"
@ -65,6 +66,19 @@ type Email struct {
Token string
}
func (user *User) Init() {
if user.Avatar != "" {
if user.Avatar[0] == '.' {
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
}
} else {
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.initPerms()
}
func (user *User) Ban(duration time.Duration, issuedBy int) error {
return user.ScheduleGroupUpdate(banGroup, issuedBy, duration)
}

View File

@ -5,7 +5,6 @@ import (
"errors"
"log"
"strconv"
"strings"
"sync"
"sync/atomic"
@ -56,32 +55,32 @@ type MemoryUserStore struct {
}
// NewMemoryUserStore gives you a new instance of MemoryUserStore
func NewMemoryUserStore(capacity int) *MemoryUserStore {
func NewMemoryUserStore(capacity int) (*MemoryUserStore, error) {
getStmt, err := qgen.Builder.SimpleSelect("users", "name, group, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, last_ip, temp_group", "uid = ?", "", "")
if err != nil {
log.Fatal(err)
return nil, err
}
existsStmt, err := qgen.Builder.SimpleSelect("users", "uid", "uid = ?", "", "")
if err != nil {
log.Fatal(err)
return nil, err
}
// Add an admin version of register_stmt with more flexibility?
// create_account_stmt, err = db.Prepare("INSERT INTO
registerStmt, err := qgen.Builder.SimpleInsert("users", "name, email, password, salt, group, is_super_admin, session, active, message, createdAt, lastActiveAt", "?,?,?,?,?,0,'',?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP()")
if err != nil {
log.Fatal(err)
return nil, err
}
usernameExistsStmt, err := qgen.Builder.SimpleSelect("users", "name", "name = ?", "", "")
if err != nil {
log.Fatal(err)
return nil, err
}
userCountStmt, err := qgen.Builder.SimpleCount("users", "", "")
if err != nil {
log.Fatal(err)
return nil, err
}
return &MemoryUserStore{
@ -92,7 +91,7 @@ func NewMemoryUserStore(capacity int) *MemoryUserStore {
register: registerStmt,
usernameExists: usernameExistsStmt,
userCount: userCountStmt,
}
}, nil
}
func (mus *MemoryUserStore) CacheGet(id int) (*User, error) {
@ -124,17 +123,7 @@ func (mus *MemoryUserStore) 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)
// TODO: Add an init method to User rather than writing this same bit of code over and over
if user.Avatar != "" {
if user.Avatar[0] == '.' {
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
}
} else {
user.Avatar = strings.Replace(config.Noavatar, "{id}", strconv.Itoa(user.ID), 1)
}
user.Link = buildProfileURL(nameToSlug(user.Name), id)
user.Tag = gstore.DirtyGet(user.Group).Tag
user.initPerms()
user.Init()
if err == nil {
mus.CacheSet(user)
}
@ -203,18 +192,8 @@ func (mus *MemoryUserStore) BulkGetMap(ids []int) (list map[int]*User, err error
return nil, err
}
// TODO: Add an init method to User rather than writing this same bit of code over and over
// Initialise the user
if user.Avatar != "" {
if user.Avatar[0] == '.' {
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
}
} else {
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.initPerms()
user.Init()
// Add it to the cache...
_ = mus.CacheSet(user)
@ -255,17 +234,7 @@ func (mus *MemoryUserStore) 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)
// TODO: Add an init method to User rather than writing this same bit of code over and over
if user.Avatar != "" {
if user.Avatar[0] == '.' {
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
}
} else {
user.Avatar = strings.Replace(config.Noavatar, "{id}", strconv.Itoa(user.ID), 1)
}
user.Link = buildProfileURL(nameToSlug(user.Name), id)
user.Tag = gstore.DirtyGet(user.Group).Tag
user.initPerms()
user.Init()
return user, err
}
@ -277,17 +246,7 @@ func (mus *MemoryUserStore) Reload(id int) error {
return err
}
// TODO: Add an init method to User rather than writing this same bit of code over and over
if user.Avatar != "" {
if user.Avatar[0] == '.' {
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
}
} else {
user.Avatar = strings.Replace(config.Noavatar, "{id}", strconv.Itoa(user.ID), 1)
}
user.Link = buildProfileURL(nameToSlug(user.Name), id)
user.Tag = gstore.DirtyGet(user.Group).Tag
user.initPerms()
user.Init()
_ = mus.CacheSet(user)
return nil
}
@ -425,32 +384,32 @@ type SQLUserStore struct {
userCount *sql.Stmt
}
func NewSQLUserStore() *SQLUserStore {
func NewSQLUserStore() (*SQLUserStore, error) {
getStmt, err := qgen.Builder.SimpleSelect("users", "name, group, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, last_ip, temp_group", "uid = ?", "", "")
if err != nil {
log.Fatal(err)
return nil, err
}
existsStmt, err := qgen.Builder.SimpleSelect("users", "uid", "uid = ?", "", "")
if err != nil {
log.Fatal(err)
return nil, err
}
// Add an admin version of register_stmt with more flexibility?
// create_account_stmt, err = db.Prepare("INSERT INTO
registerStmt, err := qgen.Builder.SimpleInsert("users", "name, email, password, salt, group, is_super_admin, session, active, message, createdAt, lastActiveAt", "?,?,?,?,?,0,'',?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP()")
if err != nil {
log.Fatal(err)
return nil, err
}
usernameExistsStmt, err := qgen.Builder.SimpleSelect("users", "name", "name = ?", "", "")
if err != nil {
log.Fatal(err)
return nil, err
}
userCountStmt, err := qgen.Builder.SimpleCount("users", "", "")
if err != nil {
log.Fatal(err)
return nil, err
}
return &SQLUserStore{
@ -459,23 +418,14 @@ func NewSQLUserStore() *SQLUserStore {
register: registerStmt,
usernameExists: usernameExistsStmt,
userCount: userCountStmt,
}
}, nil
}
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)
if user.Avatar != "" {
if user.Avatar[0] == '.' {
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
}
} else {
user.Avatar = strings.Replace(config.Noavatar, "{id}", strconv.Itoa(user.ID), 1)
}
user.Link = buildProfileURL(nameToSlug(user.Name), id)
user.Tag = gstore.DirtyGet(user.Group).Tag
user.initPerms()
user.Init()
return user, err
}
@ -508,16 +458,7 @@ func (mus *SQLUserStore) BulkGetMap(ids []int) (list map[int]*User, err error) {
}
// Initialise the user
if user.Avatar != "" {
if user.Avatar[0] == '.' {
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
}
} else {
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.initPerms()
user.Init()
// Add it to the list to be returned
list[user.ID] = user
@ -530,16 +471,7 @@ 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)
if user.Avatar != "" {
if user.Avatar[0] == '.' {
user.Avatar = "/uploads/avatar_" + strconv.Itoa(user.ID) + user.Avatar
}
} else {
user.Avatar = strings.Replace(config.Noavatar, "{id}", strconv.Itoa(user.ID), 1)
}
user.Link = buildProfileURL(nameToSlug(user.Name), id)
user.Tag = gstore.DirtyGet(user.Group).Tag
user.initPerms()
user.Init()
return user, err
}