gosora/common/group_store.go
Azareal c60118e7c4 WIP forum action code. Currently disabled.
Add Http Conn Count tracking.
Move more panel phrases into the panel namespace.
Use a string builder in hookgen.
Use Countf() in a couple of places to eliminate boilerplate.
Reduce prepared stmt boilerplate in forum store with a lambda.
Reduce prepared stmt boilerplate in topic.go with a lambda.
Reduce prepared stmt boilerplate in group.go with a lambda.
Add TestSetCreatedAt method to *Topic.
Add DateOlderThanQ method to *accDeleteBuilder and *accUpdateBuilder.
Add Stmt method to *accUpdateBuilder and *AccSelectBuilder.
Add AccBuilder interface.
Shorten variable names.
Shorten extractPerm name to ep.
Add avatar_visibility setting stub. Implementation coming in a later commit.
Don't set an IP for installer generated posts.

Add counters_perf_tick_row hook.

Add avatar_visibility phrase.
Add avatar_visibility_label phrase.
Rename forums_no_description to forums_no_desc.
Rename panel.forums_create_description_label to panel.forums_create_desc_label.
Rename panel.forums_create_description to panel.forums_create_desc.
Rename panel_forum_description to panel.forum_desc.
Rename panel_forum_description_placeholder to panel.forum_desc_placeholder.
Add panel_debug_http_conns_label phrase.
Add panel.forum_actions_head phrase.
Add panel.forum_actions_create_head phrase.
Add panel.forum_action_run_on_topic_creation phrase.
Add panel.forum_action_run_days_after_topic_creation phrase.
Add panel.forum_action_run_days_after_topic_last_reply phrase.
Add panel.forum_action_action phrase.
Add panel.forum_action_action_delete phrase.
Add panel.forum_action_action_lock phrase.
Add panel.forum_action_action_unlock phrase.
Add panel.forum_action_action_move phrase.
Add panel.forum_action_extra phrase.
Add panel.forum_action_create_button phrase.

You will need to run the patcher / updater for this commit.
2021-04-08 00:23:11 +10:00

376 lines
8.4 KiB
Go

/* Under Heavy Construction */
package common
import (
"database/sql"
"encoding/json"
"errors"
"log"
"sort"
"strconv"
"sync"
qgen "github.com/Azareal/Gosora/query_gen"
)
var Groups GroupStore
// ? - We could fallback onto the database when an item can't be found in the cache?
type GroupStore interface {
LoadGroups() error
DirtyGet(id int) *Group
Get(id int) (*Group, error)
GetCopy(id int) (Group, error)
Exists(id int) bool
Create(name, tag string, isAdmin, isMod, isBanned bool) (id int, err error)
GetAll() ([]*Group, error)
GetRange(lower, higher int) ([]*Group, error)
Reload(id int) error // ? - Should we move this to GroupCache? It might require us to do some unnecessary casting though
Count() int
}
type GroupCache interface {
CacheSet(g *Group) error
SetCanSee(gid int, canSee []int) error
CacheAdd(g *Group) error
Length() int
}
type MemoryGroupStore struct {
groups map[int]*Group // TODO: Use a sync.Map instead of a map?
groupCount int
getAll *sql.Stmt
get *sql.Stmt
count *sql.Stmt
userCount *sql.Stmt
sync.RWMutex
}
func NewMemoryGroupStore() (*MemoryGroupStore, error) {
acc := qgen.NewAcc()
ug := "users_groups"
return &MemoryGroupStore{
groups: make(map[int]*Group),
groupCount: 0,
getAll: acc.Select(ug).Columns("gid,name,permissions,plugin_perms,is_mod,is_admin,is_banned,tag").Prepare(),
get: acc.Select(ug).Columns("name,permissions,plugin_perms,is_mod,is_admin,is_banned,tag").Where("gid=?").Prepare(),
count: acc.Count(ug).Prepare(),
userCount: acc.Count("users").Where("group=?").Prepare(),
}, acc.FirstError()
}
// TODO: Move this query from the global stmt store into this store
func (s *MemoryGroupStore) LoadGroups() error {
s.Lock()
defer s.Unlock()
s.groups[0] = &Group{ID: 0, Name: "Unknown"}
rows, err := s.getAll.Query()
if err != nil {
return err
}
defer rows.Close()
i := 1
for ; rows.Next(); i++ {
g := &Group{ID: 0}
err := rows.Scan(&g.ID, &g.Name, &g.PermissionsText, &g.PluginPermsText, &g.IsMod, &g.IsAdmin, &g.IsBanned, &g.Tag)
if err != nil {
return err
}
err = s.initGroup(g)
if err != nil {
return err
}
s.groups[g.ID] = g
}
if err = rows.Err(); err != nil {
return err
}
s.groupCount = i
DebugLog("Binding the Not Loggedin Group")
GuestPerms = s.dirtyGetUnsafe(6).Perms // ! Race?
TopicListThaw.Thaw()
return nil
}
// TODO: Hit the database when the item isn't in memory
func (s *MemoryGroupStore) dirtyGetUnsafe(id int) *Group {
group, ok := s.groups[id]
if !ok {
return &blankGroup
}
return group
}
// TODO: Hit the database when the item isn't in memory
func (s *MemoryGroupStore) DirtyGet(id int) *Group {
s.RLock()
group, ok := s.groups[id]
s.RUnlock()
if !ok {
return &blankGroup
}
return group
}
// TODO: Hit the database when the item isn't in memory
func (s *MemoryGroupStore) Get(id int) (*Group, error) {
s.RLock()
group, ok := s.groups[id]
s.RUnlock()
if !ok {
return nil, ErrNoRows
}
return group, nil
}
// TODO: Hit the database when the item isn't in memory
func (s *MemoryGroupStore) GetCopy(id int) (Group, error) {
s.RLock()
group, ok := s.groups[id]
s.RUnlock()
if !ok {
return blankGroup, ErrNoRows
}
return *group, nil
}
func (s *MemoryGroupStore) Reload(id int) error {
// TODO: Reload this data too
g, e := s.Get(id)
if e != nil {
LogError(errors.New("can't get cansee data for group #" + strconv.Itoa(id)))
return nil
}
canSee := g.CanSee
g = &Group{ID: id, CanSee: canSee}
e = s.get.QueryRow(id).Scan(&g.Name, &g.PermissionsText, &g.PluginPermsText, &g.IsMod, &g.IsAdmin, &g.IsBanned, &g.Tag)
if e != nil {
return e
}
if e = s.initGroup(g); e != nil {
LogError(e)
return nil
}
s.CacheSet(g)
TopicListThaw.Thaw()
return nil
}
func (s *MemoryGroupStore) initGroup(g *Group) error {
e := json.Unmarshal(g.PermissionsText, &g.Perms)
if e != nil {
log.Printf("g: %+v\n", g)
log.Print("bad group perms: ", g.PermissionsText)
return e
}
DebugLogf(g.Name+": %+v\n", g.Perms)
e = json.Unmarshal(g.PluginPermsText, &g.PluginPerms)
if e != nil {
log.Printf("g: %+v\n", g)
log.Print("bad group plugin perms: ", g.PluginPermsText)
return e
}
DebugLogf(g.Name+": %+v\n", g.PluginPerms)
//group.Perms.ExtData = make(map[string]bool)
// TODO: Can we optimise the bit where this cascades down to the user now?
if g.IsAdmin || g.IsMod {
g.IsBanned = false
}
e = s.userCount.QueryRow(g.ID).Scan(&g.UserCount)
if e != sql.ErrNoRows {
return e
}
return nil
}
func (s *MemoryGroupStore) SetCanSee(gid int, canSee []int) error {
s.Lock()
group, ok := s.groups[gid]
if !ok {
s.Unlock()
return nil
}
ngroup := &Group{}
*ngroup = *group
ngroup.CanSee = canSee
s.groups[group.ID] = ngroup
s.Unlock()
return nil
}
func (s *MemoryGroupStore) CacheSet(g *Group) error {
s.Lock()
s.groups[g.ID] = g
s.Unlock()
return nil
}
// TODO: Hit the database when the item isn't in memory
func (s *MemoryGroupStore) Exists(id int) bool {
s.RLock()
group, ok := s.groups[id]
s.RUnlock()
return ok && group.Name != ""
}
// ? Allow two groups with the same name?
// TODO: Refactor this
func (s *MemoryGroupStore) Create(name, tag string, isAdmin, isMod, isBanned bool) (gid int, err error) {
permstr := "{}"
tx, err := qgen.Builder.Begin()
if err != nil {
return 0, err
}
defer tx.Rollback()
insertTx, err := qgen.Builder.SimpleInsertTx(tx, "users_groups", "name,tag,is_admin,is_mod,is_banned,permissions,plugin_perms", "?,?,?,?,?,?,'{}'")
if err != nil {
return 0, err
}
res, err := insertTx.Exec(name, tag, isAdmin, isMod, isBanned, permstr)
if err != nil {
return 0, err
}
gid64, err := res.LastInsertId()
if err != nil {
return 0, err
}
gid = int(gid64)
perms := BlankPerms
blankIntList := []int{}
pluginPerms := make(map[string]bool)
pluginPermsBytes := []byte("{}")
GetHookTable().Vhook("create_group_preappend", &pluginPerms, &pluginPermsBytes)
// Generate the forum permissions based on the presets...
forums, err := Forums.GetAll()
if err != nil {
return 0, err
}
presetSet := make(map[int]string)
permSet := make(map[int]*ForumPerms)
for _, f := range forums {
var thePreset string
switch {
case isAdmin:
thePreset = "admins"
case isMod:
thePreset = "staff"
case isBanned:
thePreset = "banned"
default:
thePreset = "members"
}
permmap := PresetToPermmap(f.Preset)
permItem := permmap[thePreset]
permItem.Overrides = true
permSet[f.ID] = permItem
presetSet[f.ID] = f.Preset
}
err = ReplaceForumPermsForGroupTx(tx, gid, presetSet, permSet)
if err != nil {
return 0, err
}
err = tx.Commit()
if err != nil {
return 0, err
}
// TODO: Can we optimise the bit where this cascades down to the user now?
if isAdmin || isMod {
isBanned = false
}
s.CacheAdd(&Group{gid, name, isMod, isAdmin, isBanned, tag, perms, []byte(permstr), pluginPerms, pluginPermsBytes, blankIntList, 0})
TopicListThaw.Thaw()
return gid, FPStore.ReloadAll()
//return gid, TopicList.RebuildPermTree()
}
func (s *MemoryGroupStore) CacheAdd(g *Group) error {
s.Lock()
s.groups[g.ID] = g
s.groupCount++
s.Unlock()
return nil
}
func (s *MemoryGroupStore) GetAll() (results []*Group, err error) {
var i int
s.RLock()
results = make([]*Group, len(s.groups))
for _, group := range s.groups {
results[i] = group
i++
}
s.RUnlock()
sort.Sort(SortGroup(results))
return results, nil
}
func (s *MemoryGroupStore) GetAllMap() (map[int]*Group, error) {
s.RLock()
defer s.RUnlock()
return s.groups, nil
}
// ? - Set the lower and higher numbers to 0 to remove the bounds
// 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 (s *MemoryGroupStore) GetRange(lower, higher int) (groups []*Group, err error) {
if lower == 0 && higher == 0 {
return s.GetAll()
}
// TODO: Simplify these four conditionals into two
if lower == 0 {
if higher < 0 {
return nil, errors.New("higher may not be lower than 0")
}
} else if higher == 0 {
if lower < 0 {
return nil, errors.New("lower may not be lower than 0")
}
}
s.RLock()
for gid, group := range s.groups {
if gid >= lower && (gid <= higher || higher == 0) {
groups = append(groups, group)
}
}
s.RUnlock()
sort.Sort(SortGroup(groups))
return groups, nil
}
func (s *MemoryGroupStore) Length() int {
s.RLock()
defer s.RUnlock()
return s.groupCount
}
func (s *MemoryGroupStore) Count() (count int) {
err := s.count.QueryRow().Scan(&count)
if err != nil {
LogError(err)
}
return count
}