diff --git a/common/user_store.go b/common/user_store.go index 78b3fa67..3f7ae084 100644 --- a/common/user_store.go +++ b/common/user_store.go @@ -14,12 +14,15 @@ import ( var Users UserStore var ErrAccountExists = errors.New("this username is already in use") var ErrLongUsername = errors.New("this username is too long") +var ErrSomeUsersNotFound = errors.New("Unable to find some users") type UserStore interface { DirtyGet(id int) *User Get(id int) (*User, error) Getn(id int) *User GetByName(name string) (*User, error) + BulkGetByName(names []string) (list []*User, err error) + RawBulkGetByNameForConvo(f func(int, string, int, bool, int, int) error, names []string) error Exists(id int) bool SearchOffset(name, email string, gid, offset, perPage int) (users []*User, err error) GetOffset(offset, perPage int) ([]*User, error) @@ -66,7 +69,7 @@ func NewDefaultUserStore(cache UserCache) (*DefaultUserStore, error) { get: acc.Select(u).Columns("name,group,active,is_super_admin,session,email,avatar,message,level,score,posts,liked,last_ip,temp_group,createdAt,enable_embeds,profile_comments,who_can_convo").Where("uid=?").Prepare(), getByName: acc.Select(u).Columns(allCols).Where("name=?").Prepare(), - searchOffset: acc.Select(u).Columns(allCols).Where("(name=? OR ?='') AND (email=? OR ?='') AND (group=? OR ?='')").Orderby("uid ASC").Limit("?,?").Prepare(), + searchOffset: acc.Select(u).Columns(allCols).Where("(name=? OR ?='') AND (email=? OR ?='') AND (group=? OR ?=0)").Orderby("uid ASC").Limit("?,?").Prepare(), getOffset: acc.Select(u).Columns(allCols).Orderby("uid ASC").Limit("?,?").Prepare(), getAll: acc.Select(u).Columns(allCols).Prepare(), @@ -75,7 +78,7 @@ func NewDefaultUserStore(cache UserCache) (*DefaultUserStore, error) { nameExists: acc.Exists(u, "name").Prepare(), count: acc.Count(u).Prepare(), - countSearch: acc.Count(u).Where("(name=? OR ?='') AND (email=? OR ?='') AND (group=? OR ?='')").Prepare(), + countSearch: acc.Count(u).Where("(name LIKE ('%'+?+'%') OR ?='') AND (email=? OR ?='') AND (group=? OR ?=0)").Prepare(), }, acc.FirstError() } @@ -90,6 +93,11 @@ func (s *DefaultUserStore) DirtyGet(id int) *User { return BlankUser() } +func (s *DefaultUserStore) scanUser(r *sql.Row, u *User) (embeds int, err error) { + e := r.Scan(&u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &u.CreatedAt, &embeds, &u.Privacy.ShowComments, &u.Privacy.AllowMessage) + return embeds, e +} + // TODO: Log weird cache errors? Not just here but in every *Cache? func (s *DefaultUserStore) Get(id int) (*User, error) { u, err := s.cache.Get(id) @@ -102,8 +110,7 @@ func (s *DefaultUserStore) Get(id int) (*User, error) { //log.Print("uncached user") u = &User{ID: id, Loggedin: true} - var embeds int - err = s.get.QueryRow(id).Scan(&u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &u.CreatedAt, &embeds, &u.Privacy.ShowComments, &u.Privacy.AllowMessage) + embeds, err := s.scanUser(s.get.QueryRow(id), u) if err == nil { if embeds != -1 { u.ParseSettings = DefaultParseSettings.CopyPtr() @@ -122,8 +129,7 @@ func (s *DefaultUserStore) Getn(id int) *User { } u = &User{ID: id, Loggedin: true} - var embeds int - err := s.get.QueryRow(id).Scan(&u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &u.CreatedAt, &embeds, &u.Privacy.ShowComments, &u.Privacy.AllowMessage) + embeds, err := s.scanUser(s.get.QueryRow(id), u) if err != nil { return nil } @@ -154,6 +160,74 @@ func (s *DefaultUserStore) GetByName(name string) (*User, error) { return u, nil } +// TODO: Optimise the query to avoid preparing it on the spot? Maybe, use knowledge of the most common IN() parameter counts? +// ! This bypasses the cache, use frugally +func (s *DefaultUserStore) BulkGetByName(names []string) (list []*User, err error) { + if len(names) == 0 { + return list, nil + } else if len(names) == 1 { + user, err := s.GetByName(names[0]) + if err != nil { + return list, err + } + return []*User{user}, nil + } + + idList, q := inqbuildstr(names) + rows, err := qgen.NewAcc().Select("users").Columns("uid,name,group,active,is_super_admin,session,email,avatar,message,level,score,posts,liked,last_ip,temp_group,createdAt,enable_embeds,profile_comments,who_can_convo").Where("name IN(" + q + ")").Query(idList...) + if err != nil { + return list, err + } + defer rows.Close() + + var embeds int + for rows.Next() { + u := &User{Loggedin: true} + err := rows.Scan(&u.ID, &u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &u.CreatedAt, &embeds, &u.Privacy.ShowComments, &u.Privacy.AllowMessage) + if err != nil { + return list, err + } + if embeds != -1 { + u.ParseSettings = DefaultParseSettings.CopyPtr() + u.ParseSettings.NoEmbed = embeds == 0 + } + u.Init() + s.cache.Set(u) + list[u.ID] = u + } + if err = rows.Err(); err != nil { + return list, err + } + + // Did we miss any users? + if len(names) > len(list) { + return list, ErrSomeUsersNotFound + } + return list, err +} + +// Special case function for efficiency +func (s *DefaultUserStore) RawBulkGetByNameForConvo(f func(int, string, int, bool, int, int) error, names []string) error { + idList, q := inqbuildstr(names) + rows, e := qgen.NewAcc().Select("users").Columns("uid,name,group,is_super_admin,temp_group,who_can_convo").Where("name IN(" + q + ")").Query(idList...) + if e != nil { + return e + } + defer rows.Close() + for rows.Next() { + var name string + var id, group, temp_group, who_can_convo int + var super_admin bool + if e = rows.Scan(&id, &name, &group, &super_admin, &temp_group, &who_can_convo); e != nil { + return e + } + if e = f(id, name, group, super_admin, temp_group, who_can_convo); e != nil { + return e + } + } + return rows.Err() +} + // TODO: Optimise this, so we don't wind up hitting the database every-time for small gaps // TODO: Make this a little more consistent with DefaultGroupStore's GetRange method func (s *DefaultUserStore) GetOffset(offset, perPage int) (users []*User, err error) { @@ -262,15 +336,7 @@ func (s *DefaultUserStore) BulkGetMap(ids []int) (list map[int]*User, err error) return list, nil } - // TODO: Add a function for the q stuff - var q string - idList := make([]interface{}, len(ids)) - for i, id := range ids { - idList[i] = strconv.Itoa(id) - q += "?," - } - q = q[0 : len(q)-1] - + idList, q := inqbuild(ids) rows, err := qgen.NewAcc().Select("users").Columns("uid,name,group,active,is_super_admin,session,email,avatar,message,level,score,posts,liked,last_ip,temp_group,createdAt,enable_embeds,profile_comments,who_can_convo").Where("uid IN(" + q + ")").Query(idList...) if err != nil { return list, err @@ -316,8 +382,7 @@ func (s *DefaultUserStore) BulkGetMap(ids []int) (list map[int]*User, err error) func (s *DefaultUserStore) BypassGet(id int) (*User, error) { u := &User{ID: id, Loggedin: true} - var embeds int - err := s.get.QueryRow(id).Scan(&u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &u.CreatedAt, &embeds, &u.Privacy.ShowComments, &u.Privacy.AllowMessage) + embeds, err := s.scanUser(s.get.QueryRow(id), u) if err == nil { if embeds != -1 { u.ParseSettings = DefaultParseSettings.CopyPtr()